pax_global_header00006660000000000000000000000064151103316750014514gustar00rootroot0000000000000052 comment=b58982ba8e771ea422126b9da0f801020ae8e05b strawberry-graphql-0.287.0/000077500000000000000000000000001511033167500155325ustar00rootroot00000000000000strawberry-graphql-0.287.0/.alexignore000066400000000000000000000000261511033167500176660ustar00rootroot00000000000000CHANGELOG.md TWEET.md strawberry-graphql-0.287.0/.alexrc000066400000000000000000000003571511033167500170160ustar00rootroot00000000000000{ "allow": [ "black", "hook", "hooks", "failure", "period", "execute", "executed", "executes", "execution", "reject", "special", "primitive", "invalid", "failed", "crash" ] } strawberry-graphql-0.287.0/.codecov.yml000066400000000000000000000005521511033167500177570ustar00rootroot00000000000000codecov: notify: require_ci_to_pass: yes coverage: precision: 2 round: down range: "70...100" status: project: yes patch: yes changes: no comment: layout: "header, diff" behavior: default require_changes: no ignore: - "strawberry/ext/mypy_plugin.py" - "setup.py" - "strawberry/experimental/pydantic/conversion_types.py" strawberry-graphql-0.287.0/.coveragerc000066400000000000000000000015421511033167500176550ustar00rootroot00000000000000# .coveragerc to control coverage.py [run] branch = True [report] # Regexes for lines to exclude from consideration exclude_lines = # Have to re-enable the standard pragma pragma: no cover # Don't complain about missing debug-only code: def __repr__ if self\.debug # Don't complain if tests don't hit defensive assertion code: raise AssertionError raise NotImplementedError raise UnsupportedTypeError # Don't complain if non-runnable code isn't run: if 0: if __name__ == .__main__.: # Don't complain about abstract methods, they aren't run: @(abc\.)?abstractmethod # Don't complain about TYPE_CHECKING if TYPE_CHECKING: @overload # Those are not supposed to be hit assert_never\(\w+\) ignore_errors = True omit = ./.venv/** noxfile.py [html] directory = coverage_html_report strawberry-graphql-0.287.0/.devcontainer/000077500000000000000000000000001511033167500202715ustar00rootroot00000000000000strawberry-graphql-0.287.0/.devcontainer/Dockerfile000066400000000000000000000002321511033167500222600ustar00rootroot00000000000000ARG VARIANT=3.13 FROM mcr.microsoft.com/devcontainers/python:${VARIANT} RUN pip3 install poetry pre-commit RUN poetry config virtualenvs.in-project true strawberry-graphql-0.287.0/.devcontainer/devcontainer.json000066400000000000000000000015461511033167500236530ustar00rootroot00000000000000{ "name": "Strawberry GraphQL", "build": { "dockerfile": "Dockerfile" }, "customizations": { "vscode": { "settings": { "terminal.integrated.shell.linux": "/bin/bash", "python.pythonPath": "/usr/local/bin/python", "python.linting.enabled": false, "python.linting.pylintEnabled": false, "ruff.enable": true, "ruff.organizeImports": true, "[python]": { "editor.formatOnSave": true, "editor.defaultFormatter": "charliermarsh.ruff", "editor.codeActionsOnSave": { "source.organizeImports.ruff": true } } }, "extensions": [ "ms-python.python", "ms-python.vscode-pylance", "charliermarsh.ruff", "eamodio.gitlens" ] } }, "postCreateCommand": "sh ./.devcontainer/post-install.sh" } strawberry-graphql-0.287.0/.devcontainer/post-install.sh000077500000000000000000000001121511033167500232530ustar00rootroot00000000000000poetry install --with dev,integrations pre-commit install --install-hooks strawberry-graphql-0.287.0/.dockerignore000066400000000000000000000001111511033167500201770ustar00rootroot00000000000000.git .github .benchmarks .devcotainer .venv .mypy_cache .nox .ruff_cache strawberry-graphql-0.287.0/.editorconfig000066400000000000000000000003241511033167500202060ustar00rootroot00000000000000root = true [*] charset = utf-8 indent_style = space [*.py] indent_size = 4 trim_trailing_whitespace = true max_line_length = 88 insert_final_newline = true [*.yml] indent_size = 2 insert_final_newline = true strawberry-graphql-0.287.0/.github/000077500000000000000000000000001511033167500170725ustar00rootroot00000000000000strawberry-graphql-0.287.0/.github/bot-action/000077500000000000000000000000001511033167500211315ustar00rootroot00000000000000strawberry-graphql-0.287.0/.github/bot-action/Dockerfile000066400000000000000000000001621511033167500231220ustar00rootroot00000000000000FROM python:3.10-alpine RUN pip install httpx==0.18.2 COPY . /action ENTRYPOINT ["python", "/action/main.py"] strawberry-graphql-0.287.0/.github/bot-action/action.yml000066400000000000000000000004371511033167500231350ustar00rootroot00000000000000name: 'Strawberry Bot Action' inputs: pr_number: required: true status: required: true change_type: required: true changelog_base64: required: true tweet: required: false release_card_url: required: false runs: using: 'docker' image: 'Dockerfile' strawberry-graphql-0.287.0/.github/bot-action/main.py000066400000000000000000000023441511033167500224320ustar00rootroot00000000000000# TODO: improve this to support multiple commands import base64 import os import httpx API_URL = os.environ["BOT_API_URL"] API_TOKEN = os.environ["API_SECRET"] mutation = """mutation AddReleaseComment($input: AddReleaseFileCommentInput!) { addReleaseFileComment(input: $input) }""" release_info = None if os.environ["INPUT_STATUS"] == "OK": changelog = base64.b64decode(os.environ["INPUT_CHANGELOG_BASE64"]).decode("utf-8") release_info = { "changeType": os.environ["INPUT_CHANGE_TYPE"], "changelog": changelog, } if tweet := os.environ.get("INPUT_TWEET"): tweet = base64.b64decode(tweet).decode("utf-8") mutation_input = { "prNumber": int(os.environ["INPUT_PR_NUMBER"]), "status": os.environ["INPUT_STATUS"], "releaseCardUrl": os.environ["INPUT_RELEASE_CARD_URL"], "tweet": tweet, "releaseInfo": release_info, } response = httpx.post( API_URL, json={"query": mutation, "variables": {"input": mutation_input}}, headers={"Authorization": f"Bearer {API_TOKEN}"}, timeout=120, ) response.raise_for_status() response_data = response.json() if "errors" in response_data: raise RuntimeError(f"Response contained errors: {response_data['errors']}") print(response_data) strawberry-graphql-0.287.0/.github/dependabot.yml000066400000000000000000000003701511033167500217220ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: pip directory: "/" schedule: interval: daily open-pull-requests-limit: 10 groups: all-dependencies: patterns: - "*" allow: - dependency-type: direct strawberry-graphql-0.287.0/.github/logo.png000066400000000000000000002172531511033167500205520ustar00rootroot00000000000000PNG  IHDR,j|g pHYs  sRGBgAMA a@IDATx ]Gu'ZԒ]-ٲX6MbffKBf`̄BY7_!3/o2IYXۘ l,l6n^}U֩?uoߓd[bح޽uN:_N-aի錜}Cy&mfq лj{\!F+.$M&'s=0G?w4XstȐM'pwܱr~u/ _eSS?~~haګZ0{@sAs5GwqkGFW0q999tXj` ;v|@Ö9z^CVBy-X01G-yXsR\ 7o|=谤h"z*sRaKsC9zH(.[Ux)Knھ}aGsC9z)Λ& fG?xp['-}1$m-'a;9zP)U \ h.x*Y[h bY' A+!:z&zܑkJNqW{A׆ù;=h>.Ð.ԋ?R] ??@Æf A5k.ڟ6~(7UKxW/^sዿCK.}WA9N1n>CRV{Q\AASRnً>/a}pX:[X?zq@>j aDs5G  n8XU$){T01űzNjkbt:}A!8k"0Yw L[EOotS?xo/p AوY/64Xst0U _ \#@oeݕs>(n޽\S?x~-Àst`Q^93ϩ=u.G؋귣aXxy'h!ÆkwuP-4i)ӪvO<- wc!a}F49:li#>'rۂqO` 97 H}fUH%)stB}. =Æ0XK_@^/Z~\\7&W?Ho*Ў_9x>>+9m4iV# 0w`7 #Z -[H`Z;842#G; <4?UCŽ.cMzus i^U}TKJfm_}£WӁ瞃!iի7ȏݓ呹+ݧ+yGgqv罅Z„\ 0Cd/]ċpP6ķpGƻy8^N܋="{z[Po~c/qhۋ++Ъ ?N.??9m |0.jm;ʥs< Lyh8L=ÊikO^鏨>joR߯zx3p}p?ڌ);?)sX] JԦ^č|o5 s/T&鷄 _uyųu]y|S (wMÊ9OC=s#P|XIEz3fЦ[[-4 Ώn6bPĞ=@`SxtJ1,P+sΟ:uU1߶g 9X!I}uWWc_ X=1gmIhD:k"ynV%@:/Hu,`{| b?=N/wn~,͋w[ތlYb%s{:iL*h+_OmQ7DryY,bTtb\pC*y0#P}.t*7 ,?2 -ogA7Š/ٴ'W Gu'r|]|q ){bv OxZJ;L>c7:ĂAS xV_cq}h+S;O ;dX.ƝszgUǸDv+gu 0/@9-Ijpeҹ`=uO_m@ˀ+@73B _P;x*aG`%Z;hޯ=R_??Oho tP '㒅kǬ.#H\N7 ؽiH<~YʡVDw~BMWS=S]p9ÁP1z:ƣV'}gʛ\0@saJ剣~ѕ>zڝ;?CU?M\?v.IRsN|pnjSUg% p /qC#|̺78:hgMA{3]wf萢={+~5Jn /w2^J>(Bw -T8\zv^\_=!, =Yn fxX^KE9}"qƲK*zxdrN;[лqz7܎!zsaJ`-L nѡN$^ Z-' U|0CHI#kCn|HJ=AG'Vz'?u0/5]2yruAn'H^ |=Pýh[ם;R0CBt 'mĂ&]~t>y\1uGCW3=Ŏv(VnGz:3Ok{,ݑ)0*^#0}#x)_qu51m4uѹnXW'EtwG=3o{?p*ic嘣Æ!&`Ɓe}wR(v/섣9oxsg'T )zR?ai5Gzݜt&!VL.Zb'9:dmvl Ep ^S<\_{Nl3ü,`<*`4hZQW).)USUtz*C;'CLjS]w,0ϽA qtt-0G;Ritt7 x}ieݻgg;P؉9@zƾ7C">jҗ.$7NrÃ-?<0{)z[_!{Ry]{`?poA. ް'a#C֮;7~q!~>G=aOҊu•WeufHOeWfyKl>3~NZY jko*ũ8u߻u]9"m:k,Yxs3C 4no|Ha96 _ o>ɸs6TeY@Uـv. kwTY_X>|/xᄇ8NZ_ىܒX磿c/v~5]gZYmRLn>\N|}bAOyuIܳy _I;b*{ρWI/꼢aWt>V/^2CDh[#ɖëׇ07E jLgMWy)많* 9љ gz8= @w?z0};hB?ߟ|-͝Hk֬=v^PGMܫao<1eOk=1%1t7@ޛg$?/o`S4:맏/NNWvݼ;ŷ<(K_H(Nu-X#|jƃ9lq˖-7P-l=B! ~T%j  @+v(t*-8cB_yχ<_\PݹlK1?d}ǹ7_:bR@vź/s:zK?@lCoٹO<9ӯܩx=S+=q {Wt /^ruf먣j(\,.XbtWo>dk||Z׵?;^\>ki< F-Aa=LGUt0*)zT Gثb2>LK\yּ1y. ΦL#ӿMV, ‚W{|wT{a/ŃHӽ8D<+3ܯ?QoZAi (GuG[_BPo> pg; s$\^[? _ 8"N>5&4}ܪ hn?|F{ٹH&? hħegÕX\{~ہ/jߋmo-|t0qy!X~A;/;I@X xTTԖ[)ĕ]~gEw}ZHʗaōu7q-{r^'z30c#k3pQ/| ]|`aoΚ>^t֡͟y6\ǂ V^,EG~ _Zl K(m׉.`#m/!c^x<*j$}*WXZykk5^:j7HS ,zpX 0ƫ>9xt}dt)(<\!0=_3 bq7ʨA;/oBqx=g~K V`[v|k%cy*ЙU06/yZ˜ ߹+LLOW}Cćf !XBkC0-pg7>+*lG~]PT 7cY"xjC>eI}ބ-w''MB4 ` F+zS"H-x{ѻګ]oTaʤ n,ף1Œm= @ p% q_W鵕7w嬘?/99Fa?f`:>ݗ=E)]q~mpRܮ\n3ѣX'?O@6Az6It?5{m/hDZƨ!/f oI"`1CL)Evc W .ΟZ+m>r?޾0 I-o:F:xuZ{O՜\*H|| 6ϮJ-9oz[AbZ0}*<1ujwLu#@O)88US41=1}3u")ϕO fy&?a;e h}uUxsmɏDN+e_^]n~iٲѫ'&&S ߪv>c‡IՐPh:$!``R>ECrJCdP^U/"VЊo][ALh많 C94|ym%߼%LdS$Shs{p#=1x uNXYye'Ʉή`=w|{%f?19xw{*|a99^>>XNqXzXB}SUpCxh[(y[:>/cYw50Ô5/g7) ^!W5DE`Skh`6/@k?ހ֎}JZ_H޲qx)u(t%{ޓO߀=iXKh Y_:+!4:vΒ:1Ns|%"EO浻>\^A|?l3[_H@ aк$ v??/E0(ţtW1` h[ǠIx7Vx /~cf`f%zZv%~olK_AObx9Xcϟ{<݋W-2Uw4|֊E^dڼx;W&M9@gZ[|\im}O7}Bvh3yF" u{|;xj}%.l3y}{{]'&&޺tssbgi^s |Tÿq|vYYj iqiLq8x4\@R^3^z,#𩑙&SKI*|ѥzO}[߯Âxn|d\׹ 7u~(&HЉe3d]t 6*A00S<ǂP.zTC;1~+몣XGj96Dkh,|ݓ+e땪ۈF<ݭ1?d`gdMǥ-4KY|[]\pA$%?hɂޣeBx@A~KcYA_ώln=Gk}ꔆq zoH߄|ri= _|Z*?'wBUM]_Ӗ/_z8HŰLqBr+h=='} ^LH焷S\h{[ kOƃ$.;ϭULM10rB1iEj|;\Ao)_2OOagWr3)`p\ ^G 4\OW/\F1$5\dX/ {hA<?l?5%oyʢ]+K +{k23~SO t,Ef(To$ϥ'@[CE8H֡V< ~F`AhA;1Cᶟ^~?`o.RozLbOr7 Zt}啷5gzU0ZEA[_tWgw^Aue^x?(l:`@ `;;W3w/}Z"< p~; =Z< M Qo^ǣX&~/{-xݓхlQ*xcu^AOf{%yxC'@Regމ3LXP34FH;n ԋli'fW@΁'>shiY_}΅eep-zX1ؓ,u ?A ~.y\C *k"=/io"$Ǿ x0j FH.9jzS8'~i#g,~zϽV3VdC#"/v <-e@Kj*2%CQ#epA+I,*%>j'O{U[]s@\LuZF5  f͚^q &:p _Wo$]a8yJZg(\_k8,oxy?ړ0~A>hU/ u4ʫ:~md≭f-;j5/wv-13zZ˺O|!fy$x)*/2G2*)q XIYz*V2|'痍8I切 t@ګ{#ָj=`Ray{oA+k+tx~c~D>{3U)pnsۉ3_̒;ؑnYu5\g~%>( ^N8a%ÁՒVϮ>^A+Ӡob1&3)3{%ge &O hBy롇<(xCqRŊsB BH_ lUNVϩ4-d nٗ^A(Uu\ wۚM C 2 ԛC9zZ~Dw0d5PYc^ILNg0o>xl1h^jX>?e'0yŠc]G`F2 RpiTGKDy!=o u1zثiu2șty#GV^V5_?]fm:nd̗yl| T*~SO%ʺ< M.x .Uᡮ&:w~<\F>7`CXgdshCMEe]u%2NzBuNx}u#FpjUa{zY "X0YV▗e{ V,?~WŽOV1Uj*z-%kgӕ4sj3|O.i+,01- @fyjo0tY 2:9m2::zŞ\`҂N'w;g<ڶ-,g:3t".cES>0aO0{Aax HБ*ob_[˺ vM}X_}xch^AV`V1.=vZL9b=ѯ8F 3gafpjxPռ =xك`c#vv~k: STCWR`pEl# N-g[ .`1|܉>\Ⱦ,ycH{ZfШ{9?_Dz≡Yw< ; |FrfGχ{4PO zrb_{Y7p':N$bYeg^G=k^AKf/$m=_f͹G{K=x3>hLg_f*zK {2?K-QYA@{4aNP"vS_Uc!Idl $+g^gID3zXqӮKj-S,GgқtΠHde#+{+ ɒ%6]n7쏮m崾;ŸW3guU亠A|K1ì} [q{ƟlEW)t^zde܏!h…n>q0<̌xR D'IԿcocȓO+>ı1iǢ۫NUֶe%W#͋ r*9*Y3c =ǭW<:8]cv8^@d"g:t}Ex~(*OZ%G9adErt?J=Z<3ZfR{||3Xq=\udKUapð;({Y|}_UC?^VZAPUCow_x8Okw?:3+r8R93J`^N }8);1`PΪ訧4 !g=#Y0+F[Jleo {xK&n:S˯gۓA; 03*3 >"OrKklTp]0twg[U nr[IޖYt!gwAGEбd_;HxrW~za_cy#S#Pœ,hgôYm9Ziz+**Yў(I= 2ʟJztK ɀgPg@3p{6z xh @I_iaAEsmc;"#GW-Ԇ\}>u,i-h, .,c3GNVLA9w_FgQ^1Lz``82|n۫bG K"^y !=#q[ atJE2ޓkSr6z*PeB="+#yd Ȉ쭐|T1,m~R+$JsSwZ*`Xʐ%7yj 6$"=S[>'|}gg: ;cT᠉$t08#Ϯ>= '#-Q]T@#X^yx?$|`r'_('+iè9wx |4<[)7`MB^Y*, Z֧:UmۺyCx^kX/kPvؙ"eևCU3ojS}"K@hNrw &/k\K /"5oA#։u$B4>sy&&rs|{𤄦R]gYhfri C!A X`C\qTl5d =˪]ϟ?u9cS9(d36X7r7=c:FT#)/D)#C*PY-Yx.m3+ڲ)I,f}^CfW+@](&CL2QzTQhYt,#Cg`YeIg $5~m:/򷴈 A|7Nh kYOxfU➶{?N[ D ټsi>q!o`σ| Vun#|yQ ;e8s3 8Y2oɣuY0)r2p]>ηxiaZ.㱦!<RCzI:Ȁw+v: &66>Hy,~{cByT"I?'>'F9hRxRT4{($eKeʷ7[Wn/[Nt\N @j],K,uzZ*+\xt3S\­Em&|ov:gXft oO_ZozK+WmVBOh7T ak)r ˕-~Cv| Cw޹}% `AI4=ܣ26c=8[x( 垍{؝6fiMr>尊Zԣ ^r_| N pѺ+QNr(ra۵TeNG4<x\XOsF">3›luv;Q~6yPhrB߹]N;\ dιr8+6tU{g]5+`+1}|y;kCRg, Zv:bl =>u0ոth)2" MeqzWfSjP67$ՉIm&2Sͳ#Bs +wK8 u^N>cȗdQ.sY:q;]kmz:ǝ?iXp5ugt6wl됴%0:X* P4*wsËs#fOη%+sRHNtFȳu\`s1321n `Ry0s=# J҉$|pRen1ˬ733tN˖sʙ: '>鸤J;.2TEeⵍDܑ2(Y0Vْ^.qX.}̶ Jo,Vc> Pb7%%fW !jxV{6IX"XuYU *+9؆sɼ ¯o3陏to^.H,# '@tjɀ܀б ܁xÇQI?:6+os06#g&f؊% {4:&^.o@ 6im=tG[o脣zz9ɠ"&%<2յ(j֠_ѩwIR2.q\y4Jc{^;yVہ5^-9a>PXfe2-(3ph?H[7"{'yF}q`fK5@4`q'%o ˀ{)d:)2B8EgOȐ}I,wz (h9!Px9:BG $Y,kfWQgH uT\"QBzX5!SVUޕ@^M X) \\Sj <} ȊSz y%Fyk@G ;DENI3|)o ćUV&LJ[7t54geiAzWRDc"!iks{v=o`=˹9VӺH1+`NtUPB>"~ӺK0Zްblư֠K"`10X mulg\TT|n:?.6Rt7lς\_KydR͂'ݛ:=GTq|t; A@ϋ˧+hczl<p8V^uU}X*&*pkمQyT@1УСES{]nc~67Kuݼ@dEW6_zed]S:Mz$1_m Iz@(2WE_d hxٓuNL~<5?O݄xUq7CHi`Utq_@r`q8yӫpQfYTVu |/K:1^m0مۘK[e)& q$hd%RCIxK8RBXdr =#j{UjI4O*$rIǍˏwb>2Jðؚ { Ɂca, *wjǺ2*;Ӹ +Mpr۩.H3q! uˇh" 5@C~6@R5515Y-.]zfw0OᨳY|MVkRڜZg#q[w1ڡ!/@b@$\\p]$ߩ~흂a).qP$Uh=ˑ|_2s_F1}?Mx@e-5/=T4#p`U],IPq, H+[q?x^oU$SR [o>W4{2o^(D;ёzHrSCkZi.Sߟ 03Y<7Xˣ摳L+'CP kF[{Kjh rz|A:ɢ83}2x_%SR2kܓ: d$SauʉģY_:ewUȈll9p- Id"^MF&wtЎ4;;n/~?;RxO0ۓ{Gca*H7(6ǓHEuldt0:wΦozbQH9V$#j>3+élRP#?zW11+M\&P=& C*$p잾b7tʝ#ylu6yzŘ4+hֲkqF:rt//$r=\ĞU5`RjKik` 6=P`qLF_z93*^ %oASW#b*~N yc@98ߵ2RȌUal g8ߏaڞf&{¾𰌒zǶ^?NfFq9u@ #oG}0WȐP3(z:c^XVl.R#@ h+H:$vQvFNÇ9:v;$պP4J lO3Rg|NnH\˿`\__}hBOOҕԦ`ocYvOv53/GͼFMqLT<3DCt:5g?}CdMRk}&0^ŒoKS\(Ƹ<G2?>ܚW=,Lp}6_^x#iPGyefL`co+{T/3T/4xs60εS4#'(Kmn7kuƪ9:dyzqnDc(5P_ӀYt1K; %9~4UYr`.JWgYbP- L);E|xn k $Mu0byQV/wk0Es@ԓ ϘW\Dy6-S[ش\,0ym<%fp#oLS+j`nRlu.z+nzө`r/0/u:ul Yx\6/*Fe,QY_5﫳Px޿UJ]mB~s{*#z 2B EJ@.0Z֭%L۲eNQ:2 N}o !u; bCg"V9{"ddB_G80 Vˎ*#uFr?)ؑ JJG\,7Z7d}=%{#9cng #8>3f!,(gӈ++`]u VBzu͸f+/m~|Oyt^NF Bko3( bTB(xRRvETSCH _!Ly",üٝ#L.y(g!\&f}EI.N_v#'!mze@ UٔC=(5br݀+!ֆeE0zQ\gJ1ٓED:=Af w"4Aab0@ɸx`5,bWwEW40{0Jxӳ=;]ٽҵ4{K/u(è8Sߍh-;qIVdy=>RoPi s yNeDC<ϲwg1$=dŵ`<[7e=h!|~:Hz!:ƺd|ֽ.ud>|ahɒykgtFΖx 2cuiu^nVr^xT u{@QО}_<#g\.#= *R:uH8fd7 @kދoz!wſ 7$ߏ7aP ; ycІwg^1Ou)ȯ.NA' XHL}9S^̴Y ^n<^!R{ =);rLDCvuݢ6c\I$Fi(E>k%{$ j\:|u9XeVW#Ay9P۰ !wb%v>"z&pRdL< Cn0:w~`AhTP(P.Tb:0q],<\oSkJSPM H@HW0=z5珯U\!RdZ]G]ýMn3t܀P%bN@:脁j|s KS`ᔻbͽ+N9g@ XˆS%w\2xSzFWa age!Z<#7_G[ ƎA< o<#Xo muCT+J>'/5VؠIYv>/éqP]I[M=IA9>3g#~3-M"l#{= aM,&u!}=.=Ǽ_:K>X#/y"`[[ftxMX|<prJaU='_jz@#rꅘȉN{2&f C Q5Z68mHTJ{v|XpɋQ]ez^M`B‡/tG0yX>5+?wrp(G,k8NST4WvNJzgK1>Jϑ}rJ*1Cr#p|;1_ #,?{@6Nlǰ *=s!;'{ߚ |Iް:ojr{ZϻT@Efh)AkZ1?/Ƃ'ww_e#B mt :cG:^9}/Jf\1}W{6tcYL`+s$Jf|)mSd9]Wh 4uJhG{ XSSaڙr!uV`*4jTi=QZ pߞw z߭{X ^|ԟ`X҆Ŀ\e-ڷlEa,Kֻ.펨b;%rԌ#e62ӣ r(r 񞬀N7]G9紬gwF{N\Fk1`QvcE_#uN?1g)?<%9Ͳ(҉dCf˞۫ZAݮ!sjc=[t[N^+Ǔ 8VXo(=Rؐ[t:y'vl$q|ɧހi3}i'kt |1+ ?XmAkYop1-] dT%~ӷ \4@^ZyĊoè½p6,mg;(Up)-C$<5ugB {կox by8LOlep\#`!LDׁrNg8%SȲgbuCt|{Jg@s\ZGjMЖQưƒkƻgK~M`0LLr- D՛yf{ExW?+k>f=D:7^LE}/]|Ç![Rm *o4OWI݋{[ZD /T`/=\c?9m)7={ևƾx !kN@䌡V߭NžhcwLj@r}YLwF MgdG-C+`hgXޚ8urm+S0shWJ W@|&?/~v޴]12j)t/yR3ݜxfW_\E{TB+g C2l* zQ:a~UOt92[ yN,;i>i WAHqԥ-D{Y1ߟ稜݊W3fM)? w'怶}o/&$$svgyc zD25ov^0l]7ydO<-R#qeK4tȔxB~O §3VkyG_<11-ZGZȖ`G>D_Ȕ;7|!Wxլ6~ʵxM)?umZxZo ׭&Tsy^VϪng8ha;M1Z.4wlY>WH'E#$Mx3kAGWjjs+ep5"%tI)x\Jʀj>]`뿓k#nF/}XPf 'CC inicXxS{V,ãޫ |}Rdʠ ?<00YeA:JrsEm 563?}77Qk:elS~ ޠ&v\lK@΂]RCD*z #@-(e;kXU=%f N6o|}dUo,c #3+= Z>3_ͽbS vrMޮV/(FCYH\Mlፒv)OP)<8-7_ׯXW‚T π"{oJCޞMP>{2fGrrj~ >Yʚ"FvuSץ#>< Ue{_@K"s$_sʓ,`|g`Gw&V-k658Fa߃{ϼ\*e48`z4=m PoQ!*k9*fm)Z}Ct_; ΓAr6g aXy \R9ES8`ۦCaytf7ɍ;%TӰKu6 sS*K hx 4B(X`"o4&`y000|. zP,7${3 ]e%rOj.at$g5{ = e;M Es^JgX^o[#grHFkɆ͍U2rMo.{&emoy3PXO TFTGzlLG_;%g[0Y_-ySfo:(ۼN`˲3X/j}}0]FyAU[CǑcB[8ҳz; F* Uo{ajLM=(o0=t){O,ֳvC=Hy QbMQdZd:4 *x "mpQew aeƄ6Sz.#[uF\7g5џ6g s೼U=cm;eu.iǢ&T Xu,kCp^ܿzSoh/a}H]k,L~EeH6o* ͘L/}c#|9X i##OK՛Zqnj UǓ^dL`~ Y[-e\`ۼLXxC-7)Lη0(Ux{IHɭXc:UN _NJ -@7yfڻ1+,HYQZ\7(oZ5K3i*V0mBO)N+sp$>(s!]ѩ+ZxHk Z7*Cϲa=a×krgٝUh mB5pL֛89S7:ne遼-tr P:'׭ra&5l^2C4…>Y=k e R<4L<#x2X Yf$q0GxGׂ-ї/K˴Z |7Q]#ֽ[0) 1vUL.$9)2\oXLN3LmCA%ii 7I9- S<./O5zOv 1; aZ^e/= 9O=Ud)&8 }zHf `F&HqƐiVp%;#+C r-i&8,rCiZ&|"S] O|my@ jZ/0̀c $vEۗq ԟȼ T`ʕK#5ԧӱ/<]T񰱷Ke$,@&ϲ2@yg@=)tuFJ:.k4˺vhGR%;n#@&XVqH8u6WL>:; "֬(|Ή;kzaG^Q].Ja{H׼/ȁϗk|KOCבrvjс(A;Zz3_a{'wo 0PE:ϲFFQ4N H|Y@/ia啻aR<(aM8eFm@ʀV敻aJNu!PkBsl0,z,'j~ PT0Jb*  ^ jgEa|]Zpco\phqI hQzOض9Ϫ6hLWM,efB[fk֍3[՟ۺk$:S-ڜMag7,ȽuitcoY h5[40@c;D ܉ԿJ+ȓ 0h|{Ouj}ᙟ`wqkvWO rJXTaUQlo m\tsJiz5Ga( ,t+XM 2ԴE͞uwt*n!G[9뚁HLm-Z6xg/OGǒImoafq unReFeGcicp΋QrN]E/|K4Z-(1> M@N] ]џҖT5xx=fӡB(ʓ&B6s-y Vq`<@=+_(A h{yn.Iy3 }g<3l/ W*y9jpŸYPSmȊ 75( 2LI 5.op+uSoT,J;VI}2L9t;Kf] `Er:/yy/eH7N:|xX^$@W,1N HvDz, Զuj(/3".lz zE]f7+wowfo4m[g4yygrIeܰwna Ϙlq61.t@rUJ tҳ{4l,5smokUm-W+F0Zrh,ZHgj'kF6]k @k<֍,̿UHz擒 D/(-sK>:[2YM+{ib QJ^(n"v`E4><;{h]t0 NgJ!YWTF6ݦ?Ifn{Mhj{EtIP\#Mvm`uKSO2s390Yƺ#꘱ڣv qz8~{beyQ@{?K!XX) U݀ȐIIsVξZx/ʩkL9eHȿd,06gee74jkF=și*=/)F<)aYKǫ=x1G!4*/kZ)9NUTk|ߤ ֳ^x<Z4]X8@.˰)d,:Lw*LYt,|{ꍬ+ў?mU|_jӦ-{xwQH)75{wKz7ocCK6āBMx̶\%7](V|Vug>Iޭ\& grVvT fse];9Aȝ2CS`1 `pڹ,EZgɭ_-z2@ ,ao: ѹ`ٷvlo ]TR-SxqaO1m/e;~@+w~<X0 Ut{\6Py5VF?+K*Ֆ;"P–rxv|6tz`J,8*vAJe1+UQct>TuU ّzh-+T*'xvXږ|@WO[T0Ⱦ; ^CD!d#:m= z@SsA+`m^#,8sx@)>g ГZ=ws7^z:Hy޾AquŶuX4sTyW(-`7Q`VXޡNOFVĨ@ //).gY)+ezIPz&r5߹Τ~LB0\6BmFY̶HF=GރYHUt5bٹdCbی^5{v&,0e! ߏDϽ:(/m|'(A1;8;?/OqޖpT;ryymk-zjXhPvhs6CFRF= g9x'XƵRqH6,@0k1T/l<3ifrm5;q g1AXy4DN%͖%{ 0T*_yhWŵWv܌K0FUqqF@&Yz+Hy!5h٠+ıj-# Ԇn/F&_ʁ;i \ĞaZk{׀S,&4K |km\NϧI(|-pҔ`Wum 2F}'sn͟ ݀-Y~ɂ 8 6 gՒ{ۊxp1ZMC~-׳ B\{\cҿ yebݻު Z#Tl@E(r`Sz!Ы$(`Di cJs~əf&M \<ё%Dž]x._cL@ShEWaYNw[>d̍JsEv<|p}LO*i^@jG럖 >k{qq^.h=\b k).TuHTNW]7 .jx4/̿7ٍy#-iҿx4:H ڲe̓ıq,閥GeAƂ% >(WPzf̧(2<+/ɳ} * E6/ouз fÁ*q?o$W58R4b!b9Xҥ]!b qg9nz{1baki=r7`[/F(fD58sVҵ{v渙ңWd^YId!D`n[{*p U%Kp^OcPqp6]t;4!Zz.ɻԪ]A t=dYcB">}S;l)0CIUо jܴr:>RaS]7ULy6`+9%Π=ZaV9 h2!вkyPoj}ςuDtZ\s+|l'ci kD]@cd;*` ;W,USvBe\#bp4>M x͟G&98XG/b+VYV-niR2_s=#!"gt} oz}W({kUV 96nMr\\)I-@zs ޤ *]]h.,aI[_MUw-g,rmAΗ֥[v!؂E&>#3/h<^y8cU[2uQFⳳxtW0Kx]o}.~g[a]śQ67Cȳw~ٹE?wLvuW%F͛wU?m,Wʆ<+*@uY*I է9AqXd̈+5oKVO_t\Cb9iUoZˏfX/5arΞ^xqyJ:鞉[[+ 3oXzE2nUu؊ƍ獓+]s^tEe},\<=ʊC.1FrLK{#%:#ZXN)'ָ +Vy$Ũ2,ÙU/8?*}s.秕7F]U'{A|F;b} }V=,#f+~6mڴ-;l= ❈“57W$%sŖ%JG8؞Nv_/q(ԇ'DJl3H9Wn EuJYdg{w$6O6/ЛgfZ2ԧ"z\.wn|Ye@?R9( =;$vWyo{;;*Ho[yfjԫ9]6>~5PU6mo5~ @qU&=%vl"Rl`fsYMе Z"^=(/ $Tk95P4댙_FNɔ/ JF0FJ3}AC;7&Rpc ҾV_Cֳy4;D= H: ئj0t`Mr-`IZ51nFq]z)Ti_t ^9|nj8H)ol?jc>z Cw=#`u>'_= DyBsZtqg@ r ˸ɵ5_Yċ^$:yJC;ﳽ<1g@%` To ',n'94ݍ ѽu`11 ک&n^+'f:,;:Ƙҧww%KPNK4+Iߦ~B l>RFްbY h7oW,ޔS5K?o}M;#`aa 9`ojz?-kG 41: 7Hؘ러uN:4+>ycfĪ=ԃ2` n󧔙d0%E/+؉i<=mꥦzr& k} |uXաj=,Webz($[0j녩ge$O#:K݁yԻ@k^S3z\+7^Su%<ۊ] rZM:^6+P5@PĆ `mQNS枑x(.=P]r/WPE0 `CuP)K#O쟇Ug,aY%(S mK#gg|)GeǴ{=@4 v9)fŀ5c+IűW߃Ӛ+nܣkΑweȃxdAO ֲ*ݳ.IArCk]] d"zCa6o|}PRbYUe:y-k"75HiiٝU2{ ֙vl< 3|%]01,^qz6U՗21LqQ@^?KA'$dN[~_܄9SH<_L:p&:<Xb?{Ul:[!@7`]wŋ2}\$ԟK"X:XX"*P.{ΡFto??ɗ]?' 5=:jALC3j,̏ .'&Xk/A  }#-:[g/3 @g춤F)!ӽwW|^4H9~Ur^:U(E@UR|*iPӋr4Wm hTNN8wѐAgpuuTRwԯbg}FNm\6Z-0 =uY:{0cϋ_7/}Z}K'窓 itdځF^nlxqhVǒ$nk_b<=嶚 ;H?_lq*k;%G\7쁱N[Ǭ0 g x_ Y) aT A =U|fA S/ y-pl='lX<9V߃h99 EĆ)hKFGq#𔅋.+YcAǵQڍwlUGV@i>)#d!/!C?. ëNZ*t괂jfvYvNE W8sutAtQ;TJwLWOrpyv̅% HRFs-6*'w?_=1k>0w޹岐AFm;yT%c|lъ6w,{ho.3bѿT/רwlk3Cydx._F*@q OVwMguv1#g/XW.]O _8Dc 7 抏=뉟q;L}Hݾ)[Nm\ ac;h=T2x M+8 l795H,1t~[qe}CyXC5gH߱~ÈtEp j9MayxӦ7;g|p'lj;ݰs7}4ʕ+CDA#Mb㬯Y4cXBe/)ǣ>($\'d@wOme❣5^/Ƭ3ȕ<l5sYulMMU35uz#;Ӌq\?k~o{;8] #XJ>Kr`yaLH>ڥ5z0˫5j:?#L]v*)i U8FsV^~m)WKU;G} ˝"ŢUV@Yܺ O 5z||>->1\f륃>AˑZflA[dx=5 0T>äUd͌zn E.TSXG="|dP800ǃ~#rJx^KS\6e4bP /D:|sc@Xj5H`3YaE TR'%Պ;j[}K(&4acoQGDzẁQ i)R<ZD^g<rH`6XuC~".suàԕ0IFY9-o*$&mSUL{32:!D J~ uLlxRÅ_{Qu1OCuJUwϾ^l6ޮPNWo7^+qdSBRj-{";k|qAF< sX /WEXQcr_}u}kt['o.}#"%IGO^W%\k )^mr#Nre:FbKRD7wK"i+WyCF q3"CͦlO4m}&E_: wٴƓ<~ sg.=8l>_Jj".z=pPkflh J/dS]eo74[xSFs0k/E_{\wDEKj|g|ip.YR-l֥>e;nYȵ,fK_P;}ʣzɱG9zN뜫vǮ^Q7W;;ro?O bǎo޳gFv8Zz5}atϜ?a0%p]) C Y$qL>Yr<1S@…fs0?\7<SuYeaR-rQxV2r7u"Y T >[ޏ>KS|}XMFn߰!Ҿ]lP5VbH:񌏏o]~D}[3^8"u(t=mdo&Sj+kz\_m~-g ß>W[Ӓ^mv?IJz+Ugbv pm-Mr8mHޕ :셤2Ss .;Egdc|Y{P LLm^zN*U5d@\Ou*:S\-'0Btz0`4H[8Bf^AH{1^+ 3msNUvkLFГ"=(F&#r#?x(x?~i\# ܳ.q[lŋ)k^+ՊM`j f gZݧ<^O^Og.Yޛ73,^ആ2܎>eSO~9=yz:gei>C <> qY@Kg_./uu?f`?kו@=D5Q}7y8)R-akK:4.'neʧ!,h㞟ٍn utR)dž4[>')]VBO^WJMu0%'.l/igYځCك4vSsEģjtq'?ao}q縳IcrO;GJeW:8{cXݮŻ;ɌhTAxgRɿ% T }ϼXUl 0,eNJlatO!,3ey3BnTYZ' pw(KZ4Re)1?^=m&p k.w҅,kq~zoP= ta;VG%CiКh[tٹRZ\3%aZ7M;y96suY@$wf 3}.Z9 @7xjޯ Xv2ЖUkw9KV <3mZE4bGoycsL]LK/T//?Szs 1xXS%f ^ w+T T=;W#t=950oﭚgrI:?G5fZr-KuJ j3` >+[ ̾ VA(i?'!u/KJzRl큈[GFZ^&5DH Դ398>+('rLE5lNf9t ;Ab~%@;&T\v'  M>hƿCoUy ؍wZרodσ#bE2 ꌾ y1H+`Ϻ3cXaުbȞ+Ɠ_C5Kzt-`LF|n@j"`x6,E<i$ Oґ:νoKMڒ,- z KP;JW1Q^A:3wvof ,Q< k|;$au3He0j4 .iFuҥssϩ6/? 9( 03;qWObSR^PAZY,>S2yYh[#h`Ma H>^' N2# ջ- wz0֗lgʅ )7.gv;4CuG}O{&aciJ܁=8b%!/j. 3ݸfcʹϙ[cN#Mwa1iN t6/i|]~*c02A8g %Sne9\wjS~2-{_d 0H3}m4_4,q9;m]ʕe4tEk 5=rg` :r KZ7\ f $\HJץj~{nV|jܳx!˺<7`zˆ$4ly7\Koi侩5#~f +9Rwt7ɽ4NRdJߵu0p}0- srk]ePpֶ#X$8vh!i=?/BaRhcw&>KnX<_̑A1_3*"ǍmVj[2n9Zt408 >}|i8ė`I|[(|R 0EQ_d^N˓ꏟIe { w <R~|\ͷ ;ۙMr:;Fz_attW1C9dfI 0EP](k3Wg˖lHL1@肣I=jQ JαɊ~T7M!T?ט6 eԯG+@uͲJڮQ7EvR.ۇщ.짡x9<|6J[EYG{GDi0Q(cHᲑ ‚"%*ȶmVSXMSg rkfaP`&łV,"`_}B8qVBh"j Àmlt,#情B?-ȄU6 D@'u̕LbAD{q)e=tL}9JuP\6z󍆉*Wę]XS 4MǛo6ۤ\=cqSC~$%[?[FL`n9|f}@9`b~x1 FY#H|+> oz=lÇM6YsѴfT::͑0Az30y4g j@IѧZ#-@e_CzZNR@Ȋt44~~[a0k}{Ĕ7~7=;qגg~w]lu49{\v [WɢjfR0F^6`p}dk)YmX&SೲwFMYPSƕ/]4I'Y,jGĔz@Aپ2o{o< bͮC5srgEl[ߖwb`:|nݻjW"ֹ)>كO+<Ȱ%2+D<v ѦSbFa6*&ߊk͌*FƂsAmV+5DcR*EC̴-uTY!W7>s R7+!=zTT~ oGl3 caY~o9b~h^pŶF]8LgJmUځ}K#4w/q- JtHuԗed/(TuvwlNjn 9k*ihy}Gfg97~R4,3[M Ϸ`{zry̒⎾'Z vZ߾QNl+@K 8Ӏ H Oyٰo7n;bYgOYkd@.߆4r9ndV/K*XL$o?k/RZ *m+bq!"0}Œ|>{\4\ X<= W<A/2T Ur@q~Ly2#u땹i/#i:PZVY>7_E%ˌm: ϶"vDmQ7n?\a80OK8+yGb+T-+` zvTrǽGsR 9^2n֎ .>kg̋&2ۈ깥Bdn1X/^<|)/:ڵ97!B\a=}w s{ƕ uBo6+K뭥/齪_ PgyORҹ&9y9]d^5.N}=g^S3VDQh +,L]GTgC9˾Lmy Ǚ.7-Tв` 緾zK@L'uˮ "DµkK>wN`U|9 d'6צko{;Gz{߽g茗a9&a97)n" ]O}8Ao51`Փm>6 !_9M !5i(>WVs~12jj v'/ԑd }~j,}ìi5)1)Lgʽ _:ep Ǹv"[*P:s\ǒyb*aց Erh eyָn7w *NVeW3l;wM=ʹ%hcZam$ZOࡱrǾMEŜkDϝ}LV\<L}E= Ue9hr z"^vڇBt{o 3l&c9!әxTedoijO u=?=?0.7(ڼb?W44O_0~CYz6aM? #V'='Q4t0?b)XtZzcTF1}'fbc^C,h̦*hޘ99KRP-33RX3oکm; HGS{ҧP0FڑeI}< )F}vYجΐqa ^diϋv AriيCw&`PGV$q҅JК]^{ߖM~v :Λ4kje1:Qa2+}tſp/,"߶5ޜF+ Ǩ,KcC=MeFRwYd0D ̐ꛔH@=u2UL1ziwzog?ưVN3Õ}: (.]cC%CO7#  ˖-S6 ¢'ŴTj:9n^4<ۆkV7~E[`dYSt}C,-~k@{{ p1u-31w%+:p`Cj̩ zn։pqbM!O0/ir3'h43 #pG~:$+6h<|8G޹.F;=E}B{{n;8rB+n*#Մx_(Ym`(cOw]%KIyV1=)^ @YI@L߁YM|poD=߅;Ңĵp ;rP^ԟj//ۑD\=(=,;'<̲@R>iSuo!5vۮ0:ث2228r|X"0` Ń+sĩ`oyq稿Bƒ:'}֘?8/Me9K{MiGÚa (sDLUB(ͷr\ ĨqxB6B49ۘI)ߤ%X1 }{c \W*ӓ( lt8 P)]M TҎ 3.r$wNLo(؀إ:XGz^}oy{mF AL@hY&1GO岥TL36]X2ul';r+H/,+̙ Ti8$ї.Jڭ7 >p#O9rTm`ޤAB`{&p< )K0@O%e#]5*wӚ1WQ.9;t^&r^"_ytiE;8?KX]ӈ<ZqAl7YOm.)7r깮R}#~ϸX84ڝJEŕ^s¹gȮ`)cL\g|ܴx_{h뛸Q XVS68gVVvvfR|P,|g5W063J=7<4S6=;a`Z(-z94ϳJ9 *QPʂ=Eɮn>TWQu QT6fӞ+gwECy R7m>Z>^ydP) {2 W(X:S[/+-3#qݞUfrn=ڇ,پ"hwL{fnxXZBetroaLS@*@E=uJW&!\pb徠jG >XEJ @s4[^S~/LӲYNhvML{d0ț,# znkꎙsyUxeppLqUrvZFlATAJUK( C-wfPEDbܳWgɴ2Vt w?O{7^gUsy]Bٔzf(-;#fOdo>b6\5 ]K<3PF7(1NXKo|df; 3Uz\u~^vtl<LuU wW7bBTNKwWvcZ.^۾xQ \OBݛw ŧΪW8]{?rNÎ 1 tk}y&bʗ+U2Oy Zu%p{RyP M.eYsՌ / Hs#:jek[9fa¥aP$xҶ3kY]㸂@S˟+eLƾCwui ϱa|;aj˘杖u VXSdYA6R-Cc^*=h%>)0pn)AIu/fQ;Lwyy sC12F\ʣ'0]>pnDrKW@Oy@HĞ׀b)Nr>ZVkPٷf'ej;w0qp\W܁pR]j;IMFb `U>B#?:3dWW̻M:C]3uR-Vyy+H3qbCX7lP ͽaaXK\}_ Z_}nj [ypse汃Q`og=0(4"1#Pt7/r睏wKzLΔQ۴$wꄧ:LI J@ 5AZBccNU>>\2 W=@ټLR;fƮ}Hai#*q 2q_﬇}?)|>#}4ԉ<޼mfҳ'9*Y?S,lѠ9KҖ~BN K0iUmqӊ ܃{o~EiZ+ ~G>{?_>\8OPvV8',W_*}ϡzA^w W,9 k;/x : OF ,MH鷨B\\~Zn΍Bdwp$8DQhZ-^c윈KtKv:QOc4 <\P9t\8Š.?X;XLI^Vs2 g( Tfg+&y^,z#IcLw14 sԢ\o>wIoC:Fs/ػ do*lD-5k\>]fO>ք٧>iMXSI5rGA,ϙ<{ܭ4R9Ɏx6{®1"C}(1mI3}1%NA՛3C(s=uME.[&4"~ 6quԆw5 g b9VMK*+yTxe⅝#I/]a^Uq yzO~B#}4JfVQw|djg'?0ü.)Prw&Rb.Wy{ c%y'5݌kJ]nNexjE}td5k~kp*]r=H:QS6ZOaٍǼ^=;pDZlz1m*'`ټ[pcXAGb/ͼ7`mT4.a]{\ô֎A{ye-M|*<דRz4Ǫ{O{<y:Kl%Er4co ©c<-`c:L )d %+=|yx^Ya0.Ǹb{[R28uku 6_x=&`LM}E=\2PMd@d'eΞn)=#GгHgIǰ0@SLXdauҁ`hS9ù.skV9o]rOVuYpZt]'Wp4L050}kbY:^)3uqIJ#sF`,V]Z;7MaٛsWZ{~ϐ2GA>UMXלB33+ Վ QA0O {KYO'㇢?T5ѡÆQ5O3Je2RcݡS^NƩ/ '>vaea;׽JɵLktyjm&(0}>1x_zmD3L+m"dua8ﭹ7L V%yI I1>G,GJ=Czڴ܇Bߟ UoXOIʢrd >{YUljSTGNW[g).<rV^"c뾸 :@˄l*pVSi]/Lw)1@MnwvJKr+bڰyM K`M0Aۏ/^ze9J6<>؏RŴ 7ZMKb.3kT @i_DTu. (^_P34s)z tUf~n;ow\LNj5>fݸ#LQ.`Rr.÷'\atM;ݧ+_G[-$ϑ1Lmm~Dc(k8\s d^\]Vg^bc*:+S^@ hS+aXmu7T;]Sv _2Rϖ]RG[o &E/@Wѽ!NYaW9B?u/S^Kc,tLHK9+wa Yءj/QDSNJ:K>_OiZXYTŷXL9ݟrk3ahS)JH zF;mZLWyx4 c?OIX/u{wRdo\urhLиWF%0-]?*>4KI9\z>3يkKyn1`^n~]IEa: ҪW7aN|߮Mþ7#} bx0J fѷ=rj-bنz\lS}[+'sQV"U?}jWX/2 L%i_w-waV>c۝1I?,+9]9ڡ y7m%BʱzСw_8L&`%h!)a^4挧>bshU?S,V ٞ.K }˽93M<dĜ<`d@LR#nx5鲰j&v |6Yݶ9bDtTLYZo 9Ke`)YV9bujYX-+m@˟v(oFPj+EYGVIGS,`޼6 (EQ ^]ﳊCTL+H祯PO O$ ܸId޾hiΜLqIcgX){9~B||[ẰGa´)h "O32fy,hLncWW()XAz5 r*IOPbg7T>+\RտS2QC, hhC5s8DY)}i^&]XڒMk6Ik~]UWC4,Χ8S;~KP6ucJ'dYc1j]h%yM6Sͺa-Qd"v , 騽t;Ge˖~ʲNV>YwIg;e|-M¨a%as䲼2ٌhsC:t2yz[1 LJ). ( `- ̪,i/î'կm]XEy3τQ*drV^Mð9>@GlDY nوπJ"Sb$f`VxʃQ> dʙߧJ %*Y2vOFT u:Ĥ\uE@-gH}I ߜ5pX)xl#$|RϞYAS)s-g() XAz5xBLQc@Q@AJ5 oX4 CY+wwȱŊ&Z0'?K/Zb^60u3GzEO#wb+8C@Xm TnXpsQAjr"L<6OTkp) X4t+zg11'H#FSaI.T=gg&d,潤]'U,Xp1E -X8gi _]dErRx}_FTGS > WJVS6R쒂NwŖO8Tov㨯Q!#EƤuU)kNv 6m|'=-qU`wuzTP@44Kr1x 6U&cznz>*O22u !3Qs@Yk/[0I*%ߧ&DLGu;: 0̽v>!UyYM=5=ʀX`!`tߖ; e{zz$K@Ei8mrJVr5Dޮ?kӡ;)]魩E;,PЙp  "~NL;Z~/5P5,a?96_ ߌ- \P~q ǮȔ;;~Aҏ*e6y:}h}r41o!dsK1kSY @q'sݶsZ <`uʍI zhC<;RMaL)H~ nHO,(MiJWtH_GDah9oڜ5~ GO }7L}.Kkf;_{橂sYZՔC fEf&Nv T7+}Z{xԈh&!~0] wHihgM?Sc{n!ySs@FoƳ=Mv$}Oqea#mյϘ4 wYV P=U~M{ Ov L]A<=\lCN$82:I\z?Iq/?+epV3=1Q8ڵ#;ʓy6s;c/; s(~*Iݷvj.^l7:&Ձ|v4OV~Gze{]bxxՑ`X":TWۅFǹhO" 6ٌBꪻ c)?Ϡߕٜ[zݑ|6r_R@D)"@YB(lI).|:wM̷ W?W1LAYhs^v,\i;6BLdTw[zP>V8IUӆa W,[6gT ;q:w-ۑـC<[Vֻ˴hLYw4\Ĕq0f:AŌ:ɰ %;-wxIae/C=;OVKՄ%Z^< +J *e<Ag!OϼQ!TW^I2 ir۾;t>U >d@vj o?m2[nKC^POQaؚ H Z+ˊ:4{v'WL#|["3퐧 @Ίe֑T&ŠO o/i](~p?jN.R #;^O :%yFS\:_OȽeO@@ w0ζ-g0Ֆ&r}sJwʹbo`hVf͟eRiet/շw~״"@t2Àoyأ XSز| "L[b *JfW8 R4ANP#6Nk[!s#z^I)H0:Ѝ"2!鈹cQd)˿I f\j% *:)8}}=zxXA"ޜ7nmAya\` S>v `̳xy,yٹ8TY`K8a}Vt1EKA8Fu~,3B䠞{t-uLa*|'zL.6uz)20ZVu LƽnvS]9+t!5tEVV[+Y#IJkUXa8뛠g. h8}Yv %V+d2ȯ'L*HA˘B,XGPeEQ$R`)նN13#0L>%m!\ʏzSR_Ѓ0w3'EP Wf1U>¬z)1зԕ>,3cziʚGNG3H˗P/zM\ɲv!ct߼Hy3 Tge+^`jcJe{3˥iys6`نe% BI" i47=:dy sptzPƖKҖzhuW;pV{7u)[tVr]R6H@)Jp` r:r#@r/aVP߲x)笀;wsok2 ]ts`3յ&d)1Ho:9:w50%<[Ypo,k7 aIo.LFT6Ef$V=ʭ;7~ս,kNXVNt>^:6Gx},X6s#>W ov<1|vn.'~? ʽ.bҮ?)p F4UtSoc,/@xYQĠؚnOOu (|X̻vw}v?68n)x*\,`eRW__A*_s_lͳI 0 v3r#ELL(8b;u!@\(p#¥dpxkey@l&}E^hsMM  i9Blֆ7uX?\d0 @2hty*@գYw#lQA;2Qӕ 1ZE1q9z Rw1 J8ݙQ+@o*G)D,8sGH JL_f*w?C@lƦ=b[uDX HMVV/:N 2S1aϣP6L`:P!m-Ǧ#)(2 XHAkP%#|\ 0 ^D7,St{czh+g%4@AQg=WeQ c00e\v01;)Ē,kvJ#rpsL1xD !\OmcyMlr:"u h8>cELoV`XG 5?XDi#, $  pSjYG<= ͳ74nYR $9 ]$u,f],1!'@S+veƽS Q o+t3Ef(2cvcUQn4 2F\Y̮̼9[zxo}hWav`2p (jxiV JZv@ fc?a^N˿~w^o.*MYU)̙V>]bp7 -V Q浐)$!"ktKha í(~M-@gYɜetXBY/\ڝ`ⴭo Pu)AeD =F/4+oh#ں}iObnP_58Me(3nT|v:)Wy@ouӖOYt /.yZ$Lw`W\,g@!9k%04lǒY>9'&f.,iFɓ5vDRM U U^9tE}z3aS,` 3 7n}?SpR09hU~dl3YOUy\|[@=%f?էgߤ~XԣĢ3dOa@l5v4ZSHxDݓ9MK$Tsp]J5 3۽뱾Z `u~3]eu^vϜz1(@I4ShKplKwT.TOr dV'$]Px@1uz7tL@,i27=2Sq @Mvd i 7[tӯ|+;v&5 )hm,VvΛ0r7ū oz}WziZ e^#@:;.X5~$5;|ʞtPACIk~ |yGN `&f,h띲tKˮutOʷ=N+vndH`SI1Mfk2S \|d\>ov 3Tk%wcSӐA0YO$لbS_JP v U)>2sIbr!eʔ$` t. bVvѰVg:b>'@v5Ng,`@f Z=]` le A!%Diь3H˰n4=0^62$-g8Wf3b~[̰0Bô9)#;Dn0 sfxH^i]c dZs 7ԗ}wU``G0)~_: 䯌MMAϛH?y.3'Y/wztud{`SmrSɞK=cWnX2-vUV=e2W myəe f\A@q4B!~&a-6@ v!.D ^!dJ1=Kk xo-sYU>4@qQ=Ÿo\ +_ʻNJzgu4=+222nҁ=ϟE9nrHXK2Su_7W00r SSSMӊ? xworBXCX.H0_ua֣qz-Ö>@ ?7ᓃ臺s ,;7Lh{xݳaD¹O/Y؜X .ʹb=#p,:NR7Nb#ӕ~n5aؚd>Z*@&$=(+a$Jշ9RT5`#a_ǹI4d*L ߟ2o{ky}0 ,`O Nљ=LW~) I0o=EShT䰇vJ%(`(OV&YKXv˂x=^=ʧQА )jVF DJNNq:as3y= p߈n֓J]1@Ht^Ci%`:}67Tk9: EHkSe͆7XKk?UK~Wg?{PR@W:dMiV̚'XBo98'2-<4/sLY˜9*"0` xPb’bu}o]#? {fw<5*Lhx7flX$!aٲx_,-|v<|449p qd풳P$w <9Dd(aJsILʯ/Ǝ޳4ưb~6o-(Њ@6W!ҹf#]=dԫ/̚MQ~!7Ca7`9.Ip;}x?屗G uh.^2biS:OIQhs}t}]M\!Db HxD Ύ{JG,'7Oc#`u$驗#2>>~A׷)H~MWDLC’-~hcbߎ0Q \݅ÓZaaJgyv :49Mo7QXi~!n47}}gR1ӑY:e&[q6p0Շ\0騛o Zz02ے:I/_6U9Co}켃߁}Cy%Pu.}7 rDS\V8*@p]q-2Z0B&T,T>]֊]BAQ8?lzݦNj3Bgt|o 6 cV*uȱ-% -vت+T sGv:%p%J#z')A!,{?q-bĄ߿ԟ#,`&r,ADI`[\ Y; KC"|$9eh;̲'uL}Z5dK;em::dN3uBƒ:p.Xž/̲#4 ReleaseInfo: with file_path.open("r") as f: line = f.readline() match = RELEASE_TYPE_REGEX.match(line) if not match: raise InvalidReleaseFileError change_type_key = match.group(1) change_type = ChangeType[change_type_key.upper()] changelog = "".join(f.readlines()).strip() return ReleaseInfo(change_type, changelog) strawberry-graphql-0.287.0/.github/workflows/000077500000000000000000000000001511033167500211275ustar00rootroot00000000000000strawberry-graphql-0.287.0/.github/workflows/codeflash.yaml000066400000000000000000000023551511033167500237500ustar00rootroot00000000000000name: Codeflash on: pull_request: paths: # So that this workflow only runs when code within the target module is modified - "strawberry/**" workflow_dispatch: concurrency: # Any new push to the PR will cancel the previous run, so that only the latest code is optimized group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: optimize: name: Optimize new Python code in this PR # Don't run codeflash on codeflash-ai[bot] commits, prevent duplicate optimizations if: ${{ github.actor != 'codeflash-ai[bot]' }} runs-on: ubuntu-latest env: CODEFLASH_API_KEY: ${{ secrets.CODEFLASH_API_KEY }} CODEFLASH_PR_NUMBER: ${{ github.event.number }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.11" - name: Install Project Dependencies run: | python -m pip install --upgrade pip pip install poetry poetry install --all-extras - name: Run Codeflash to optimize code run: | poetry env use python poetry run codeflash --benchmark --benchmarks-root tests/benchmarks strawberry-graphql-0.287.0/.github/workflows/codeql-analysis.yml000066400000000000000000000010361511033167500247420ustar00rootroot00000000000000name: 🔐 CodeQL on: push: branches: [main] pull_request: branches: [main] schedule: - cron: '0 18 * * 5' jobs: analyze: runs-on: ubuntu-latest permissions: security-events: write actions: read contents: read steps: - name: Checkout repository uses: actions/checkout@v3 - name: Initialize CodeQL uses: github/codeql-action/init@v2 with: languages: python - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 strawberry-graphql-0.287.0/.github/workflows/e2e-tests.yml000066400000000000000000000032341511033167500234670ustar00rootroot00000000000000name: 🎭 E2E Tests on: push: branches: [main] pull_request: branches: [main] paths: - "e2e/**" - ".github/workflows/e2e-tests.yml" jobs: e2e-tests: name: 🎭 Run E2E Tests runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Bun uses: oven-sh/setup-bun@v2 - name: Install dependencies run: | cd e2e bun install - name: Install Playwright browsers run: | cd e2e bunx playwright install --with-deps - name: Setup Python uses: actions/setup-python@v5 with: python-version: "3.12" - name: Install Poetry uses: snok/install-poetry@v1 with: version: latest virtualenvs-create: true virtualenvs-in-project: true - name: Install Python dependencies run: | poetry install --extras cli poetry run pip install graphql-core==3.3.0a9 - name: Start Strawberry server run: | cd e2e poetry run strawberry dev app:schema --port 8000 & echo $! > server.pid sleep 5 # Wait for server to start - name: Check if server is running run: | curl -f http://localhost:8000/graphql || (echo "Server is not running" && exit 1) echo "GraphQL server is running successfully" - name: Run Playwright tests run: | cd e2e bunx playwright test - uses: actions/upload-artifact@v4 if: always() with: name: playwright-report path: e2e/playwright-report/ retention-days: 30 strawberry-graphql-0.287.0/.github/workflows/federation-compatibility.yml000066400000000000000000000023461511033167500266460ustar00rootroot00000000000000name: 🛰️ Federation compatibility tests concurrency: group: ${{ github.head_ref || github.run_id }}-federation cancel-in-progress: true on: push: branches: [main] pull_request: branches: [main] paths: - "strawberry/federation/**" - "strawberry/printer/**" - "pyproject.toml" - "poetry.lock" - ".github/workflows/federation-compatibility.yml" jobs: federation-tests: name: Federation tests runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - run: pipx install poetry - uses: actions/setup-python@v4 id: setup-python with: python-version: "3.12" cache: "poetry" - run: poetry env use python3.12 - run: poetry install - name: export schema run: poetry run strawberry export-schema schema:schema > schema.graphql working-directory: federation-compatibility - uses: apollographql/federation-subgraph-compatibility@v2 with: compose: 'federation-compatibility/docker-compose.yml' schema: 'federation-compatibility/schema.graphql' port: 4001 token: ${{ secrets.BOT_TOKEN }} failOnWarning: false failOnRequired: true strawberry-graphql-0.287.0/.github/workflows/invite-contributors.yml000066400000000000000000000014101511033167500256770ustar00rootroot00000000000000name: 👥 Invite contributors on: push: branches: - main jobs: invite-contributor: name: Invite contributors runs-on: ubuntu-latest steps: - name: Invite contributors uses: strawberry-graphql/invite-to-org-action@v4 with: organisation: "strawberry-graphql" comment: | Thanks for contributing to Strawberry! 🎉 You've been invited to join the Strawberry GraphQL organisation 😊 You can also request a free sticker by filling this form: https://forms.gle/dmnfQUPoY5gZbVT67 And don't forget to join our discord server: https://strawberry.rocks/discord 🔥 team-slug: "strawberry-contributors" github-token: ${{ secrets.BOT_TOKEN }} strawberry-graphql-0.287.0/.github/workflows/issue-manager.yml000066400000000000000000000013551511033167500244160ustar00rootroot00000000000000name: Issue Manager on: schedule: - cron: "0 0 * * *" issue_comment: types: - created issues: types: - labeled pull_request_target: types: - labeled workflow_dispatch: jobs: issue-manager: runs-on: ubuntu-latest steps: - uses: tiangolo/issue-manager@0.5.0 with: token: ${{ secrets.GITHUB_TOKEN }} config: > { "info-needed": { "delay": "P14D", "message": "Hi, this issue requires extra info to be actionable. We're closing this issue because it has not been actionable for a while now. Feel free to provide the requested information and we'll happily open it again! 😊" } } strawberry-graphql-0.287.0/.github/workflows/ok-to-preview.yml000066400000000000000000000033271511033167500243670ustar00rootroot00000000000000name: 🆗 Ok to preview on: pull_request_target: types: [labeled] jobs: ok-to-preview: if: ${{ github.event.label.name == 'ok-to-preview' }} runs-on: ubuntu-latest steps: - name: Get changed files #uses: lots0logs/gh-action-get-changed-files@2.1.4 uses: Mineflash07/gh-action-get-changed-files@feature/support-pr-target-event # Remove as soon as PR is merged with: token: ${{ secrets.GITHUB_TOKEN }} - name: Get comment message id: get-comment-message run: | import os, json, textwrap all_files_path = os.path.join(os.environ["HOME"], "files.json") event_json_path = os.environ["GITHUB_EVENT_PATH"] with open(all_files_path) as f: all_files = json.load(f) with open(event_json_path) as f: event_data = json.load(f) links = [ "https://strawberry.rocks/docs/pr/{pr_number}/{path}".format( pr_number=event_data["number"], path=file.replace(".md", "").replace("docs/", "") ) for file in all_files if file.startswith("docs/") and file.endswith(".md") ] message = textwrap.dedent( """ Hi 👋 You can find a preview of the docs here: {links} """ ).format(links="\n".join(links)) message = message.replace("%", "%25").replace("\n", "%0A").replace("\r", "%0D") output = "::set-output name=message::{}".format(message) print(output) shell: python - name: Create comment uses: peter-evans/create-or-update-comment@v1 with: issue-number: ${{ github.event.number }} body: ${{ steps.get-comment-message.outputs.message }} strawberry-graphql-0.287.0/.github/workflows/pre-release-pr.yml000066400000000000000000000060021511033167500244730ustar00rootroot00000000000000name: 🎁 Release test pre-release version on: repository_dispatch: types: [pre-release-command] jobs: pre-release: name: Pre-release runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 with: ref: ${{ github.event.client_payload.pull_request.head.sha }} - name: Set up Python uses: actions/setup-python@v4 with: python-version: "3.10" - name: Install deps run: | python -m pip install pip --upgrade pip install poetry pip install githubrelease pip install autopub pip install httpx - name: Check if release exists id: check_release run: echo "::set-output name=release::$(autopub check)" - name: Metadata id: metadata if: steps.check_release.outputs.release == '' run: echo "::set-output name=commit::$(git rev-parse HEAD)" - name: Find Release Comment uses: peter-evans/find-comment@v1 id: find_comment if: steps.check_release.outputs.release == '' with: token: ${{ secrets.BOT_TOKEN }} issue-number: ${{ github.event.client_payload.github.payload.issue.number }} comment-author: botberry body-includes: "# Pre-release" - name: Create or update comment uses: peter-evans/create-or-update-comment@v1 if: steps.check_release.outputs.release == '' with: token: ${{ secrets.BOT_TOKEN }} comment-id: ${{ steps.find_comment.outputs.comment-id }} issue-number: ${{ github.event.client_payload.github.payload.issue.number }} body: | # Pre-release :wave: Releasing commit [${{ steps.metadata.outputs.commit }}] to PyPi as pre-release! :package: edit-mode: replace - name: Publish pre-release version if: steps.check_release.outputs.release == '' id: release env: GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }} POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }} run: | autopub prepare poetry version $(poetry version -s).dev.$(date '+%s') poetry build poetry publish --username __token__ echo "::set-output name=version::$(poetry version -s)" - name: Create or update comment uses: peter-evans/create-or-update-comment@v1 if: steps.check_release.outputs.release == '' with: token: ${{ secrets.BOT_TOKEN }} comment-id: ${{ steps.find_comment.outputs.comment-id }} issue-number: ${{ github.event.client_payload.github.payload.issue.number }} body: | # Pre-release :wave: Pre-release **${{ steps.release.outputs.version }}** [${{ steps.metadata.outputs.commit }}] has been released on PyPi! :rocket: You can try it by doing: ```shell poetry add strawberry-graphql==${{ steps.release.outputs.version }} ``` edit-mode: replace strawberry-graphql-0.287.0/.github/workflows/release-check.yml000066400000000000000000000101151511033167500243430ustar00rootroot00000000000000name: 🆙 Release file check on: pull_request_target: types: [synchronize, reopened, opened, ready_for_review] branches: - main paths: - "strawberry/**" - "pyproject.toml" jobs: get-contributor-info: name: Get PR info runs-on: ubuntu-latest outputs: contributor-name: ${{ steps.get-info.outputs.contributor-name }} contributor-username: ${{ steps.get-info.outputs.contributor-username }} contributor-twitter-username: ${{ steps.get-info.outputs.contributor-twitter-username }} steps: - name: Get PR info id: get-info uses: strawberry-graphql/get-pr-info-action@v6 - run: echo "${{ steps.get-info.outputs.contributor-twitter-username }}" skip-if-bot: name: Set skip if PR is from a bot runs-on: ubuntu-latest needs: get-contributor-info outputs: skip: ${{ steps.skip.outputs.skip }} steps: - name: Set skip to true if contributor is a bot id: skip shell: python run: | bots = [ "dependabot-preview[bot]", "dependabot-preview", "dependabot", "dependabot[bot]", ] username = "${{ needs.get-contributor-info.outputs.contributor-username }}" if username in bots: print(f"Skipping {username} because it is a bot") print("::set-output name=skip::true") else: print("::set-output name=skip::false") release-file-check: name: Release check runs-on: ubuntu-latest needs: skip-if-bot if: needs.skip-if-bot.outputs.skip == 'false' outputs: changelog: ${{ steps.release-check.outputs.changelog }} status: ${{ steps.release-check.outputs.release_status }} change_type: ${{ steps.release-check.outputs.change_type }} steps: - name: Checkout code uses: actions/checkout@v2 with: ref: "refs/pull/${{ github.event.number }}/merge" - name: Release file check uses: strawberry-graphql/strawberry/.github/release-check-action@main id: release-check if: github.event.pull_request.draft == false read-tweet-md: name: Read TWEET.md runs-on: ubuntu-latest needs: [get-contributor-info, release-file-check] outputs: tweet: ${{ steps.extract.outputs.tweet }} steps: - uses: actions/checkout@v2 with: ref: "refs/pull/${{ github.event.number }}/merge" - name: Extract tweet message and changelog id: extract uses: strawberry-graphql/tweet-actions/read-tweet@v6 with: changelog: ${{ needs.release-file-check.outputs.changelog }} version: "(next)" contributor_name: ${{ needs.get-contributor-info.outputs.contributor-name }} contributor_twitter_username: ${{ needs.get-contributor-info.outputs.contributor-twitter-username }} validate-tweet: runs-on: ubuntu-latest needs: read-tweet-md if: ${{ needs.read-tweet-md.outputs.tweet != '' }} steps: - name: Validate tweet uses: strawberry-graphql/tweet-actions/validate-tweet@v6 with: tweet: ${{ needs.read-tweet-md.outputs.tweet }} send-comment: runs-on: ubuntu-latest needs: [release-file-check, read-tweet-md] if: github.event.pull_request.draft == false steps: - uses: actions/checkout@v2 - name: Send comment uses: ./.github/bot-action env: BOT_API_URL: ${{ secrets.BOT_API_URL }} API_SECRET: ${{ secrets.API_SECRET }} with: pr_number: ${{ github.event.number }} status: ${{ needs.release-file-check.outputs.status }} change_type: ${{ needs.release-file-check.outputs.change_type }} changelog_base64: ${{ needs.release-file-check.outputs.changelog }} tweet: ${{ needs.read-tweet-md.outputs.tweet }} fail-if-status-is-not-ok: runs-on: ubuntu-latest needs: [release-file-check, send-comment] steps: - name: Fail if status is not ok if: ${{ needs.release-file-check.outputs.status != 'OK' }} run: exit 1 strawberry-graphql-0.287.0/.github/workflows/release.yml000066400000000000000000000175671511033167500233120ustar00rootroot00000000000000name: 🆙 Release concurrency: release on: push: branches: - main jobs: release-file-check: name: Get information about release runs-on: ubuntu-latest outputs: changelog: ${{ steps.release-check.outputs.changelog }} status: ${{ steps.release-check.outputs.release_status }} change_type: ${{ steps.release-check.outputs.change_type }} steps: - uses: actions/checkout@v1 - name: Release file check uses: ./.github/release-check-action id: release-check release: name: Release runs-on: ubuntu-latest needs: release-file-check if: ${{ needs.release-file-check.outputs.status == 'OK' }} outputs: version: ${{ steps.get-version.outputs.version }} steps: - uses: actions/checkout@v2 with: persist-credentials: false - name: Set up Python uses: actions/setup-python@v4 with: python-version: "3.10" - name: Install deps run: | python -m pip install pip --upgrade pip install poetry pip install githubrelease pip install autopub pip install httpx - name: Check if we should release id: check_release run: | set +e echo ::set-output name=release::$(autopub check) - name: Publish if: steps.check_release.outputs.release == '' env: GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }} POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }} run: | git remote set-url origin https://${{ secrets.BOT_TOKEN }}@github.com/${{ github.repository }} autopub prepare poetry build autopub commit autopub githubrelease poetry publish --username __token__ - name: Get project version id: get-version shell: python run: | import os from pathlib import Path from autopub.base import get_project_version with Path(os.environ["GITHUB_OUTPUT"]).open('a') as f: f.write(f"version={get_project_version()}\n") get-contributor-info: name: Get PR info runs-on: ubuntu-latest needs: release-file-check if: ${{ needs.release-file-check.outputs.status == 'OK' }} outputs: contributor-name: ${{ steps.get-info.outputs.contributor-name }} contributor-username: ${{ steps.get-info.outputs.contributor-username }} contributor-twitter-username: ${{ steps.get-info.outputs.contributor-twitter-username }} pr-number: ${{ steps.get-info.outputs.pr-number }} steps: - name: Get PR info id: get-info uses: strawberry-graphql/get-pr-info-action@v6 update-release-on-github: name: Update release on GitHub runs-on: ubuntu-latest needs: [release-file-check, get-contributor-info, release] if: ${{ needs.release-file-check.outputs.status == 'OK' }} steps: - uses: actions/setup-python@v4 with: python-version: "3.10" - name: Install dependencies run: pip install httpx - name: Update release on GitHub shell: python run: | import os import httpx tag = os.environ["TAG"] contributor_username = os.environ["CONTRIBUTOR_USERNAME"] pr_number = os.environ["PR_NUMBER"] response = httpx.get( url=f"https://api.github.com/repos/strawberry-graphql/strawberry/releases/tags/{tag}", headers={ "Accept": "application/vnd.github.v3+json", }, ) response.raise_for_status() data = response.json() release_id = data["id"] release_body = data["body"].strip() release_footer = f""" Releases contributed by @{contributor_username} via #{pr_number} """.strip() updated_release_body = f"{release_body}\n\n{release_footer}" response = httpx.patch( url=f"https://api.github.com/repos/strawberry-graphql/strawberry/releases/{release_id}", json={"body": updated_release_body}, headers={ "Accept": "application/vnd.github.v3+json", "Authorization": f"token {os.environ['GITHUB_TOKEN']}", }, ) response.raise_for_status() env: GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }} TAG: ${{ needs.release.outputs.version }} CONTRIBUTOR_USERNAME: ${{ needs.get-contributor-info.outputs.contributor-username }} PR_NUMBER: ${{ needs.get-contributor-info.outputs.pr-number }} read-tweet-md: name: Read TWEET.md runs-on: ubuntu-latest needs: [release, get-contributor-info, release-file-check] if: ${{ needs.release-file-check.outputs.status == 'OK' && needs.get-contributor-info.outputs.contributor-name }} outputs: tweet: ${{ steps.extract.outputs.tweet }} has-tweet-file: ${{ steps.extract.outputs.has-tweet-file }} steps: - uses: actions/checkout@v1 - name: Extract tweet message and changelog id: extract uses: strawberry-graphql/tweet-actions/read-tweet@v6 with: changelog: ${{ needs.release-file-check.outputs.changelog }} version: ${{ needs.release.outputs.version }} contributor_name: ${{ needs.get-contributor-info.outputs.contributor-name }} contributor_twitter_username: ${{ needs.get-contributor-info.outputs.contributor-twitter-username }} send-tweet: name: Send tweet runs-on: ubuntu-latest needs: [release, read-tweet-md, get-contributor-info] if: ${{ needs.release-file-check.outputs.status == 'OK' }} steps: - uses: actions/setup-python@v4 with: python-version: "3.10" - name: Install dependencies run: pip install tweepy==4.14.0 - name: Send tweet shell: python run: | import base64 import os import tweepy TWITTER_CONSUMER_KEY = os.environ["TWITTER_API_KEY"] TWITTER_CONSUMER_SECRET = os.environ["TWITTER_API_SECRET"] TWITTER_ACCESS_TOKEN = os.environ["TWITTER_ACCESS_TOKEN"] TWITTER_ACCESS_TOKEN_SECRET = os.environ["TWITTER_ACCESS_TOKEN_SECRET"] TWITTER_BEARER_TOKEN = os.environ["TWITTER_BEARER_TOKEN"] tweepy_v2 = tweepy.Client( TWITTER_BEARER_TOKEN, consumer_key=TWITTER_CONSUMER_KEY, consumer_secret=TWITTER_CONSUMER_SECRET, access_token=TWITTER_ACCESS_TOKEN, access_token_secret=TWITTER_ACCESS_TOKEN_SECRET, ) def post_tweet() -> None: text = base64.b64decode(os.environ["TWEET"]).decode("utf-8") tweepy_v2.create_tweet(text=text) post_tweet() env: TWEET: ${{ needs.read-tweet-md.outputs.tweet }} TWITTER_API_KEY: ${{ secrets.TWITTER_API_KEY }} TWITTER_API_SECRET: ${{ secrets.TWITTER_API_SECRET }} TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }} TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} TWITTER_BEARER_TOKEN: ${{ secrets.TWITTER_BEARER_TOKEN }} remove-tweet-file: name: Remove TWEET.md if: always() && needs.read-tweet-md.outputs.has-tweet-file == 'true' needs: [send-tweet, read-tweet-md] runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 with: persist-credentials: false - name: Remove TWEET.md and commit env: GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }} run: | git config --global user.name 'Strawberry GraphQL Bot' git config --global user.email 'bot@strawberry.rocks' git remote set-url origin https://${{ secrets.BOT_TOKEN }}@github.com/${{ github.repository }} git pull git rm TWEET.md git commit -m "Remove TWEET.md" git push strawberry-graphql-0.287.0/.github/workflows/slash-commands.yml000066400000000000000000000006401511033167500245630ustar00rootroot00000000000000name: 💬 Slash Command Dispatch on: issue_comment: types: [created] jobs: slashCommandDispatch: runs-on: ubuntu-latest steps: - name: Slash Command Dispatch uses: peter-evans/slash-command-dispatch@v2 with: token: ${{ secrets.BOT_TOKEN }} reaction-token: ${{ secrets.BOT_TOKEN }} permission: admin commands: | pre-release strawberry-graphql-0.287.0/.github/workflows/test.yml000066400000000000000000000112011511033167500226240ustar00rootroot00000000000000name: 🔂 Unit tests concurrency: group: ${{ github.head_ref || github.run_id }} cancel-in-progress: true on: push: branches: [main] pull_request: branches: [main] paths: - "strawberry/**" - "tests/**" - "noxfile.py" - "pyproject.toml" - "poetry.lock" - ".github/workflows/test.yml" jobs: generate-jobs-tests: name: 💻 Generate test matrix runs-on: ubuntu-latest outputs: sessions: ${{ steps.set-matrix.outputs.sessions }} steps: - uses: actions/checkout@v4 - name: Install uv uses: astral-sh/setup-uv@v3 - run: uv venv - run: uv pip install poetry nox nox-poetry - id: set-matrix shell: bash run: | . .venv/bin/activate echo sessions=$( nox --json -t tests -l | jq 'map( { session, name: "\( .name ) on \( .python )\( if .call_spec != {} then " (\(.call_spec | to_entries | map("\(.key)=\(.value)") | join(", ")))" else "" end )" } )' ) | tee --append $GITHUB_OUTPUT unit-tests: name: 🔬 ${{ matrix.session.name }} needs: [generate-jobs-tests] runs-on: ubuntu-latest strategy: fail-fast: false matrix: session: ${{ fromJson(needs.generate-jobs-tests.outputs.sessions) }} steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: | 3.10 3.11 3.12 3.13 3.14 - run: pip install poetry nox nox-poetry uv - run: nox -r -t tests -s "${{ matrix.session.session }}" - uses: actions/upload-artifact@v4 if: ${{ always() }} with: name: coverage-${{ matrix.session.session }} path: coverage.xml upload-coverage: name: 🆙 Upload Coverage needs: [unit-tests] runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v4 - uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true verbose: true benchmarks: name: 📈 Benchmarks # Using this version because CodSpeed doesn't support Ubuntu 24.04 LTS yet runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - run: pipx install poetry - uses: actions/setup-python@v5 id: setup-python with: python-version: "3.12" architecture: x64 cache: "poetry" - run: poetry env use 3.12 - run: poetry install if: steps.setup-python.outputs.cache-hit != 'true' - name: Run benchmarks uses: CodSpeedHQ/action@v4.1.1 with: token: ${{ secrets.CODSPEED_TOKEN }} mode: instrumentation run: poetry run pytest tests/benchmarks --codspeed -p no:codeflash-benchmark lint: name: ✨ Lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: pipx install poetry - run: pipx install coverage - uses: actions/setup-python@v5 id: setup-python with: python-version: "3.12" cache: "poetry" - run: poetry install --with integrations if: steps.setup-python.outputs.cache-hit != 'true' - run: | mkdir .mypy_cache poetry run mypy --install-types --non-interactive --cache-dir=.mypy_cache/ --config-file mypy.ini unit-tests-on-windows: name: 🪟 Tests on Windows runs-on: windows-latest steps: - uses: actions/checkout@v4 - run: pipx install poetry - run: pipx install coverage - uses: actions/setup-python@v5 id: setup-python with: python-version: "3.11" cache: "poetry" - run: poetry install --with integrations if: steps.setup-python.outputs.cache-hit != 'true' # Since we are running all the integrations at once, we can't use # pydantic v2. It is not compatible with starlette yet - run: poetry run pip install pydantic==1.10 # we use poetry directly instead of nox since we want to # test all integrations at once on windows # but we want to exclude tests/mypy since we are using an old version of pydantic - run: | poetry run pytest --cov=. --cov-append --cov-report=xml -n auto --showlocals --ignore tests/mypy -vv - name: coverage xml run: coverage xml -i if: ${{ always() }} - uses: codecov/codecov-action@v4 if: ${{ always() }} with: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true verbose: true strawberry-graphql-0.287.0/.gitignore000066400000000000000000000051351511033167500175260ustar00rootroot00000000000000# Created by https://www.gitignore.io/api/macos,linux,python,windows # Edit at https://www.gitignore.io/?templates=macos,linux,python,windows ### Linux ### *~ # temporary files which can be created if a process still has a handle open of a deleted file .fuse_hidden* # KDE directory preferences .directory # Linux trash folder which might appear on any partition or disk .Trash-* # .nfs files are created when an open file is removed but is still being accessed .nfs* ### macOS ### # General .DS_Store .AppleDouble .LSOverride # Icon must end with two \r Icon # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns .com.apple.timemachine.donotpresent # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk ### Python ### # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ Pipfile Pipfile.lock # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ ### Python Patch ### .venv/ ### Windows ### # Windows thumbnail cache files Thumbs.db ehthumbs.db ehthumbs_vista.db # Dump file *.stackdump # Folder config file [Dd]esktop.ini # Recycle Bin used on file shares $RECYCLE.BIN/ # Windows Installer files *.cab *.msi *.msix *.msm *.msp # Windows shortcuts *.lnk # End of https://www.gitignore.io/api/macos,linux,python,windows .vscode/ .idea/ node_modules/ strawberry-graphql-0.287.0/.gitpod.yml000066400000000000000000000004021511033167500176150ustar00rootroot00000000000000tasks: - before: | curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python - source $HOME/.poetry/env pip install pre-commit init: | poetry install pre-commit install --install-hooks strawberry-graphql-0.287.0/.pre-commit-config.yaml000066400000000000000000000021451511033167500220150ustar00rootroot00000000000000repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.14.5 hooks: - id: ruff-format exclude: ^tests/\w+/snapshots/ - id: ruff exclude: ^tests/\w+/snapshots/ - repo: https://github.com/patrick91/pre-commit-alex rev: aa5da9e54b92ab7284feddeaf52edf14b1690de3 hooks: - id: alex exclude: (CHANGELOG|TWEET).md - repo: https://github.com/pre-commit/mirrors-prettier rev: v4.0.0-alpha.8 hooks: - id: prettier files: '^docs/.*\.mdx?$' - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - id: trailing-whitespace - id: check-merge-conflict - id: end-of-file-fixer exclude: ^tests/relay/snapshots - id: check-toml - id: no-commit-to-branch args: ["--branch", "main"] - repo: https://github.com/adamchainz/blacken-docs rev: 1.20.0 hooks: - id: blacken-docs args: [--skip-errors] files: '\.(rst|md|markdown|py|mdx)$' - repo: https://github.com/ComPWA/taplo-pre-commit rev: v0.9.3 hooks: - id: taplo-format strawberry-graphql-0.287.0/.prettierrc000066400000000000000000000000601511033167500177120ustar00rootroot00000000000000{ "proseWrap": "always", "printWidth": 80 } strawberry-graphql-0.287.0/CHANGELOG.md000066400000000000000000013305111511033167500173470ustar00rootroot00000000000000CHANGELOG ========= 0.287.0 - 2025-11-22 -------------------- Change `strawberry.http.base.BaseView.encode_json()` type hint to `str | bytes` and adjust dependent code appropriately. Contributed by [David](https://github.com/Brandieee) via [PR #4054](https://github.com/strawberry-graphql/strawberry/pull/4054/) 0.286.1 - 2025-11-21 -------------------- Set Content-Type to `text/plain` for exceptions so that these are displayed correctly. Contributed by [Michael Gorven](https://github.com/mgorven) via [PR #4037](https://github.com/strawberry-graphql/strawberry/pull/4037/) 0.286.0 - 2025-11-18 -------------------- This release changes `_enum_definition` to `__strawberry_definition__`, this is a follow up to previous internal changes. If you were relying on `_enum_definition` you should update your code to use `__strawberry_definition__`. We also expose `has_enum_definition` to check if a type is a strawberry enum definition. ```python from enum import Enum import strawberry from strawberry.types.enum import StrawberryEnumDefinition, has_enum_definition @strawberry.enum class ExampleEnum(Enum): pass has_enum_definition(ExampleEnum) # True # Now you can use ExampleEnum.__strawberry_definition__ to access the enum definition ``` Contributed by [Luis Gustavo](https://github.com/Ckk3) via [PR #3999](https://github.com/strawberry-graphql/strawberry/pull/3999/) 0.285.0 - 2025-11-10 -------------------- This release removes support for Apollo Federation v1 and improves Federation v2 support with explicit version control and new directives. ## Breaking Changes - **Removed support for Apollo Federation v1**: All schemas now use Federation v2 - **Removed `enable_federation_2` parameter**: Replaced with `federation_version` parameter - Federation v2 is now always enabled with version 2.11 as the default ## Migration ### If you were using `enable_federation_2=True` Remove the parameter: ```python # Before schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) # After schema = strawberry.federation.Schema(query=Query) ``` ### If you were using Federation v1 You must migrate to Federation v2. See the [breaking changes documentation](https://strawberry.rocks/docs/breaking-changes/0.285.0) for detailed migration instructions. ## New Features - **Version control**: Specify Federation v2 versions (2.0 - 2.11): ```python schema = strawberry.federation.Schema( query=Query, federation_version="2.5" # Specify a specific version if needed ) ``` - **New directives**: Added support for `@context`, `@fromContext`, `@cost`, and `@listSize` directives (v2.7+) - **Automatic validation**: Ensures directives are compatible with your chosen federation version - **Improved performance**: Faster version parsing using dictionary lookups Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #4045](https://github.com/strawberry-graphql/strawberry/pull/4045/) 0.284.4 - 2025-11-10 -------------------- Bumped minimum Typer version to fix strawberry CLI commands. Contributed by [Val Liu](https://github.com/valliu) via [PR #4049](https://github.com/strawberry-graphql/strawberry/pull/4049/) 0.284.3 - 2025-11-10 -------------------- Removed `Transfer-Encoding: chunked` header from multipart streaming responses. This fixes HTTP 405 errors on Vercel and other serverless platforms. The server/gateway will handle chunked encoding automatically when needed. Contributed by [Louis Amon](https://github.com/LouisAmon) via [PR #4047](https://github.com/strawberry-graphql/strawberry/pull/4047/) 0.284.2 - 2025-11-05 -------------------- Update typing of `variables` in the test clients to match the actual flexibility of this field. Contributed by [Ben XO](https://github.com/ben-xo) via [PR #4044](https://github.com/strawberry-graphql/strawberry/pull/4044/) 0.284.1 - 2025-10-18 -------------------- This release fixes the usage of `strawberry.Maybe` inside modules using `from __future__ import annotations` Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #4031](https://github.com/strawberry-graphql/strawberry/pull/4031/) 0.284.0 - 2025-10-17 -------------------- This release drops support for Python 3.9, which reached its end-of-life (EOL) in October 2025. The minimum supported Python version is now 3.10. We strongly recommend upgrading to Python 3.10 or a newer version, as older versions are no longer maintained and may contain security vulnerabilities. Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #4018](https://github.com/strawberry-graphql/strawberry/pull/4018/) 0.283.3 - 2025-10-10 -------------------- Adds support for lazy unions. Contributed by [Radosław Cybulski](https://github.com/rcybulski1122012) via [PR #4017](https://github.com/strawberry-graphql/strawberry/pull/4017/) 0.283.2 - 2025-10-07 -------------------- This release adds support for the upcoming Python 3.14 Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3828](https://github.com/strawberry-graphql/strawberry/pull/3828/) 0.283.1 - 2025-10-06 -------------------- Fixed multipart subscription header parsing to properly handle optional boundary parameters and quoted subscription spec values. This improves compatibility with different GraphQL clients that may send headers in various formats. **Key improvements:** - Made the `boundary=graphql` parameter optional in multipart subscription detection - Added proper quote stripping for `subscriptionSpec` values (e.g., `subscriptionSpec="1.0"`) - Enhanced test coverage for different header format scenarios **Example of supported headers:** ```raw Accept: multipart/mixed;boundary=graphql;subscriptionSpec=1.0,application/json Accept: multipart/mixed;subscriptionSpec="1.0",application/json ``` Contributed by [Louis Amon](https://github.com/LouisAmon) via [PR #4002](https://github.com/strawberry-graphql/strawberry/pull/4002/) 0.283.0 - 2025-10-06 -------------------- In this release, we renamed the `strawberry server` command to `strawberry dev` to better reflect its purpose as a development server. We also deprecated the `strawberry-graphql[debug-server]` extra in favor of `strawberry-graphql[cli]`. Please update your dependencies accordingly. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) via [PR #4011](https://github.com/strawberry-graphql/strawberry/pull/4011/) 0.282.0 - 2025-09-07 -------------------- This release fixes mask errors to support strawberry execution results. Contributed by [Aryan Iyappan](https://github.com/aryaniyaps) via [PR #3987](https://github.com/strawberry-graphql/strawberry/pull/3987/) 0.281.0 - 2025-08-26 -------------------- In this release we removed the `--log-operations` option from the `strawberry server` command. The option only worked for WebSocket connections to the debug server, and had limited utility. Removing this option allowed us to remove the undocumented `debug` option from all HTTP view integrations and WebSocket protocol implementation, simplifying the codebase. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) via [PR #3979](https://github.com/strawberry-graphql/strawberry/pull/3979/) 0.280.0 - 2025-08-19 -------------------- This release unifies the format of HTTP error response bodies across all HTTP view integrations. Previously, the Chalice integration used a custom JSON body response different from the plain string used by other integrations. Now, all integrations will return a plain string for HTTP error responses. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) via [PR #3978](https://github.com/strawberry-graphql/strawberry/pull/3978/) 0.279.0 - 2025-08-19 -------------------- This release changes the `strawberry.Maybe` type to provide a more consistent and intuitive API for handling optional fields in GraphQL inputs. **Breaking Change**: The `Maybe` type definition has been changed from `Union[Some[Union[T, None]], None]` to `Union[Some[T], None]`. This means: - `Maybe[str]` now only accepts string values or absent fields (refuses explicit null) - `Maybe[str | None]` accepts strings, null, or absent fields (maintains previous behavior) This provides a cleaner API where `if field is not None` consistently means "field was provided" for all Maybe fields. A codemod is available to automatically migrate your code: `strawberry upgrade maybe-optional` See the [breaking changes documentation](https://strawberry.rocks/docs/breaking-changes/0.279.0) for migration details. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3961](https://github.com/strawberry-graphql/strawberry/pull/3961/) 0.278.1 - 2025-08-05 -------------------- This release removes some internal code in favour of using an external dependency, this will help us with maintaining the codebase in the future 😊 Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3967](https://github.com/strawberry-graphql/strawberry/pull/3967/) 0.278.0 - 2025-07-19 -------------------- ## Add GraphQL Query batching support GraphQL query batching is now supported across all frameworks (sync and async) To enable query batching, add a valid `batching_config` to the schema configuration. This makes your GraphQL API compatible with batching features supported by various client side libraries, such as [Apollo GraphQL](https://www.apollographql.com/docs/react/api/link/apollo-link-batch-http) and [Relay](https://github.com/relay-tools/react-relay-network-modern?tab=readme-ov-file#batching-several-requests-into-one). Example (FastAPI): ```py import strawberry from fastapi import FastAPI from strawberry.fastapi import GraphQLRouter from strawberry.schema.config import StrawberryConfig @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "Hello World" schema = strawberry.Schema( Query, config=StrawberryConfig(batching_config={"max_operations": 10}) ) graphql_app = GraphQLRouter(schema) app = FastAPI() app.include_router(graphql_app, prefix="/graphql") ``` Example (Flask): ```py import strawberry from flask import Flask from strawberry.flask.views import GraphQLView app = Flask(__name__) @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "Hello World" schema = strawberry.Schema( Query, config=StrawberryConfig(batching_config={"max_operations": 10}) ) app.add_url_rule( "/graphql/batch", view_func=GraphQLView.as_view("graphql_view", schema=schema), ) if __name__ == "__main__": app.run() ``` Note: Query Batching is not supported for multipart subscriptions Contributed by [Aryan Iyappan](https://github.com/aryaniyaps) via [PR #3755](https://github.com/strawberry-graphql/strawberry/pull/3755/) 0.277.1 - 2025-07-19 -------------------- This release fixes the resolution of `Generics` when specializing using a union defined with `Annotated`, like in the example below: ```python from typing import Annotated, Generic, TypeVar, Union import strawberry T = TypeVar("T") @strawberry.type class User: name: str age: int @strawberry.type class ProUser: name: str age: float @strawberry.type class GenType(Generic[T]): data: T GeneralUser = Annotated[Union[User, ProUser], strawberry.union("GeneralUser")] @strawberry.type class Response(GenType[GeneralUser]): ... @strawberry.type class Query: @strawberry.field def user(self) -> Response: ... schema = strawberry.Schema(query=Query) ``` Before this would raise a `TypeError`, now it works as expected. Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #3950](https://github.com/strawberry-graphql/strawberry/pull/3950/) 0.277.0 - 2025-07-18 -------------------- This release adds experimental support for GraphQL's `@defer` and `@stream` directives, enabling incremental delivery of response data. Note: this only works when using Strawberry with `graphql-core>=3.3.0a9`. ## Features - **`@defer` directive**: Allows fields to be resolved asynchronously and delivered incrementally - **`@stream` directive**: Enables streaming of list fields using the new `strawberry.Streamable` type - **`strawberry.Streamable[T]`**: A new generic type for defining streamable fields that work with `@stream` ## Configuration To enable these experimental features, configure your schema with: ```python from strawberry.schema.config import StrawberryConfig schema = strawberry.Schema( query=Query, config=StrawberryConfig(enable_experimental_incremental_execution=True) ) ``` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3819](https://github.com/strawberry-graphql/strawberry/pull/3819/) 0.276.2 - 2025-07-18 -------------------- This release renames the `ExecutionContext.errors` attribute to `ExecutionContext.pre_execution_errors` to better reflect its purpose. The old `errors` attribute is now deprecated but still available for backward compatibility. The `pre_execution_errors` attribute specifically stores errors that occur during the pre-execution phase (parsing and validation), making the distinction clearer from errors that might occur during the actual execution phase. For backward compatibility, accessing `ExecutionContext.errors` will now emit a deprecation warning and return the value of `pre_execution_errors`. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3947](https://github.com/strawberry-graphql/strawberry/pull/3947/) 0.276.1 - 2025-07-18 -------------------- This release fixes an issue where `DuplicatedTypeName` exception would be raised for nested generics like in the example below: ```python from typing import Generic, TypeVar import strawberry T = TypeVar("T") @strawberry.type class Wrapper(Generic[T]): value: T @strawberry.type class Query: a: Wrapper[Wrapper[int]] b: Wrapper[Wrapper[int]] schema = strawberry.Schema(query=Query) ``` This piece of code and similar ones will now work correctly. Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #3946](https://github.com/strawberry-graphql/strawberry/pull/3946/) 0.276.0 - 2025-07-14 -------------------- This release fixes NameConverter to properly handle lazy types. Contributed by [Radosław Cybulski](https://github.com/rcybulski1122012) via [PR #3944](https://github.com/strawberry-graphql/strawberry/pull/3944/) 0.275.7 - 2025-07-14 -------------------- This release adds support for lazy types in ConnectionExtension. Contributed by [Radosław Cybulski](https://github.com/rcybulski1122012) via [PR #3941](https://github.com/strawberry-graphql/strawberry/pull/3941/) 0.275.6 - 2025-07-13 -------------------- In this release, we updated Strawberry to gracefully handle requests containing an invalid `extensions` parameter. Previously, such requests could result in internal server errors. Now, Strawberry will return a 400 Bad Request response with a clear error message, conforming to the GraphQL over HTTP specification. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) via [PR #3943](https://github.com/strawberry-graphql/strawberry/pull/3943/) 0.275.5 - 2025-06-26 -------------------- This release improves performance of argument conversion for lists of primitives. Contributed by [blothmann](https://github.com/blothmann) via [PR #3773](https://github.com/strawberry-graphql/strawberry/pull/3773/) 0.275.4 - 2025-06-26 -------------------- In this release, we updated Strawberry to gracefully handle requests containing an invalid `variables` parameter. Previously, such requests could result in internal server errors. Now, Strawberry will return a 400 Bad Request response with a clear error message, conforming to the GraphQL over HTTP specification. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) via [PR #3932](https://github.com/strawberry-graphql/strawberry/pull/3932/) 0.275.3 - 2025-06-25 -------------------- In this release, we updated Strawberry to gracefully handle requests containing an invalid `query` parameter. Previously, such requests could result in internal server errors, but now they will return a 400 Bad Request response with an appropriate error message, conforming to the GraphQL over HTTP specification. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) via [PR #3927](https://github.com/strawberry-graphql/strawberry/pull/3927/) 0.275.2 - 2025-06-22 -------------------- Fixes a bug that caused merged unions with duplicated entries to fail the schema validation when merging two `strawberry.union` types. Contributed by [Erik Wrede](https://github.com/erikwrede) via [PR #3923](https://github.com/strawberry-graphql/strawberry/pull/3923/) 0.275.1 - 2025-06-22 -------------------- In this release, we updated the `aiohttp` integration to handle `aiohttp.ClientConnectionResetError`s, which can occur when a WebSocket connection is unexpectedly closed, gracefully. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) via [PR #3922](https://github.com/strawberry-graphql/strawberry/pull/3922/) 0.275.0 - 2025-06-20 -------------------- Adds a new CLI command `strawberry locate-definition` that allows you to find the source location of a definition in the schema. ``` strawberry locate-definition path.to.schema:schema ObjectName ``` ``` strawberry locate-definition path.to.schema:schema ObjectName.fieldName ``` Results take the form of `path/to/file.py:line:column`, for example: `src/models/user.py:45:12`. This can be used, for example, with the go to definition feature of VS Code's Relay extension (configured via the `relay.pathToLocateCommand` setting). Contributed by [Sam Millar](https://github.com/millar) via [PR #3902](https://github.com/strawberry-graphql/strawberry/pull/3902/) 0.274.3 - 2025-06-19 -------------------- This release adds compatibility with LibCST v1.8 Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3921](https://github.com/strawberry-graphql/strawberry/pull/3921/) 0.274.2 - 2025-06-18 -------------------- Introduces an optional operation_extensions parameter throughout the GraphQL execution flow—adding it to execution entry points and embedding it into the ExecutionContext—so custom extensions can access per-operation metadata. Contributed by [Matt Gilene](https://github.com/mdgilene) via [PR #3878](https://github.com/strawberry-graphql/strawberry/pull/3878/) 0.274.1 - 2025-06-18 -------------------- This release fixes an issue that caused schema generation with `Maybe` to fail when using lists, such as `Maybe[List[User]]`. Contributed by [Erik Wrede](https://github.com/erikwrede) via [PR #3920](https://github.com/strawberry-graphql/strawberry/pull/3920/) 0.274.0 - 2025-06-16 -------------------- In this release, we fixed various edge cases around operation selection in GraphQL documents. Now, operation selection works consistently across all protocols, both in documents with single and multiple operations. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) via [PR #3916](https://github.com/strawberry-graphql/strawberry/pull/3916/) 0.273.3 - 2025-06-16 -------------------- In this release, we updated the type hints for `subscription_protocols` across all HTTP view integrations. It's now consistently defined as `Sequence[str]`, the minimum type required by Strawberry. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) via [PR #3910](https://github.com/strawberry-graphql/strawberry/pull/3910/) 0.273.2 - 2025-06-15 -------------------- In this release, we replaced the usage of an undocumented AIOHTTP `MultipartReader` API with the intended public API. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) via [PR #3906](https://github.com/strawberry-graphql/strawberry/pull/3906/) 0.273.1 - 2025-06-15 -------------------- This release fixes that the Chalice HTTP view integration did not set appropriate content-type headers for responses, as it's recommended by the GraphQL over HTTP specification. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) via [PR #3904](https://github.com/strawberry-graphql/strawberry/pull/3904/) 0.273.0 - 2025-06-10 -------------------- Starting with this release, Strawberry will throw an error if one of your input types tries to inherit from one or more interfaces. This new error enforces the GraphQL specification that input types cannot implement interfaces. The following code, for example, will now throw an error: ```python import strawberry @strawberry.interface class SomeInterface: some_field: str @strawberry.input class SomeInput(SomeInterface): another_field: int ``` Contributed by [Ivan Gonzalez](https://github.com/scratchmex) via [PR #1254](https://github.com/strawberry-graphql/strawberry/pull/1254/) 0.272.1 - 2025-06-10 -------------------- This release modifies export-schema cli to include an EOF newline if --output option is provided. This allows better review in github.com for the generated schema files. Contributed by [Yunkai Zhou](https://github.com/yunkaiz) via [PR #3896](https://github.com/strawberry-graphql/strawberry/pull/3896/) 0.272.0 - 2025-06-10 -------------------- This release features a dedicated extension to disable introspection queries. Disabling introspection queries was already possible using the `AddValidationRules` extension. However, using this new extension requires fewer steps and makes the feature more discoverable. ## Usage example: ```python import strawberry from strawberry.extensions import DisableIntrospection @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "Hello, world!" schema = strawberry.Schema( Query, extensions=[ DisableIntrospection(), ], ) ``` Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) via [PR #3895](https://github.com/strawberry-graphql/strawberry/pull/3895/) 0.271.2 - 2025-06-09 -------------------- This release fixes an `AttributeError` that occurred when a fragment and an `OperationDefinitionNode` shared the same name, and the fragment appeared first in the document. The following example will now work as expected: ```graphql fragment UserAgent on UserAgentType { id } query UserAgent { userAgent { ...UserAgent } } ``` Contributed by [Arthur](https://github.com/Speedy1991) via [PR #3882](https://github.com/strawberry-graphql/strawberry/pull/3882/) 0.271.1 - 2025-06-07 -------------------- This Release contains fix of enum value was not working in generic container in lazy union. Contributed by [Alex](https://github.com/benzolium) via [PR #3883](https://github.com/strawberry-graphql/strawberry/pull/3883/) 0.271.0 - 2025-06-04 -------------------- Added a new configuration option `_unsafe_disable_same_type_validation` that allows disabling the same type validation check in the schema converter. This is useful in cases where you need to have multiple type definitions with the same name in your schema. Example: ```python @strawberry.type(name="DuplicatedType") class A: a: int @strawberry.type(name="DuplicatedType") class B: b: int schema = strawberry.Schema( query=Query, types=[A, B], config=strawberry.StrawberryConfig(_unsafe_disable_same_type_validation=True), ) ``` Note: This is an unsafe option and should be used with caution as it bypasses a safety check in the schema converter. Contributed by [Asylbek](https://github.com/narmatov-asylbek) via [PR #3887](https://github.com/strawberry-graphql/strawberry/pull/3887/) 0.270.6 - 2025-06-04 -------------------- This release fixes that the `create_type` tool asked users to pass a `name` for fields without resolvers even when a `name` was already provided. The following code now works as expected: ```python import strawberry from strawberry.tools import create_type first_name = strawberry.field(name="firstName") Query = create_type(f"Query", [first_name]) ``` Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) via [PR #3885](https://github.com/strawberry-graphql/strawberry/pull/3885/) 0.270.5 - 2025-06-01 -------------------- In this release, we improved some GraphQL over WS error messages. More precise error messages are now returned if Strawberry fails to find an operation in the query document. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) via [PR #3869](https://github.com/strawberry-graphql/strawberry/pull/3869/) 0.270.4 - 2025-05-29 -------------------- This release fixes that the Strawberry debug server no longer supported WebSockets out of the box. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) via [PR #3872](https://github.com/strawberry-graphql/strawberry/pull/3872/) 0.270.3 - 2025-05-29 -------------------- This release fixes an dependency issue with the Strawberry CLI and libcst. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) via [PR #3875](https://github.com/strawberry-graphql/strawberry/pull/3875/) 0.270.2 - 2025-05-24 -------------------- This release resolves the issue of subscriptions started via the legacy `graphql-ws` WebSocket subprotocol getting stuck if a non-existing `operationName` was specified. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) via [PR #3858](https://github.com/strawberry-graphql/strawberry/pull/3858/) 0.270.1 - 2025-05-22 -------------------- Fix multipart subscriptions by always yielding the closing boundary if it's enqueued. Contributed by [Roger Yang](https://github.com/RogerHYang) via [PR #3866](https://github.com/strawberry-graphql/strawberry/pull/3866/) 0.270.0 - 2025-05-20 -------------------- This release adds support for GraphQL over WebSocket transport protocols to the Quart integration. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) via [PR #3860](https://github.com/strawberry-graphql/strawberry/pull/3860/) 0.269.0 - 2025-05-17 -------------------- This release adds support for input extension (To explain and document) Contributed by [Omar Marzouk](https://github.com/omarzouk) via [PR #3461](https://github.com/strawberry-graphql/strawberry/pull/3461/) 0.268.2 - 2025-05-17 -------------------- This release (finally) fixes support for using `ID` and `GlobalID` in the same schema. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3859](https://github.com/strawberry-graphql/strawberry/pull/3859/) 0.268.1 - 2025-05-12 -------------------- This releases fixed an issue that prevented from using `ID` and `GlobalID` at the same time, like in this example: ```python import strawberry from strawberry.relay.types import GlobalID @strawberry.type class Query: @strawberry.field def hello(self, id: GlobalID) -> str: return "Hello World" @strawberry.field def hello2(self, id: strawberry.ID) -> str: return "Hello World" schema = strawberry.Schema( Query, ) ``` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3855](https://github.com/strawberry-graphql/strawberry/pull/3855/) 0.268.0 - 2025-05-10 -------------------- This release renames the generated type from `GlobalID` to `ID` in the GraphQL schema. This means that when using `relay.Node`, like in this example: ```python @strawberry.type class Fruit(relay.Node): code: relay.NodeID[int] name: str ``` You'd create a GraphQL type that looks like this: ```graphql type Fruit implements Node { id: ID! name: String! } ``` while previously you'd get this: ```graphql type Fruit implements Node { id: GlobalID! name: String! } ``` The runtime behaviour is still the same, so if you want to use `GlobalID` in Python code, you can still do so, for example: ```python @strawberry.type class Mutation: @strawberry.mutation @staticmethod async def update_fruit_weight(id: relay.GlobalID, weight: float) -> Fruit: # while `id` is a GraphQL `ID` type, here is still an instance of `relay.GlobalID` fruit = await id.resolve_node(info, ensure_type=Fruit) fruit.weight = weight return fruit ``` If you want to revert this change, and keep `GlobalID` in the schema, you can use the following configuration: ```python schema = strawberry.Schema( query=Query, config=StrawberryConfig(relay_use_legacy_global_id=True) ) ``` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3853](https://github.com/strawberry-graphql/strawberry/pull/3853/) 0.267.0 - 2025-05-10 -------------------- This release adds support to use `strawberry.Parent` with future annotations. For example, the following code will now work as intended: ```python from __future__ import annotations def get_full_name(user: strawberry.Parent[User]) -> str: return f"{user.first_name} {user.last_name}" @strawberry.type class User: first_name: str last_name: str full_name: str = strawberry.field(resolver=get_full_name) @strawberry.type class Query: @strawberry.field def user(self) -> User: return User(first_name="John", last_name="Doe") schema = strawberry.Schema(query=Query) ``` Or even when not using future annotations, but delaying the evaluation of `User`, like: ```python # Note the User being delayed by passing it as a string def get_full_name(user: strawberry.Parent["User"]) -> str: return f"{user.first_name} {user.last_name}" @strawberry.type class User: first_name: str last_name: str full_name: str = strawberry.field(resolver=get_full_name) @strawberry.type class Query: @strawberry.field def user(self) -> User: return User(first_name="John", last_name="Doe") schema = strawberry.Schema(query=Query) ``` Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #3851](https://github.com/strawberry-graphql/strawberry/pull/3851/) 0.266.1 - 2025-05-06 -------------------- This release adds a new (preferable) way to handle optional updates. Up until now when you wanted to inffer if an input value was null or absent you'd use `strawberry.UNSET` which is a bit cumbersome and error prone. Now you can use `strawberry.Maybe` to identify if a value was provided or not. e.g. ```python import strawberry @strawberry.type class User: name: str phone: str | None @strawberry.input class UpdateUserInput: name: str phone: strawberry.Maybe[str] @strawberry.type class Mutation: def update_user(self, input: UpdateUserInput) -> None: reveal_type(input.phone) # strawberry.Some[str | None] | None if input.phone: reveal_type(input.phone.value) # str | None update_user_phone(input.phone.value) ``` Or, if you can use pattern matching: ```python @strawberry.type class Mutation: def update_user(self, input: UpdateUserInput) -> None: match input.phone: case strawberry.Some(value=value): update_user_phone(input.phone.value) ``` You can also use `strawberry.Maybe` as a field argument like so ```python import strawberry @strawberry.field def filter_users(self, phone: strawberry.Maybe[str] = None) -> list[User]: if phone: return filter_users_by_phone(phone.value) return get_all_users() ``` Contributed by [ניר](https://github.com/nrbnlulu) via [PR #3791](https://github.com/strawberry-graphql/strawberry/pull/3791/) 0.266.0 - 2025-04-19 -------------------- This release adds support for custom names in enum values using the `name` parameter in `strawberry.enum_value`. This allows you to specify a different name for an enum value in the GraphQL schema while keeping the original Python enum member name. For example: ```python @strawberry.enum class IceCreamFlavour(Enum): VANILLA = "vanilla" CHOCOLATE_COOKIE = strawberry.enum_value("chocolate", name="chocolateCookie") ``` This will produce a GraphQL schema with the custom name: ```graphql enum IceCreamFlavour { VANILLA chocolateCookie } ``` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3841](https://github.com/strawberry-graphql/strawberry/pull/3841/) 0.265.1 - 2025-04-15 -------------------- Fix bug where files would be converted into io.BytesIO when using the sanic GraphQLView instead of using the sanic File type Contributed by [Maypher](https://github.com/Maypher) via [PR #3751](https://github.com/strawberry-graphql/strawberry/pull/3751/) 0.265.0 - 2025-04-15 -------------------- This release adds support for using strawberry.union with generics, like in this example: ```python @strawberry.type class ObjectQueries[T]: @strawberry.field def by_id( self, id: strawberry.ID ) -> Union[T, Annotated[NotFoundError, strawberry.union("ByIdResult")]]: ... @strawberry.type class Query: @strawberry.field def some_type_queries(self, id: strawberry.ID) -> ObjectQueries[SomeType]: ... ``` which, now, creates a correct union type named `SomeTypeByIdResult` Contributed by [Jacob Allen](https://github.com/enoua5) via [PR #3515](https://github.com/strawberry-graphql/strawberry/pull/3515/) 0.264.1 - 2025-04-15 -------------------- Change pydantic conversion to not load field data unless requested Contributed by [Mark Moes](https://github.com/Mark90) via [PR #3812](https://github.com/strawberry-graphql/strawberry/pull/3812/) 0.264.0 - 2025-04-12 -------------------- This releases improves support for `relay.Edge` subclasses. `resolve_edge` now accepts `**kwargs`, so custom fields can be added to your edge classes without wholly replacing `resolve_edge`: ```python @strawberry.type(name="Edge", description="An edge in a connection.") class CustomEdge(relay.Edge[NodeType]): index: int @classmethod def resolve_edge(cls, node: NodeType, *, cursor: Any = None, **kwargs: Any) -> Self: assert isinstance(cursor, int) return super().resolve_edge(node, cursor=cursor, index=cursor, **kwargs) ``` You can also specify a custom cursor prefix, in case you want to implement a different kind of cursor than a plain `ListConnection`: ```python @strawberry.type(name="Edge", description="An edge in a connection.") class CustomEdge(relay.Edge[NodeType]): CURSOR_PREFIX: ClassVar[str] = "mycursor" ``` Contributed by [Take Weiland](https://github.com/diesieben07) via [PR #3836](https://github.com/strawberry-graphql/strawberry/pull/3836/) 0.263.2 - 2025-04-05 -------------------- This release contains a few improvements to how `AsyncGenerators` are handled by strawberry codebase, ensuring they get properly closed in case of unexpected errors. Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #3834](https://github.com/strawberry-graphql/strawberry/pull/3834/) 0.263.1 - 2025-04-04 -------------------- This releases add support for passing in a custom `TracerProvider` to the `OpenTelemetryExtension`. Contributed by [Chase Dorsey](https://github.com/cdorsey) via [PR #3830](https://github.com/strawberry-graphql/strawberry/pull/3830/) 0.263.0 - 2025-04-01 -------------------- Adds the ability to include pydantic computed fields when using pydantic.type decorator. Example: ```python class UserModel(pydantic.BaseModel): age: int @computed_field @property def next_age(self) -> int: return self.age + 1 @strawberry.experimental.pydantic.type( UserModel, all_fields=True, include_computed=True ) class User: pass ``` Will allow `nextAge` to be requested from a user entity. Contributed by [Tyler Nisonoff](https://github.com/tylernisonoff) via [PR #3798](https://github.com/strawberry-graphql/strawberry/pull/3798/) 0.262.6 - 2025-03-28 -------------------- This release updates the Content-Type header from ⁠`"text/html"` to `⁠"text/html; charset=utf-8"` to prevent the GraphQL IDE from displaying unusual or incorrect characters. Contributed by [Moritz Ulmer](https://github.com/moritz89) via [PR #3824](https://github.com/strawberry-graphql/strawberry/pull/3824/) 0.262.5 - 2025-03-13 -------------------- This release updates the internals of our subscription implementation, to make the code easier to maintain for future changes. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3787](https://github.com/strawberry-graphql/strawberry/pull/3787/) 0.262.4 - 2025-03-13 -------------------- This release adds support for the upcoming version of Pydantic (2.11) Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3808](https://github.com/strawberry-graphql/strawberry/pull/3808/) 0.262.3 - 2025-03-13 -------------------- This release changes the required version of packaging from >=24 to >=23, in order to allow using Strawberry on https://play.strawberry.rocks Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3806](https://github.com/strawberry-graphql/strawberry/pull/3806/) 0.262.2 - 2025-03-12 -------------------- This release adds missing `packaging` dependency required by `DatadogTracingExtension` Contributed by [Jakub Bacic](https://github.com/jakub-bacic) via [PR #3803](https://github.com/strawberry-graphql/strawberry/pull/3803/) 0.262.1 - 2025-03-06 -------------------- This release updates the handling of the Django `graphql/graphiql.html` template, if provided; it will now receive the current request as context. Contributed by [ash](https://github.com/sersorrel) via [PR #3800](https://github.com/strawberry-graphql/strawberry/pull/3800/) 0.262.0 - 2025-03-04 -------------------- This release adds support for exporting schema created by a callable: ```bash strawberry export-schema package.module:create_schema ``` when ```python def create_schema(): return strawberry.Schema(query=Query) ``` Contributed by [Alexey Pelykh](https://github.com/alexey-pelykh) via [PR #3797](https://github.com/strawberry-graphql/strawberry/pull/3797/) 0.261.1 - 2025-02-27 -------------------- This release updates the Python version requirement to use ⁠python >= 3.9 instead of ⁠^3.9 to avoid conflicts with other projects that use ⁠>= 3.x Contributed by [John Lyu](https://github.com/PaleNeutron) via [PR #3789](https://github.com/strawberry-graphql/strawberry/pull/3789/) 0.261.0 - 2025-02-27 -------------------- This release adds support for `type[strawberry.UNSET]` in addition to `strawberry.types.unset.UnsetType` for annotations. ```python @strawberry.type class User: name: str | None = UNSET age: int | None | type[strawberry.UNSET] = UNSET ``` Contributed by [Alexey Pelykh](https://github.com/alexey-pelykh) via [PR #3765](https://github.com/strawberry-graphql/strawberry/pull/3765/) 0.260.4 - 2025-02-27 -------------------- This release adds support for Datadog ddtrace v3.0.0 in the `DatadogTracingExtension` Contributed by [Jon Finerty](https://github.com/jonfinerty) via [PR #3794](https://github.com/strawberry-graphql/strawberry/pull/3794/) 0.260.3 - 2025-02-27 -------------------- This release fixes the issue that some subscription resolvers were not canceled if a client unexpectedly disconnected. Contributed by [Jakub Bacic](https://github.com/jakub-bacic) via [PR #3778](https://github.com/strawberry-graphql/strawberry/pull/3778/) 0.260.2 - 2025-02-13 -------------------- This release fixes an issue where directives with input types using snake_case would not be printed in the schema. For example, the following: ```python @strawberry.input class FooInput: hello: str hello_world: str @strawberry.schema_directive(locations=[Location.FIELD_DEFINITION]) class FooDirective: input: FooInput @strawberry.type class Query: @strawberry.field( directives=[ FooDirective(input=FooInput(hello="hello", hello_world="hello world")), ] ) def foo(self, info) -> str: ... ``` Would previously print as: ```graphql directive @fooDirective( input: FooInput! optionalInput: FooInput ) on FIELD_DEFINITION type Query { foo: String! @fooDirective(input: { hello: "hello" }) } input FooInput { hello: String! hello_world: String! } ``` Now it will be correctly printed as: ```graphql directive @fooDirective( input: FooInput! optionalInput: FooInput ) on FIELD_DEFINITION type Query { foo: String! @fooDirective(input: { hello: "hello", helloWorld: "hello world" }) } input FooInput { hello: String! hello_world: String! } ``` Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #3780](https://github.com/strawberry-graphql/strawberry/pull/3780/) 0.260.1 - 2025-02-13 -------------------- This release fixes an issue where extensions were being duplicated when custom directives were added to the schema. Previously, when user directives were present, extensions were being appended twice to the extension list, causing them to be executed multiple times during query processing. The fix ensures that extensions are added only once and maintains their original order. Test cases have been added to validate this behavior and ensure extensions are executed exactly once. Contributed by [DONEY K PAUL](https://github.com/doney-dkp) via [PR #3783](https://github.com/strawberry-graphql/strawberry/pull/3783/) 0.260.0 - 2025-02-12 -------------------- Support aliases (TypeVar passthrough) in `get_specialized_type_var_map`. Contributed by [Alexey Pelykh](https://github.com/alexey-pelykh) via [PR #3766](https://github.com/strawberry-graphql/strawberry/pull/3766/) 0.259.1 - 2025-02-12 -------------------- This release adjusts the `context_getter` attribute from the fastapi `GraphQLRouter` to accept an async callables. Contributed by [Alexey Pelykh](https://github.com/alexey-pelykh) via [PR #3763](https://github.com/strawberry-graphql/strawberry/pull/3763/) 0.259.0 - 2025-02-09 -------------------- This release refactors some of the internal execution logic by: 1. Moving execution logic from separate files into schema.py for better organization 2. Using graphql-core's parse and validate functions directly instead of wrapping them 3. Removing redundant execute.py and subscribe.py files This is an internal refactor that should not affect the public API or functionality. The changes make the codebase simpler and easier to maintain. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3771](https://github.com/strawberry-graphql/strawberry/pull/3771/) 0.258.1 - 2025-02-09 -------------------- This release adjusts the schema printer to avoid printing a schema directive value set to `UNSET` as `""` (empty string). For example, the following: ```python @strawberry.input class FooInput: a: str | None = strawberry.UNSET b: str | None = strawberry.UNSET @strawberry.schema_directive(locations=[Location.FIELD_DEFINITION]) class FooDirective: input: FooInput @strawberry.type class Query: @strawberry.field(directives=[FooDirective(input=FooInput(a="aaa"))]) def foo(self, info) -> str: ... ``` Would previously print as: ```graphql directive @fooDirective( input: FooInput! optionalInput: FooInput ) on FIELD_DEFINITION type Query { foo: String! @fooDirective(input: { a: "aaa", b: "" }) } input FooInput { a: String b: String } ``` Now it will be correctly printed as: ```graphql directive @fooDirective( input: FooInput! optionalInput: FooInput ) on FIELD_DEFINITION type Query { foo: String! @fooDirective(input: { a: "aaa" }) } input FooInput { a: String b: String } ``` Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #3770](https://github.com/strawberry-graphql/strawberry/pull/3770/) 0.258.0 - 2025-01-12 -------------------- Add the ability to override the "max results" a relay's connection can return on a per-field basis. The default value for this is defined in the schema's config, and set to `100` unless modified by the user. Now, that per-field value will take precedence over it. For example: ```python @strawerry.type class Query: # This will still use the default value in the schema's config fruits: ListConnection[Fruit] = relay.connection() # This will reduce the maximum number of results to 10 limited_fruits: ListConnection[Fruit] = relay.connection(max_results=10) # This will increase the maximum number of results to 10 higher_limited_fruits: ListConnection[Fruit] = relay.connection(max_results=10_000) ``` Note that this only affects `ListConnection` and subclasses. If you are implementing your own connection resolver, there's an extra keyword named `max_results: int | None` that will be passed to it. Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #3746](https://github.com/strawberry-graphql/strawberry/pull/3746/) 0.257.0 - 2025-01-09 -------------------- The common `node: Node` used to resolve relay nodes means we will be relying on is_type_of to check if the returned object is in fact a subclass of the Node interface. However, integrations such as Django, SQLAlchemy and Pydantic will not return the type itself, but instead an alike object that is later resolved to the expected type. In case there are more than one possible type defined for that model that is being returned, the first one that replies True to `is_type_of` check would be used in the resolution, meaning that when asking for `"PublicUser:123"`, strawberry could end up returning `"User:123"`, which can lead to security issues (such as data leakage). In here we are introducing a new `strawberry.cast`, which will be used to mark an object with the already known type by us, and when asking for is_type_of that mark will be used to check instead, ensuring we will return the correct type. That `cast` is already in place for the relay node resolution and pydantic. Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #3749](https://github.com/strawberry-graphql/strawberry/pull/3749/) 0.256.1 - 2024-12-23 -------------------- This release updates Strawberry internally to no longer pass keywords arguments to `pathlib.PurePath`. Support for supplying keyword arguments to `pathlib.PurePath` is deprecated and scheduled for removal in Python 3.14 Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) via [PR #3738](https://github.com/strawberry-graphql/strawberry/pull/3738/) 0.256.0 - 2024-12-21 -------------------- This release drops support for Python 3.8, which reached its end-of-life (EOL) in October 2024. The minimum supported Python version is now 3.9. We strongly recommend upgrading to Python 3.9 or a newer version, as older versions are no longer maintained and may contain security vulnerabilities. Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #3730](https://github.com/strawberry-graphql/strawberry/pull/3730/) 0.255.0 - 2024-12-20 -------------------- This release adds support for making Relay connection optional, this is useful when you want to add permission classes to the connection and not fail the whole query if the user doesn't have permission to access the connection. Example: ```python import strawberry from strawberry import relay from strawberry.permission import BasePermission class IsAuthenticated(BasePermission): message = "User is not authenticated" # This method can also be async! def has_permission( self, source: typing.Any, info: strawberry.Info, **kwargs ) -> bool: return False @strawberry.type class Fruit(relay.Node): code: relay.NodeID[int] name: str weight: float @classmethod def resolve_nodes( cls, *, info: strawberry.Info, node_ids: Iterable[str], ): return [] @strawberry.type class Query: node: relay.Node = relay.node() @relay.connection( relay.ListConnection[Fruit] | None, permission_classes=[IsAuthenticated()] ) def fruits(self) -> Iterable[Fruit]: # This can be a database query, a generator, an async generator, etc return all_fruits.values() ``` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3707](https://github.com/strawberry-graphql/strawberry/pull/3707/) 0.254.1 - 2024-12-20 -------------------- This release updates the Context and RootValue vars to have a default value of `None`, this makes it easier to use the views without having to pass in a value for these vars. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3732](https://github.com/strawberry-graphql/strawberry/pull/3732/) 0.254.0 - 2024-12-13 -------------------- This release adds a new `on_ws_connect` method to all HTTP view integrations. The method is called when a `graphql-transport-ws` or `graphql-ws` connection is established and can be used to customize the connection acknowledgment behavior. This is particularly useful for authentication, authorization, and sending a custom acknowledgment payload to clients when a connection is accepted. For example: ```python class MyGraphQLView(GraphQLView): async def on_ws_connect(self, context: Dict[str, object]): connection_params = context["connection_params"] if not isinstance(connection_params, dict): # Reject without a custom graphql-ws error payload raise ConnectionRejectionError() if connection_params.get("password") != "secret: # Reject with a custom graphql-ws error payload raise ConnectionRejectionError({"reason": "Invalid password"}) if username := connection_params.get("username"): # Accept with a custom acknowledgement payload return {"message": f"Hello, {username}!"} # Accept without a acknowledgement payload return await super().on_ws_connect(context) ``` Take a look at our documentation to learn more. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) via [PR #3720](https://github.com/strawberry-graphql/strawberry/pull/3720/) 0.253.1 - 2024-12-03 -------------------- Description: Fixed a bug in the OpenTelemetryExtension class where the _span_holder dictionary was incorrectly shared across all instances. This was caused by defining _span_holder as a class-level attribute with a mutable default value (dict()). Contributed by [Conglei](https://github.com/conglei) via [PR #3716](https://github.com/strawberry-graphql/strawberry/pull/3716/) 0.253.0 - 2024-11-23 -------------------- In this release, the return types of the `get_root_value` and `get_context` methods were updated to be consistent across all view integrations. Before this release, the return types used by the ASGI and Django views were too generic. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) via [PR #3712](https://github.com/strawberry-graphql/strawberry/pull/3712/) 0.252.0 - 2024-11-22 -------------------- The view classes of all integrations now have a `decode_json` method that allows you to customize the decoding of HTTP JSON requests. This is useful if you want to use a different JSON decoder, for example, to optimize performance. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) via [PR #3709](https://github.com/strawberry-graphql/strawberry/pull/3709/) 0.251.0 - 2024-11-21 -------------------- Starting with this release, the same JSON encoder is used to encode HTTP responses and WebSocket messages. This enables developers to override the `encode_json` method on their views to customize the JSON encoder used by all web protocols. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) via [PR #3708](https://github.com/strawberry-graphql/strawberry/pull/3708/) 0.250.1 - 2024-11-19 -------------------- This release refactors part of the legacy `graphql-ws` protocol implementation, making it easier to read, maintain, and extend. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) via [PR #3704](https://github.com/strawberry-graphql/strawberry/pull/3704/) 0.250.0 - 2024-11-18 -------------------- In this release, we migrated the `graphql-transport-ws` types from data classes to typed dicts. Using typed dicts enabled us to precisely model `null` versus `undefined` values, which are common in that protocol. As a result, we could remove custom conversion methods handling these cases and simplify the codebase. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) via [PR #3701](https://github.com/strawberry-graphql/strawberry/pull/3701/) 0.249.0 - 2024-11-18 -------------------- After a year-long deprecation period, the `SentryTracingExtension` has been removed in favor of the official Sentry SDK integration. To migrate, remove the `SentryTracingExtension` from your Strawberry schema and then follow the [official Sentry SDK integration guide](https://docs.sentry.io/platforms/python/integrations/strawberry/). Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) via [PR #3672](https://github.com/strawberry-graphql/strawberry/pull/3672/) 0.248.1 - 2024-11-08 -------------------- This release fixes the following deprecation warning: ``` Failing to pass a value to the 'type_params' parameter of 'typing._eval_type' is deprecated, as it leads to incorrect behaviour when calling typing._eval_type on a stringified annotation that references a PEP 695 type parameter. It will be disallowed in Python 3.15. ``` This was only trigger in Python 3.13 and above. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3692](https://github.com/strawberry-graphql/strawberry/pull/3692/) 0.248.0 - 2024-11-07 -------------------- In this release, all types of the legacy graphql-ws protocol were refactored. The types are now much stricter and precisely model the difference between null and undefined fields. As a result, our protocol implementation and related tests are now more robust and easier to maintain. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) via [PR #3689](https://github.com/strawberry-graphql/strawberry/pull/3689/) 0.247.2 - 2024-11-05 -------------------- This release fixes the issue that some coroutines in the WebSocket protocol handlers were never awaited if clients disconnected shortly after starting an operation. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) via [PR #3687](https://github.com/strawberry-graphql/strawberry/pull/3687/) 0.247.1 - 2024-11-01 -------------------- Starting with this release, both websocket-based protocols will handle unexpected socket disconnections more gracefully. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) via [PR #3685](https://github.com/strawberry-graphql/strawberry/pull/3685/) 0.247.0 - 2024-10-21 -------------------- This release fixes a regression in the legacy GraphQL over WebSocket protocol. Legacy protocol implementations should ignore client message parsing errors. During a recent refactor, Strawberry changed this behavior to match the new protocol, where parsing errors must close the WebSocket connection. The expected behavior is restored and adequately tested in this release. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) via [PR #3670](https://github.com/strawberry-graphql/strawberry/pull/3670/) 0.246.3 - 2024-10-21 -------------------- This release addresses a bug where directives were being added multiple times when defined in an interface which multiple objects inherits from. The fix involves deduplicating directives when applying extensions/permissions to a field, ensuring that each directive is only added once. Contributed by [Arthur](https://github.com/Speedy1991) via [PR #3674](https://github.com/strawberry-graphql/strawberry/pull/3674/) 0.246.2 - 2024-10-12 -------------------- This release tweaks the Flask integration's `render_graphql_ide` method to be stricter typed internally, making type checkers ever so slightly happier. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) via [PR #3666](https://github.com/strawberry-graphql/strawberry/pull/3666/) 0.246.1 - 2024-10-09 -------------------- This release adds support for using raw Python enum types in your schema (enums that are not decorated with `@strawberry.enum`) This is useful if you have enum types from other places in your code that you want to use in strawberry. i.e ```py # somewhere.py from enum import Enum class AnimalKind(Enum): AXOLOTL, CAPYBARA = range(2) # gql/animals from somewhere import AnimalKind @strawberry.type class AnimalType: kind: AnimalKind ``` Contributed by [ניר](https://github.com/nrbnlulu) via [PR #3639](https://github.com/strawberry-graphql/strawberry/pull/3639/) 0.246.0 - 2024-10-07 -------------------- The AIOHTTP, ASGI, and Django test clients' `asserts_errors` option has been renamed to `assert_no_errors` to better reflect its purpose. This change is backwards-compatible, but the old option name will raise a deprecation warning. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) via [PR #3661](https://github.com/strawberry-graphql/strawberry/pull/3661/) 0.245.0 - 2024-10-07 -------------------- This release removes the dated `subscriptions_enabled` setting from the Django and Channels integrations. Instead, WebSocket support is now enabled by default in all GraphQL IDEs. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) via [PR #3660](https://github.com/strawberry-graphql/strawberry/pull/3660/) 0.244.1 - 2024-10-06 -------------------- Fixes an issue where the codegen tool would crash when working with a nullable list of types. Contributed by [Jacob Allen](https://github.com/enoua5) via [PR #3653](https://github.com/strawberry-graphql/strawberry/pull/3653/) 0.244.0 - 2024-10-05 -------------------- Starting with this release, WebSocket logic now lives in the base class shared between all HTTP integrations. This makes the behaviour of WebSockets much more consistent between integrations and easier to maintain. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) via [PR #3638](https://github.com/strawberry-graphql/strawberry/pull/3638/) 0.243.1 - 2024-09-26 -------------------- This releases adds support for Pydantic 2.9.0's Mypy plugin Contributed by [Krisque](https://github.com/chrisemke) via [PR #3632](https://github.com/strawberry-graphql/strawberry/pull/3632/) 0.243.0 - 2024-09-25 -------------------- Starting with this release, multipart uploads are disabled by default and Strawberry Django view is no longer implicitly exempted from Django's CSRF protection. Both changes relieve users from implicit security implications inherited from the GraphQL multipart request specification which was enabled in Strawberry by default. These are breaking changes if you are using multipart uploads OR the Strawberry Django view. Migrations guides including further information are available on the Strawberry website. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) via [PR #3645](https://github.com/strawberry-graphql/strawberry/pull/3645/) 0.242.0 - 2024-09-19 -------------------- Starting with this release, clients using the legacy graphql-ws subprotocol will receive an error when they try to send binary data frames. Before, binary data frames were silently ignored. While vaguely defined in the protocol, the legacy graphql-ws subprotocol is generally understood to only support text data frames. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) via [PR #3633](https://github.com/strawberry-graphql/strawberry/pull/3633/) 0.241.0 - 2024-09-16 -------------------- You can now configure your schemas to provide a custom subclass of `strawberry.types.Info` to your types and queries. ```py import strawberry from strawberry.schema.config import StrawberryConfig from .models import ProductModel class CustomInfo(strawberry.Info): @property def selected_group_id(self) -> int | None: """Get the ID of the group you're logged in as.""" return self.context["request"].headers.get("Group-ID") @strawberry.type class Group: id: strawberry.ID name: str @strawberry.type class User: id: strawberry.ID name: str group: Group @strawberry.type class Query: @strawberry.field def user(self, id: strawberry.ID, info: CustomInfo) -> Product: kwargs = {"id": id, "name": ...} if info.selected_group_id is not None: # Get information about the group you're a part of, if # available. kwargs["group"] = ... return User(**kwargs) schema = strawberry.Schema( Query, config=StrawberryConfig(info_class=CustomInfo), ) ``` Contributed by [Ethan Henderson](https://github.com/parafoxia) via [PR #3592](https://github.com/strawberry-graphql/strawberry/pull/3592/) 0.240.4 - 2024-09-13 -------------------- This release fixes how we check for multipart subscriptions to be in line with the latest changes in the spec. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3627](https://github.com/strawberry-graphql/strawberry/pull/3627/) 0.240.3 - 2024-09-12 -------------------- This release fixes an issue that prevented extensions to receive the result from the execution context when executing operations in async. Contributed by [ניר](https://github.com/nrbnlulu) via [PR #3629](https://github.com/strawberry-graphql/strawberry/pull/3629/) 0.240.2 - 2024-09-11 -------------------- This release updates how we check for GraphQL core's version to remove a dependency on the `packaging` package. Contributed by [Nicholas Bollweg](https://github.com/bollwyvl) via [PR #3622](https://github.com/strawberry-graphql/strawberry/pull/3622/) 0.240.1 - 2024-09-11 -------------------- This release adds support for Python 3.13 (which will be out soon!) Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3510](https://github.com/strawberry-graphql/strawberry/pull/3510/) 0.240.0 - 2024-09-10 -------------------- This release adds support for schema-extensions in subscriptions. Here's a small example of how to use them (they work the same way as query and mutation extensions): ```python import asyncio from typing import AsyncIterator import strawberry from strawberry.extensions.base_extension import SchemaExtension @strawberry.type class Subscription: @strawberry.subscription async def notifications(self, info: strawberry.Info) -> AsyncIterator[str]: for _ in range(3): yield "Hello" class MyExtension(SchemaExtension): async def on_operation(self): # This would run when the subscription starts print("Subscription started") yield # The subscription has ended print("Subscription ended") schema = strawberry.Schema( query=Query, subscription=Subscription, extensions=[MyExtension] ) ``` Contributed by [ניר](https://github.com/nrbnlulu) via [PR #3554](https://github.com/strawberry-graphql/strawberry/pull/3554/) 0.239.2 - 2024-09-03 -------------------- This release fixes a TypeError on Python 3.8 due to us using a `asyncio.Queue[Tuple[bool, Any]](1)` instead of `asyncio.Queue(1)`. Contributed by [Daniel Szoke](https://github.com/szokeasaurusrex) via [PR #3615](https://github.com/strawberry-graphql/strawberry/pull/3615/) 0.239.1 - 2024-09-02 -------------------- This release fixes an issue with the http multipart subscription where the status code would be returned as `None`, instead of 200. We also took the opportunity to update the internals to better support additional protocols in future. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3610](https://github.com/strawberry-graphql/strawberry/pull/3610/) 0.239.0 - 2024-08-31 -------------------- This release adds support for multipart subscriptions in almost all[^1] of our http integrations! [Multipart subcriptions](https://www.apollographql.com/docs/router/executing-operations/subscription-multipart-protocol/) are a new protocol from Apollo GraphQL, built on the [Incremental Delivery over HTTP spec](https://github.com/graphql/graphql-over-http/blob/main/rfcs/IncrementalDelivery.md), which is also used for `@defer` and `@stream`. The main advantage of this protocol is that when using the Apollo Client libraries you don't need to install any additional dependency, but in future this feature should make it easier for us to implement `@defer` and `@stream` Also, this means that you don't need to use Django Channels for subscription, since this protocol is based on HTTP we don't need to use websockets. [^1]: Flask, Chalice and the sync Django integration don't support this. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3076](https://github.com/strawberry-graphql/strawberry/pull/3076/) 0.238.1 - 2024-08-30 -------------------- Fix an issue where `StrawberryResolver.is_async` was returning `False` for a function decorated with asgiref's `@sync_to_async`. The root cause is that in python >= 3.12 coroutine functions are market using `inspect.markcoroutinefunction`, which should be checked with `inspect.iscoroutinefunction` instead of `asyncio.iscoroutinefunction` Contributed by [Hyun S. Moon](https://github.com/shmoon-kr) via [PR #3599](https://github.com/strawberry-graphql/strawberry/pull/3599/) 0.238.0 - 2024-08-30 -------------------- This release removes the integration of Starlite, as it has been deprecated since 11 May 2024. If you are using Starlite, please consider migrating to Litestar (https://litestar.dev) or another alternative. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3609](https://github.com/strawberry-graphql/strawberry/pull/3609/) 0.237.3 - 2024-07-31 -------------------- This release fixes the type of the ASGI request handler's `scope` argument, making type checkers ever so slightly happier. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) via [PR #3581](https://github.com/strawberry-graphql/strawberry/pull/3581/) 0.237.2 - 2024-07-26 -------------------- This release makes the ASGI and FastAPI integrations share their HTTP request adapter code, making Strawberry ever so slightly smaller and easier to maintain. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) via [PR #3582](https://github.com/strawberry-graphql/strawberry/pull/3582/) 0.237.1 - 2024-07-24 -------------------- This release adds support for GraphQL-core v3.3 (which has not yet been released). Note that we continue to support GraphQL-core v3.2 as well. Contributed by [ניר](https://github.com/nrbnlulu) via [PR #3570](https://github.com/strawberry-graphql/strawberry/pull/3570/) 0.237.0 - 2024-07-24 -------------------- This release ensures using pydantic 2.8.0 doesn't break when using experimental pydantic_type and running mypy. Contributed by [Martin Roy](https://github.com/lindycoder) via [PR #3562](https://github.com/strawberry-graphql/strawberry/pull/3562/) 0.236.2 - 2024-07-23 -------------------- Update federation entity resolver exception handling to set the result to the original error instead of a `GraphQLError`, which obscured the original message and meta-fields. Contributed by [Bradley Oesch](https://github.com/bradleyoesch) via [PR #3144](https://github.com/strawberry-graphql/strawberry/pull/3144/) 0.236.1 - 2024-07-23 -------------------- This release fixes an issue where optional lazy types using `| None` were failing to be correctly resolved inside modules using future annotations, e.g. ```python from __future__ import annotations from typing import Annotated, TYPE_CHECKING import strawberry if TYPE_CHECKING: from types import Group @strawberry.type class Person: group: Annotated["Group", strawberry.lazy("types.group")] | None ``` This should now work as expected. Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #3576](https://github.com/strawberry-graphql/strawberry/pull/3576/) 0.236.0 - 2024-07-17 -------------------- This release changes some of the internals of Strawberry, it shouldn't be affecting most of the users, but since we have changed the structure of the code you might need to update your imports. Thankfully we also provide a codemod for this, you can run it with: ```bash strawberry upgrade update-imports ``` This release also includes additional documentation to some of the classes, methods and functions, this is in preparation for having the API reference in the documentation ✨ Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3546](https://github.com/strawberry-graphql/strawberry/pull/3546/) 0.235.2 - 2024-07-08 -------------------- This release removes an unnecessary check from our internal GET query parsing logic making it simpler and (insignificantly) faster. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) via [PR #3558](https://github.com/strawberry-graphql/strawberry/pull/3558/) 0.235.1 - 2024-06-26 -------------------- This release improves the performance when returning a lot of data, especially when using generic inputs (where we got a 7x speedup in our benchmark!). Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3549](https://github.com/strawberry-graphql/strawberry/pull/3549/) 0.235.0 - 2024-06-10 -------------------- This release adds a new configuration to disable field suggestions in the error response. ```python @strawberry.type class Query: name: str schema = strawberry.Schema( query=Query, config=StrawberryConfig(disable_field_suggestions=True) ) ``` Trying to query `{ nam }` will not suggest to query `name` instead. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3537](https://github.com/strawberry-graphql/strawberry/pull/3537/) 0.234.3 - 2024-06-10 -------------------- Fixes a bug where pydantic models as the default value for an input did not print the proper schema. See [this issue](https://github.com/strawberry-graphql/strawberry/issues/3285). Contributed by [ppease](https://github.com/ppease) via [PR #3499](https://github.com/strawberry-graphql/strawberry/pull/3499/) 0.234.2 - 2024-06-07 -------------------- This release fixes an issue when trying to retrieve specialized type vars from a generic type that has been aliased to a name, in cases like: ```python @strawberry.type class Fruit(Generic[T]): ... SpecializedFruit = Fruit[str] ``` Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #3535](https://github.com/strawberry-graphql/strawberry/pull/3535/) 0.234.1 - 2024-06-06 -------------------- Improved error message when supplying GlobalID with invalid or unknown type name component Contributed by [Take Weiland](https://github.com/diesieben07) via [PR #3533](https://github.com/strawberry-graphql/strawberry/pull/3533/) 0.234.0 - 2024-06-01 -------------------- This release separates the `relay.ListConnection` logic that calculates the slice of the nodes into a separate function. This allows for easier reuse of that logic for other places/libraries. The new function lives in the `strawberry.relay.utils` and can be used by calling `SliceMetadata.from_arguments`. This has no implications to end users. Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #3530](https://github.com/strawberry-graphql/strawberry/pull/3530/) 0.233.3 - 2024-05-31 -------------------- This release fixes a typing issue where trying to type a `root` argument with `strawberry.Parent` would fail, like in the following example: ```python import strawberry @strawberry.type class SomeType: @strawberry.field def hello(self, root: strawberry.Parent[str]) -> str: return "world" ``` This should now work as intended. Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #3529](https://github.com/strawberry-graphql/strawberry/pull/3529/) 0.233.2 - 2024-05-31 -------------------- This release fixes an introspection issue when requesting `isOneOf` on built-in scalars, like `String`. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3528](https://github.com/strawberry-graphql/strawberry/pull/3528/) 0.233.1 - 2024-05-30 -------------------- This release exposes `get_arguments` in the schema_converter module to allow integrations, such as strawberry-django, to reuse that functionality if needed. This is an internal change with no impact for end users. Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #3527](https://github.com/strawberry-graphql/strawberry/pull/3527/) 0.233.0 - 2024-05-29 -------------------- This release refactors our Federation integration to create types using Strawberry directly, instead of using low level types from GraphQL-core. The only user facing change is that now the `info` object passed to the `resolve_reference` function is the `strawberry.Info` object instead of the one coming coming from GraphQL-core. This is a **breaking change** for users that were using the `info` object directly. If you need to access the original `info` object you can do so by accessing the `_raw_info` attribute. ```python import strawberry @strawberry.federation.type(keys=["upc"]) class Product: upc: str @classmethod def resolve_reference(cls, info: strawberry.Info, upc: str) -> "Product": # Access the original info object original_info = info._raw_info return Product(upc=upc) ``` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3525](https://github.com/strawberry-graphql/strawberry/pull/3525/) 0.232.2 - 2024-05-28 -------------------- This release fixes an issue that would prevent using lazy aliased connections to annotate a connection field. For example, this should now work correctly: ```python # types.py @strawberry.type class Fruit: ... FruitConnection: TypeAlias = ListConnection[Fruit] ``` ```python # schema.py @strawberry.type class Query: fruits: Annotated["FruitConnection", strawberry.lazy("types")] = ( strawberry.connection() ) ``` Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #3524](https://github.com/strawberry-graphql/strawberry/pull/3524/) 0.232.1 - 2024-05-27 -------------------- This release fixes an issue where mypy would complain when using a typed async resolver with `strawberry.field(resolver=...)`. Now the code will type check correctly. We also updated our test suite to make we catch similar issues in the future. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3516](https://github.com/strawberry-graphql/strawberry/pull/3516/) 0.232.0 - 2024-05-25 -------------------- This release improves type checking for async resolver functions when used as `strawberry.field(resolver=resolver_func)`. Now doing this will raise a type error: ```python import strawberry def some_resolver() -> int: return 0 @strawberry.type class User: # Note the field being typed as str instead of int name: str = strawberry.field(resolver=some_resolver) ``` Contributed by [Bryan Ricker](https://github.com/bricker) via [PR #3241](https://github.com/strawberry-graphql/strawberry/pull/3241/) 0.231.1 - 2024-05-25 -------------------- Fixes an issue where lazy annotations raised an error when used together with a List Contributed by [jeich](https://github.com/jeich) via [PR #3388](https://github.com/strawberry-graphql/strawberry/pull/3388/) 0.231.0 - 2024-05-25 -------------------- When calling the CLI without all the necessary dependencies installed, a `MissingOptionalDependenciesError` will be raised instead of a `ModuleNotFoundError`. This new exception will provide a more helpful hint regarding how to fix the problem. Contributed by [Ethan Henderson](https://github.com/parafoxia) via [PR #3511](https://github.com/strawberry-graphql/strawberry/pull/3511/) 0.230.0 - 2024-05-22 -------------------- This release adds support for `@oneOf` on input types! 🎉 You can use `one_of=True` on input types to create an input type that should only have one of the fields set. ```python import strawberry @strawberry.input(one_of=True) class ExampleInputTagged: a: str | None = strawberry.UNSET b: int | None = strawberry.UNSET ``` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3429](https://github.com/strawberry-graphql/strawberry/pull/3429/) 0.229.2 - 2024-05-22 -------------------- This release fixes an issue when using `Annotated` + `strawberry.lazy` + deferred annotations such as: ```python from __future__ import annotations import strawberry from typing import Annotated @strawberry.type class Query: a: Annotated["datetime", strawberry.lazy("datetime")] schema = strawberry.Schema(Query) ``` Before this would only work if `datetime` was not inside quotes. Now it should work as expected! Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #3507](https://github.com/strawberry-graphql/strawberry/pull/3507/) 0.229.1 - 2024-05-15 -------------------- This release fixes a regression from 0.229.0 where using a generic interface inside a union would return an error. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3502](https://github.com/strawberry-graphql/strawberry/pull/3502/) 0.229.0 - 2024-05-12 -------------------- This release improves our support for generic types, now using the same the same generic multiple times with a list inside an interface or union is supported, for example the following will work: ```python import strawberry @strawberry.type class BlockRow[T]: items: list[T] @strawberry.type class Query: @strawberry.field def blocks(self) -> list[BlockRow[str] | BlockRow[int]]: return [ BlockRow(items=["a", "b", "c"]), BlockRow(items=[1, 2, 3, 4]), ] schema = strawberry.Schema(query=Query) ``` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3463](https://github.com/strawberry-graphql/strawberry/pull/3463/) 0.228.0 - 2024-05-12 -------------------- This releases updates the JSON scalar definition to have the updated `specifiedBy` URL. The release is marked as minor because it will change the generated schema if you're using the JSON scalar. Contributed by [Egor](https://github.com/Birdi7) via [PR #3478](https://github.com/strawberry-graphql/strawberry/pull/3478/) 0.227.7 - 2024-05-12 -------------------- This releases updates the `field-extensions` documentation's `StrawberryField` stability warning to include stable features. The release is marked as patch because it only changes documentation. Contributed by [Ray Sy](https://github.com/fireteam99) via [PR #3496](https://github.com/strawberry-graphql/strawberry/pull/3496/) 0.227.6 - 2024-05-11 -------------------- Fix `AssertionError` caused by the `DatadogTracingExtension` whenever the query is unavailable. The bug in question was reported by issue [#3150](https://github.com/strawberry-graphql/strawberry/issues/3150). The datadog extension would throw an `AssertionError` whenever there was no query available. This could happen if, for example, a user POSTed something to `/graphql` with a JSON that doesn't contain a `query` field as per the GraphQL spec. The fix consists of adding `query_missing` to the `operation_type` tag, and also adding `query_missing` to the resource name. It also makes it easier to look for logs of users making invalid queries by searching for `query_missing` in Datadog. Contributed by [Lucas Valente](https://github.com/serramatutu) via [PR #3483](https://github.com/strawberry-graphql/strawberry/pull/3483/) 0.227.5 - 2024-05-11 -------------------- **Deprecations:** This release deprecates the `Starlite` integration in favour of the `LiteStar` integration. Refer to the [LiteStar](./litestar.md) integration for more information. LiteStar is a [renamed](https://litestar.dev/about/organization.html#litestar-and-starlite) and upgraded version of Starlite. Before: ```python from strawberry.starlite import make_graphql_controller ``` After: ```python from strawberry.litestar import make_graphql_controller ``` Contributed by [Egor](https://github.com/Birdi7) via [PR #3492](https://github.com/strawberry-graphql/strawberry/pull/3492/) 0.227.4 - 2024-05-09 -------------------- This release fixes a bug in release 0.227.3 where FragmentSpread nodes were not resolving edges. Contributed by [Eric Uriostigue](https://github.com/euriostigue) via [PR #3487](https://github.com/strawberry-graphql/strawberry/pull/3487/) 0.227.3 - 2024-05-01 -------------------- This release adds an optimization to `ListConnection` such that only queries with `edges` or `pageInfo` in their selected fields triggers `resolve_edges`. This change is particularly useful for the `strawberry-django` extension's `ListConnectionWithTotalCount` and the only selected field is `totalCount`. An extraneous SQL query is prevented with this optimization. Contributed by [Eric Uriostigue](https://github.com/euriostigue) via [PR #3480](https://github.com/strawberry-graphql/strawberry/pull/3480/) 0.227.2 - 2024-04-21 -------------------- This release fixes a minor issue where the docstring for the relay util `to_base64` described the return type incorrectly. Contributed by [Gavin Bannerman](https://github.com/gbannerman) via [PR #3467](https://github.com/strawberry-graphql/strawberry/pull/3467/) 0.227.1 - 2024-04-20 -------------------- This release fixes an issue where annotations on `@strawberry.type`s were overridden by our code. With release all annotations should be preserved. This is useful for libraries that use annotations to introspect Strawberry types. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3003](https://github.com/strawberry-graphql/strawberry/pull/3003/) 0.227.0 - 2024-04-19 -------------------- This release improves the schema codegen, making it more robust and easier to use. It does this by introducing a directed acyclic graph for the schema codegen, which should reduce the amount of edits needed to make the generated code work, since it will be able to generate the code in the correct order (based on the dependencies of each type). Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3116](https://github.com/strawberry-graphql/strawberry/pull/3116/) 0.226.2 - 2024-04-19 -------------------- This release updates our Mypy plugin to add support for Pydantic >= 2.7.0 Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3462](https://github.com/strawberry-graphql/strawberry/pull/3462/) 0.226.1 - 2024-04-19 -------------------- This releases fixes a bug in the mypy plugin where the `from_pydantic` method was not correctly typed. Contributed by [Corentin-Br](https://github.com/Corentin-Br) via [PR #3368](https://github.com/strawberry-graphql/strawberry/pull/3368/) 0.226.0 - 2024-04-17 -------------------- Starting with this release, any error raised from within schema extensions will abort the operation and is returned to the client. This corresponds to the way we already handle field extension errors and resolver errors. This is particular useful for schema extensions performing checks early in the request lifecycle, for example: ```python class MaxQueryLengthExtension(SchemaExtension): MAX_QUERY_LENGTH = 8192 async def on_operation(self): if len(self.execution_context.query) > self.MAX_QUERY_LENGTH: raise StrawberryGraphQLError(message="Query too large") yield ``` Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) via [PR #3217](https://github.com/strawberry-graphql/strawberry/pull/3217/) 0.225.1 - 2024-04-15 -------------------- This change fixes GET request queries returning a 400 if a content_type header is supplied Contributed by [Nathan John](https://github.com/vethan) via [PR #3452](https://github.com/strawberry-graphql/strawberry/pull/3452/) 0.225.0 - 2024-04-14 -------------------- This release adds support for using FastAPI APIRouter arguments in GraphQLRouter. Now you have the opportunity to specify parameters such as `tags`, `route_class`, `deprecated`, `include_in_schema`, etc: ```python import strawberry from fastapi import FastAPI from strawberry.fastapi import GraphQLRouter @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "Hello World" schema = strawberry.Schema(Query) graphql_app = GraphQLRouter(schema, tags=["graphql"]) app = FastAPI() app.include_router(graphql_app, prefix="/graphql") ``` Contributed by [Nikita Paramonov](https://github.com/nparamonov) via [PR #3442](https://github.com/strawberry-graphql/strawberry/pull/3442/) 0.224.2 - 2024-04-13 -------------------- This releases fixes a bug where schema extensions where not running a LIFO order. Contributed by [ניר](https://github.com/nrbnlulu) via [PR #3416](https://github.com/strawberry-graphql/strawberry/pull/3416/) 0.224.1 - 2024-03-30 -------------------- This release fixes a deprecation warning when using the Apollo Tracing Extension. Contributed by [A. Coady](https://github.com/coady) via [PR #3410](https://github.com/strawberry-graphql/strawberry/pull/3410/) 0.224.0 - 2024-03-30 -------------------- This release adds support for using both Pydantic v1 and v2, when importing from `pydantic.v1`. This is automatically detected and the correct version is used. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3426](https://github.com/strawberry-graphql/strawberry/pull/3426/) 0.223.0 - 2024-03-29 -------------------- This release adds support for Apollo Federation in the schema codegen. Now you can convert a schema like this: ```graphql extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key", "@shareable"]) type Query { me: User } type User @key(fields: "id") { id: ID! username: String! @shareable } ``` to a Strawberry powered schema like this: ```python import strawberry @strawberry.type class Query: me: User | None @strawberry.federation.type(keys=["id"]) class User: id: strawberry.ID username: str = strawberry.federation.field(shareable=True) schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) ``` By running the following command: ```bash strawberry schema-codegen example.graphql ``` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3417](https://github.com/strawberry-graphql/strawberry/pull/3417/) 0.222.0 - 2024-03-27 -------------------- This release adds support for Apollo Federation v2.7 which includes the `@authenticated`, `@requiresScopes`, `@policy` directives, as well as the `label` argument for `@override`. As usual, we have first class support for them in the `strawberry.federation` namespace, here's an example: ```python from strawberry.federation.schema_directives import Override @strawberry.federation.type( authenticated=True, policy=[["client", "poweruser"], ["admin"]], requires_scopes=[["client", "poweruser"], ["admin"]], ) class Product: upc: str = strawberry.federation.field( override=Override(override_from="mySubGraph", label="percent(1)") ) ``` Contributed by [Tyger Taco](https://github.com/TygerTaco) via [PR #3420](https://github.com/strawberry-graphql/strawberry/pull/3420/) 0.221.1 - 2024-03-21 -------------------- This release properly allows passing one argument to the `Info` class. This is now fully supported: ```python import strawberry from typing import TypedDict class Context(TypedDict): user_id: str @strawberry.type class Query: @strawberry.field def info(self, info: strawberry.Info[Context]) -> str: return info.context["user_id"] ``` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3419](https://github.com/strawberry-graphql/strawberry/pull/3419/) 0.221.0 - 2024-03-21 -------------------- This release improves the `Info` type, by adding support for default TypeVars and by exporting it from the main module. This makes it easier to use `Info` in your own code, without having to import it from `strawberry.types.info`. ### New export By exporting `Info` from the main module, now you can do the follwing: ```python import strawberry @strawberry.type class Query: @strawberry.field def info(self, info: strawberry.Info) -> str: # do something with info return "hello" ``` ### Default TypeVars The `Info` type now has default TypeVars, so you can use it without having to specify the type arguments, like we did in the example above. Make sure to use the latest version of Mypy or Pyright for this. It also means that you can only pass one value to it if you only care about the context type: ```python import strawberry from .context import Context @strawberry.type class Query: @strawberry.field def info(self, info: strawberry.Info[Context]) -> str: return info.context.user_id ``` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3418](https://github.com/strawberry-graphql/strawberry/pull/3418/) 0.220.0 - 2024-03-08 -------------------- This release adds support to allow passing `connection_params` as dictionary to `GraphQLWebsocketCommunicator` class when testing [channels integration](https://strawberry.rocks/docs/integrations/channels#testing) ### Example ```python GraphQLWebsocketCommunicator( application=application, path="/graphql", connection_params={"username": "strawberry"}, ) ``` Contributed by [selvarajrajkanna](https://github.com/selvarajrajkanna) via [PR #3403](https://github.com/strawberry-graphql/strawberry/pull/3403/) 0.219.2 - 2024-02-06 -------------------- This releases updates the dependency of `python-multipart` to be at least `0.0.7` (which includes a security fix). It also removes the upper bound for `python-multipart` so you can always install the latest version (if compatible) 😊 Contributed by [Srikanth](https://github.com/XChikuX) via [PR #3375](https://github.com/strawberry-graphql/strawberry/pull/3375/) 0.219.1 - 2024-01-28 -------------------- - Improved error message when supplying in incorrect before or after argument with using relay and pagination. - Add extra PR requirement in README.md Contributed by [SD](https://github.com/sdobbelaere) via [PR #3361](https://github.com/strawberry-graphql/strawberry/pull/3361/) 0.219.0 - 2024-01-24 -------------------- This release adds support for [litestar](https://litestar.dev/). ```python import strawberry from litestar import Request, Litestar from strawberry.litestar import make_graphql_controller from strawberry.types.info import Info def custom_context_getter(request: Request): return {"custom": "context"} @strawberry.type class Query: @strawberry.field def hello(self, info: strawberry.Info[object, None]) -> str: return info.context["custom"] schema = strawberry.Schema(Query) GraphQLController = make_graphql_controller( schema, path="/graphql", context_getter=custom_context_getter, ) app = Litestar( route_handlers=[GraphQLController], ) ``` Contributed by [Matthieu MN](https://github.com/gazorby) via [PR #3213](https://github.com/strawberry-graphql/strawberry/pull/3213/) 0.218.1 - 2024-01-23 -------------------- This release fixes a small issue in the GraphQL Transport websocket where the connection would fail when receiving extra parameters in the payload sent from the client. This would happen when using Apollo Sandbox. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3356](https://github.com/strawberry-graphql/strawberry/pull/3356/) 0.218.0 - 2024-01-22 -------------------- This release adds a new method `get_fields` on the `Schema` class. You can use `get_fields` to hide certain field based on some conditions, for example: ```python @strawberry.type class User: name: str email: str = strawberry.field(metadata={"tags": ["internal"]}) @strawberry.type class Query: user: User def public_field_filter(field: StrawberryField) -> bool: return "internal" not in field.metadata.get("tags", []) class PublicSchema(strawberry.Schema): def get_fields( self, type_definition: StrawberryObjectDefinition ) -> List[StrawberryField]: return list(filter(public_field_filter, type_definition.fields)) schema = PublicSchema(query=Query) ``` The schema here would only have the `name` field on the `User` type. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3274](https://github.com/strawberry-graphql/strawberry/pull/3274/) 0.217.1 - 2024-01-04 -------------------- This hotfix enables permission extensions to be used with AsyncGenerators. Contributed by [Erik Wrede](https://github.com/erikwrede) via [PR #3318](https://github.com/strawberry-graphql/strawberry/pull/3318/) 0.217.0 - 2023-12-18 -------------------- Permissions classes now use a `FieldExtension`. The new preferred way to add permissions is to use the `PermissionsExtension` class: ```python import strawberry from strawberry.permission import PermissionExtension, BasePermission class IsAuthorized(BasePermission): message = "User is not authorized" error_extensions = {"code": "UNAUTHORIZED"} def has_permission(self, source, info, **kwargs) -> bool: return False @strawberry.type class Query: @strawberry.field(extensions=[PermissionExtension(permissions=[IsAuthorized()])]) def name(self) -> str: return "ABC" ``` The old way of adding permissions using `permission_classes` is still supported via the automatic addition of a `PermissionExtension` on the field. ### ⚠️ Breaking changes Previously the `kwargs` argument keys for the `has_permission` method were using camel casing (depending on your schema configuration), now they will always follow the python name defined in your resolvers. ```python class IsAuthorized(BasePermission): message = "User is not authorized" def has_permission( self, source, info, **kwargs: typing.Any ) -> bool: # pragma: no cover # kwargs will have a key called "a_key" # instead of `aKey` return False @strawberry.type class Query: @strawberry.field(permission_classes=[IsAuthorized]) def name(self, a_key: str) -> str: # pragma: no cover return "Erik" ``` Using the new `PermissionExtension` API, permissions support even more features: #### Silent errors To return `None` or `[]` instead of raising an error, the `fail_silently ` keyword argument on `PermissionExtension` can be set to `True`. #### Custom Error Extensions & classes Permissions will now automatically add pre-defined error extensions to the error, and can use a custom `GraphQLError` class. This can be configured by modifying the `error_class` and `error_extensions` attributes on the `BasePermission` class. #### Customizable Error Handling To customize the error handling, the `on_unauthorized` method on the `BasePermission` class can be used. Further changes can be implemented by subclassing the `PermissionExtension` class. #### Schema Directives Permissions will automatically be added as schema directives to the schema. This behavior can be altered by setting the `add_directives` to `False` on `PermissionExtension`, or by setting the `_schema_directive` class attribute of the permission to a custom directive. Contributed by [Erik Wrede](https://github.com/erikwrede) via [PR #2570](https://github.com/strawberry-graphql/strawberry/pull/2570/) 0.216.1 - 2023-12-12 -------------------- Don't require `NodeId` annotation if resolve_id is overwritten on `Node` implemented types Contributed by [Alexander](https://github.com/devkral) via [PR #2844](https://github.com/strawberry-graphql/strawberry/pull/2844/) 0.216.0 - 2023-12-06 -------------------- Override encode_json() method in Django BaseView to use DjangoJSONEncoder Contributed by [Noam Stolero](https://github.com/noamsto) via [PR #3273](https://github.com/strawberry-graphql/strawberry/pull/3273/) 0.215.3 - 2023-12-06 -------------------- Fixed the base view so it uses `parse_json` when loading parameters from the query string instead of `json.loads`. Contributed by [Elias Gabriel](https://github.com/thearchitector) via [PR #3272](https://github.com/strawberry-graphql/strawberry/pull/3272/) 0.215.2 - 2023-12-05 -------------------- This release updates the Apollo Sandbox integration to all you to pass cookies to the GraphQL endpoint by enabling the **Include cookes** option in the Sandbox settings. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3278](https://github.com/strawberry-graphql/strawberry/pull/3278/) 0.215.1 - 2023-11-20 -------------------- Improved error message when supplying GlobalID format that relates to another type than the query itself. Contributed by [SD](https://github.com/sdobbelaere) via [PR #3194](https://github.com/strawberry-graphql/strawberry/pull/3194/) 0.215.0 - 2023-11-19 -------------------- Adds an optional `extensions` parameter to `strawberry.federation.field`, with default value `None`. The key is passed through to `strawberry.field`, so the functionality is exactly as described [here](https://strawberry.rocks/docs/guides/field-extensions). Example: ```python strawberry.federation.field(extensions=[InputMutationExtension()]) ``` Contributed by [Bryan Ricker](https://github.com/bricker) via [PR #3239](https://github.com/strawberry-graphql/strawberry/pull/3239/) 0.214.0 - 2023-11-15 -------------------- This release updates the GraphiQL packages to their latest versions: - `graphiql@3.0.9` - `@graphiql/plugin-explorer@1.0.2` Contributed by [Rodrigo Feijao](https://github.com/rodrigofeijao) via [PR #3227](https://github.com/strawberry-graphql/strawberry/pull/3227/) 0.213.0 - 2023-11-08 -------------------- This release adds support in _all_ all our HTTP integration for choosing between different GraphQL IDEs. For now we support [GraphiQL](https://github.com/graphql/graphiql) (the default), [Apollo Sandbox](https://www.apollographql.com/docs/graphos/explorer/sandbox/), and [Pathfinder](https://pathfinder.dev/). **Deprecations:** This release deprecates the `graphiql` option in all HTTP integrations, in favour of `graphql_ide`, this allows us to only have one settings to change GraphQL ide, or to disable it. Here's a couple of examples of how you can use this: ### FastAPI ```python import strawberry from fastapi import FastAPI from strawberry.fastapi import GraphQLRouter from api.schema import schema graphql_app = GraphQLRouter(schema, graphql_ide="apollo-sandbox") app = FastAPI() app.include_router(graphql_app, prefix="/graphql") ``` ### Django ```python from django.urls import path from strawberry.django.views import GraphQLView from api.schema import schema urlpatterns = [ path("graphql/", GraphQLView.as_view(schema=schema, graphql_ide="pathfinder")), ] ``` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3209](https://github.com/strawberry-graphql/strawberry/pull/3209/) 0.212.0 - 2023-11-07 -------------------- This release changes how we check for generic types. Previously, any type that had a generic typevar would be considered generic for the GraphQL schema, this would generate un-necessary types in some cases. Now, we only consider a type generic if it has a typevar that is used as the type of a field or one of its arguments. For example the following type: ```python @strawberry.type class Edge[T]: cursor: strawberry.ID some_interna_value: strawberry.Private[T] ``` Will not generate a generic type in the schema, as the typevar `T` is not used as the type of a field or argument. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3202](https://github.com/strawberry-graphql/strawberry/pull/3202/) 0.211.2 - 2023-11-06 -------------------- This release removes unused `graphiql` submodules for Flask, Quart and Sanic. Contributed by [Pierre Chapuis](https://github.com/catwell) via [PR #3203](https://github.com/strawberry-graphql/strawberry/pull/3203/) 0.211.1 - 2023-10-25 -------------------- This release fixes an issue that prevented the `parser_cache` extension to be used in combination with other extensions such as `MaxTokensLimiter`. The following should work as expected now: ```python schema = strawberry.Schema( query=Query, extensions=[MaxTokensLimiter(max_token_count=20), ParserCache()] ) ``` Contributed by [David Šanda](https://github.com/Dazix) via [PR #3170](https://github.com/strawberry-graphql/strawberry/pull/3170/) 0.211.0 - 2023-10-24 -------------------- This release adds a Quart view. Contributed by [Pierre Chapuis](https://github.com/catwell) via [PR #3162](https://github.com/strawberry-graphql/strawberry/pull/3162/) 0.210.0 - 2023-10-24 -------------------- This release deprecates our `SentryTracingExtension`, as it is now incorporated directly into Sentry itself as of [version 1.32.0](https://github.com/getsentry/sentry-python/releases/tag/1.32.0). You can now directly instrument Strawberry with Sentry. Below is the revised usage example: ```python import sentry_sdk from sentry_sdk.integrations.strawberry import StrawberryIntegration sentry_sdk.init( dsn="___PUBLIC_DSN___", integrations=[ # make sure to set async_execution to False if you're executing # GraphQL queries synchronously StrawberryIntegration(async_execution=True), ], traces_sample_rate=1.0, ) ``` Many thanks to @sentrivana for their work on this integration! 0.209.8 - 2023-10-20 -------------------- Fix strawberry mypy plugin for pydantic v2 Contributed by [Corentin-Br](https://github.com/Corentin-Br) via [PR #3159](https://github.com/strawberry-graphql/strawberry/pull/3159/) 0.209.7 - 2023-10-15 -------------------- Remove stack_info from error log messages to not clutter error logging with unnecessary information. Contributed by [Malte Finsterwalder](https://github.com/finsterwalder) via [PR #3143](https://github.com/strawberry-graphql/strawberry/pull/3143/) 0.209.6 - 2023-10-07 -------------------- Add text/html content-type to chalice graphiql response Contributed by [Julian Popescu](https://github.com/jpopesculian) via [PR #3137](https://github.com/strawberry-graphql/strawberry/pull/3137/) 0.209.5 - 2023-10-03 -------------------- This release adds a new private hook in our HTTP views, it is called `_handle_errors` and it is meant to be used by Sentry (or other integrations) to handle errors without having to patch methods that could be overridden by the users Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3127](https://github.com/strawberry-graphql/strawberry/pull/3127/) 0.209.4 - 2023-10-02 -------------------- This release changes how we check for conflicting resolver arguments to exclude `self` from those checks, which were introduced on version 0.208.0. It is a common pattern among integrations, such as the Django one, to use `root: Model` in the resolvers for better typing inference. Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #3131](https://github.com/strawberry-graphql/strawberry/pull/3131/) 0.209.3 - 2023-10-02 -------------------- Mark Django's asyncview as a coroutine using `asgiref.sync.markcoroutinefunction` to support using it with Python 3.12. Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #3124](https://github.com/strawberry-graphql/strawberry/pull/3124/) 0.209.2 - 2023-09-24 -------------------- Fix generation of input based on pydantic models using nested `Annotated` type annotations: ```python import strawberry from pydantic import BaseModel class User(BaseModel): age: Optional[Annotated[int, "metadata"]] @strawberry.experimental.pydantic.input(all_fields=True) class UserInput: pass ``` Contributed by [Matthieu MN](https://github.com/gazorby) via [PR #3109](https://github.com/strawberry-graphql/strawberry/pull/3109/) 0.209.1 - 2023-09-21 -------------------- This release fixes an issue when trying to generate code from a schema that was using double quotes inside descriptions. The following schema will now generate code correctly: ```graphql """ A type of person or character within the "Star Wars" Universe. """ type Species { """ The classification of this species, such as "mammal" or "reptile". """ classification: String! } ``` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3112](https://github.com/strawberry-graphql/strawberry/pull/3112/) 0.209.0 - 2023-09-19 -------------------- This release adds support for generating Strawberry types from SDL files. For example, given the following SDL file: ```graphql type Query { user: User } type User { id: ID! name: String! } ``` you can run ```bash strawberry schema-codegen schema.graphql ``` to generate the following Python code: ```python import strawberry @strawberry.type class Query: user: User | None @strawberry.type class User: id: strawberry.ID name: str schema = strawberry.Schema(query=Query) ``` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3096](https://github.com/strawberry-graphql/strawberry/pull/3096/) 0.208.3 - 2023-09-19 -------------------- Adding support for additional pydantic built in types like EmailStr or PostgresDsn. Contributed by [ppease](https://github.com/ppease) via [PR #3101](https://github.com/strawberry-graphql/strawberry/pull/3101/) 0.208.2 - 2023-09-18 -------------------- This release fixes an issue that would prevent using generics with unnamed unions, like in this example: ```python from typing import Generic, TypeVar, Union import strawberry T = TypeVar("T") @strawberry.type class Connection(Generic[T]): nodes: list[T] @strawberry.type class Entity1: id: int @strawberry.type class Entity2: id: int @strawberry.type class Query: entities: Connection[Union[Entity1, Entity2]] ``` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3099](https://github.com/strawberry-graphql/strawberry/pull/3099/) 0.208.1 - 2023-09-15 -------------------- This fixes a bug where codegen would choke trying to find a field in the schema for a generic type. Contributed by [Matt Gilson](https://github.com/mgilson) via [PR #3077](https://github.com/strawberry-graphql/strawberry/pull/3077/) 0.208.0 - 2023-09-14 -------------------- Adds new strawberry.Parent type annotation to support resolvers without use of self. E.g. ```python @dataclass class UserRow: id_: str @strawberry.type class User: @strawberry.field @staticmethod async def name(parent: strawberry.Parent[UserRow]) -> str: return f"User Number {parent.id_}" @strawberry.type class Query: @strawberry.field def user(self) -> User: return UserRow(id_="1234") ``` Contributed by [mattalbr](https://github.com/mattalbr) via [PR #3017](https://github.com/strawberry-graphql/strawberry/pull/3017/) 0.207.1 - 2023-09-14 -------------------- This fixes a bug where codegen would choke on FragmentSpread nodes in the GraphQL during type collection. e.g.: ``` fragment PartialBlogPost on BlogPost { title } query OperationName { interface { id ... on BlogPost { ...PartialBlogPost } ... on Image { url } } } ``` The current version of the code generator is not able to handle the `...PartialBogPost` in this position because it assumes it can only find `Field` type nodes even though the spread should be legit. Contributed by [Matt Gilson](https://github.com/mgilson) via [PR #3086](https://github.com/strawberry-graphql/strawberry/pull/3086/) 0.207.0 - 2023-09-14 -------------------- This release removes the deprecated `ignore` argument from the `QueryDepthLimiter` extension. Contributed by [Kai Benevento](https://github.com/benesgarage) via [PR #3093](https://github.com/strawberry-graphql/strawberry/pull/3093/) 0.206.0 - 2023-09-13 -------------------- `strawberry codegen` can now operate on multiple input query files. The previous behavior of naming the file `types.js` and `types.py` for the builtin `typescript` and `python` plugins respectively is preserved, but only if a single query file is passed. When more than one query file is passed, the code generator will now use the stem of the query file's name to construct the name of the output files. e.g. `my_query.graphql` -> `my_query.js` or `my_query.py`. Creators of custom plugins are responsible for controlling the name of the output file themselves. To accomodate this, if the `__init__` method of a `QueryCodegenPlugin` has a parameter named `query` or `query_file`, the `pathlib.Path` to the query file will be passed to the plugin's `__init__` method. Finally, the `ConsolePlugin` has also recieved two new lifecycle methods. Unlike other `QueryCodegenPlugin`, the same instance of the `ConsolePlugin` is used for each query file processed. This allows it to keep state around how many total files were processed. The `ConsolePlugin` recieved two new lifecycle hooks: `before_any_start` and `after_all_finished` that get called at the appropriate times. Contributed by [Matt Gilson](https://github.com/mgilson) via [PR #2911](https://github.com/strawberry-graphql/strawberry/pull/2911/) 0.205.0 - 2023-08-24 -------------------- `strawberry codegen` previously choked for inputs that used the `strawberry.UNSET` sentinal singleton value as a default. The intent here is to say that if a variable is not part of the request payload, then the `UNSET` default value will not be modified and the service code can then treat an unset value differently from a default value, etc. For codegen, we treat the `UNSET` default value as a `GraphQLNullValue`. The `.value` property is the `UNSET` object in this case (instead of the usual `None`). In the built-in python code generator, this causes the client to generate an object with a `None` default. Custom client generators can sniff at this value and update their behavior. Contributed by [Matt Gilson](https://github.com/mgilson) via [PR #3050](https://github.com/strawberry-graphql/strawberry/pull/3050/) 0.204.0 - 2023-08-15 -------------------- Adds a new flag to `export-schema` command, `--output`, which allows the user to specify the output file. If unset (current behavior), the command will continue to print to stdout. Contributed by [Chris Hua](https://github.com/stillmatic) via [PR #3033](https://github.com/strawberry-graphql/strawberry/pull/3033/) 0.203.3 - 2023-08-14 -------------------- Mark pydantic constrained list test with need_pydantic_v1 since it is removed in pydantic V2 Contributed by [tjeerddie](https://github.com/tjeerddie) via [PR #3034](https://github.com/strawberry-graphql/strawberry/pull/3034/) 0.203.2 - 2023-08-14 -------------------- Enhancements: - Improved pydantic conversion compatibility with specialized list classes. - Modified `StrawberryAnnotation._is_list` to check if the `annotation` extends from the `list` type, enabling it to be considered a list. - in `StrawberryAnnotation` Moved the `_is_list` check before the `_is_generic` check in `resolve` to avoid `unsupported` error in `_is_generic` before it checked `_is_list`. This enhancement enables the usage of constrained lists as class types and allows the creation of specialized lists. The following example demonstrates this feature: ```python import strawberry from pydantic import BaseModel, ConstrainedList class FriendList(ConstrainedList): min_items = 1 class UserModel(BaseModel): age: int friend_names: FriendList[str] @strawberry.experimental.pydantic.type(UserModel) class User: age: strawberry.auto friend_names: strawberry.auto ``` Contributed by [tjeerddie](https://github.com/tjeerddie) via [PR #2909](https://github.com/strawberry-graphql/strawberry/pull/2909/) 0.203.1 - 2023-08-12 -------------------- This release updates the built-in GraphiQL to the current latest version (3.0.5), it also updates React to the current latest version (18.2.0) and uses the production distribution instead of development to reduce bundle size. Contributed by [Kien Dang](https://github.com/kiendang) via [PR #3031](https://github.com/strawberry-graphql/strawberry/pull/3031/) 0.203.0 - 2023-08-10 -------------------- Add support for extra colons in the `GlobalID` string. Before, the string `SomeType:some:value` would produce raise an error saying that it was expected the string to be splited in 2 parts when doing `.split(":")`. Now we are using `.split(":", 1)`, meaning that the example above will consider `SomeType` to be the type name, and `some:value` to be the node_id. Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #3025](https://github.com/strawberry-graphql/strawberry/pull/3025/) 0.202.1 - 2023-08-09 -------------------- TypingUnionType import error check is reraised because TypingGenericAlias is checked at the same time which is checked under 3.9 instead of under 3.10 Fix by separating TypingUnionType and TypingGenericAlias imports in their own try-catch Contributed by [tjeerddie](https://github.com/tjeerddie) via [PR #3023](https://github.com/strawberry-graphql/strawberry/pull/3023/) 0.202.0 - 2023-08-08 -------------------- This release updates Strawberry's codebase to use new features in Python 3.8. It also removes `backports.cached-property` from our dependencies, as we can now rely on the standard library's `functools.cached_property`. Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #2995](https://github.com/strawberry-graphql/strawberry/pull/2995/) 0.201.1 - 2023-08-08 -------------------- Fix strawberry mypy plugin for pydantic v1 Contributed by [tjeerddie](https://github.com/tjeerddie) via [PR #3019](https://github.com/strawberry-graphql/strawberry/pull/3019/) 0.201.0 - 2023-08-08 -------------------- Fix import error in `strawberry.ext.mypy_plugin` for users who don't use pydantic. Contributed by [David Němec](https://github.com/davidnemec) via [PR #3018](https://github.com/strawberry-graphql/strawberry/pull/3018/) 0.200.0 - 2023-08-07 -------------------- Adds initial support for pydantic V2. This is extremely experimental for wider initial testing. We do not encourage using this in production systems yet. Contributed by [James Chua](https://github.com/thejaminator) via [PR #2972](https://github.com/strawberry-graphql/strawberry/pull/2972/) 0.199.3 - 2023-08-06 -------------------- This release fixes an issue on `relay.ListConnection` where async iterables that returns non async iterable objects after being sliced where producing errors. This should fix an issue with async strawberry-graphql-django when returning already prefetched QuerySets. Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #3014](https://github.com/strawberry-graphql/strawberry/pull/3014/) 0.199.2 - 2023-08-03 -------------------- This releases improves how we handle Annotated and async types (used in subscriptions). Previously we weren't able to use unions with names inside subscriptions, now that's fixed 😊 Example: ```python @strawberry.type class A: a: str @strawberry.type class B: b: str @strawberry.type class Query: x: str = "Hello" @strawberry.type class Subscription: @strawberry.subscription async def example_with_union(self) -> AsyncGenerator[Union[A, B], None]: yield A(a="Hi") ``` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3008](https://github.com/strawberry-graphql/strawberry/pull/3008/) 0.199.1 - 2023-08-02 -------------------- This release fixes an issue in the `graphql-ws` implementation where sending a `null` payload would cause the connection to be closed. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #3007](https://github.com/strawberry-graphql/strawberry/pull/3007/) 0.199.0 - 2023-08-01 -------------------- This release changes how we handle generic type vars, bringing support to the new generic syntax in Python 3.12 (which will be out in October). This now works: ```python @strawberry.type class Edge[T]: cursor: strawberry.ID node_field: T @strawberry.type class Query: @strawberry.field def example(self) -> Edge[int]: return Edge(cursor=strawberry.ID("1"), node_field=1) schema = strawberry.Schema(query=Query) ``` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2993](https://github.com/strawberry-graphql/strawberry/pull/2993/) 0.198.0 - 2023-07-31 -------------------- This release adds support for returning interfaces directly in resolvers: ```python @strawberry.interface class Node: id: strawberry.ID @classmethod def resolve_type(cls, obj: Any, *args: Any, **kwargs: Any) -> str: return "Video" if obj.id == "1" else "Image" @strawberry.type class Video(Node): ... @strawberry.type class Image(Node): ... @strawberry.type class Query: @strawberry.field def node(self, id: strawberry.ID) -> Node: return Node(id=id) schema = strawberry.Schema(query=Query, types=[Video, Image]) ``` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2989](https://github.com/strawberry-graphql/strawberry/pull/2989/) 0.197.0 - 2023-07-30 -------------------- This release removes support for Python 3.7 as its end of life was on 27 Jun 2023. This will allow us to reduce the number of CI jobs we have, and potentially use newer features of Python. ⚡ Contributed by [Alexander](https://github.com/devkral) via [PR #2907](https://github.com/strawberry-graphql/strawberry/pull/2907/) 0.196.2 - 2023-07-28 -------------------- This release fixes an issue when trying to use `Annotated[strawberry.auto, ...]` on python 3.10 or older, which got evident after the fix from 0.196.1. Previously we were throwing the type away, since it usually is `Any`, but python 3.10 and older will validate that the first argument passed for `Annotated` is callable (3.11+ does not do that anymore), and `StrawberryAuto` is not. This changes it to keep that `Any`, which is also what someone would expect when resolving the annotation using our custom `eval_type` function. Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #2990](https://github.com/strawberry-graphql/strawberry/pull/2990/) 0.196.1 - 2023-07-26 -------------------- This release fixes an issue where annotations resolution for auto and lazy fields using `Annotated` where not preserving the remaining arguments because of a typo in the arguments filtering. Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #2983](https://github.com/strawberry-graphql/strawberry/pull/2983/) 0.196.0 - 2023-07-26 -------------------- This release adds support for union with a single member, they are useful for future proofing your schema in cases you know a field will be part of a union in future. ```python import strawberry from typing import Annotated @strawberry.type class Audio: duration: int @strawberry.type class Query: # note: Python's Union type doesn't support single members, # Union[Audio] is exactly the same as Audio, so we use # use Annotated and strawberry.union to tell Strawberry this is # a union with a single member latest_media: Annotated[Audio, strawberry.union("MediaItem")] schema = strawberry.Schema(query=Query) ``` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2982](https://github.com/strawberry-graphql/strawberry/pull/2982/) 0.195.3 - 2023-07-22 -------------------- This release no longer requires an upperbound pin for uvicorn, ensuring compatibility with future versions of uvicorn without the need for updating Strawberry. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2968](https://github.com/strawberry-graphql/strawberry/pull/2968/) 0.195.2 - 2023-07-15 -------------------- This release introduces a bug fix for relay connection where previously they wouldn't work without padding the `first` argument. Contributed by [Alexander](https://github.com/devkral) via [PR #2938](https://github.com/strawberry-graphql/strawberry/pull/2938/) 0.195.1 - 2023-07-15 -------------------- This release fixes a bug where returning a generic type from a field that was returning an interface would throw an error. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2955](https://github.com/strawberry-graphql/strawberry/pull/2955/) 0.195.0 - 2023-07-14 -------------------- Improve the time complexity of `strawberry.interface` using `resolve_type`. Achieved time complexity is now O(1) with respect to the number of implementations of an interface. Previously, the use of `is_type_of` resulted in a worst-case performance of O(n). **Before**: ```shell --------------------------------------------------------------------------- Name (time in ms) Min Max --------------------------------------------------------------------------- test_interface_performance[1] 18.0224 (1.0) 50.3003 (1.77) test_interface_performance[16] 22.0060 (1.22) 28.4240 (1.0) test_interface_performance[256] 69.1364 (3.84) 76.1349 (2.68) test_interface_performance[4096] 219.6461 (12.19) 231.3732 (8.14) --------------------------------------------------------------------------- ``` **After**: ```shell --------------------------------------------------------------------------- Name (time in ms) Min Max --------------------------------------------------------------------------- test_interface_performance[1] 14.3921 (1.0) 46.2064 (2.79) test_interface_performance[16] 14.8669 (1.03) 16.5732 (1.0) test_interface_performance[256] 15.8977 (1.10) 24.4618 (1.48) test_interface_performance[4096] 18.7340 (1.30) 21.2899 (1.28) --------------------------------------------------------------------------- ``` Contributed by [San Kilkis](https://github.com/skilkis) via [PR #1949](https://github.com/strawberry-graphql/strawberry/pull/1949/) 0.194.4 - 2023-07-08 -------------------- This release makes sure that `Schema.process_errors()` is called _once_ for every error which happens with `graphql-transport-ws` operations. Contributed by [Kristján Valur Jónsson](https://github.com/kristjanvalur) via [PR #2899](https://github.com/strawberry-graphql/strawberry/pull/2899/) 0.194.3 - 2023-07-08 -------------------- Added default argument to the typer Argument function, this adds support for older versions of typer. Contributed by [Jaime Coello de Portugal](https://github.com/jaimecp89) via [PR #2906](https://github.com/strawberry-graphql/strawberry/pull/2906/) 0.194.2 - 2023-07-08 -------------------- This release includes a performance improvement to `strawberry.lazy()` to allow relative module imports to be resolved faster. Contributed by [Karim Alibhai](https://github.com/karimsa) via [PR #2926](https://github.com/strawberry-graphql/strawberry/pull/2926/) 0.194.1 - 2023-07-08 -------------------- This release adds a setter on `StrawberryAnnotation.annotation`, this fixes an issue on Strawberry Django. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2932](https://github.com/strawberry-graphql/strawberry/pull/2932/) 0.194.0 - 2023-07-08 -------------------- Restore evaled type access in `StrawberryAnnotation` Prior to Strawberry 192.2 the `annotation` attribute of `StrawberryAnnotation` would return an evaluated type when possible due reserved argument parsing. 192.2 moved the responsibility of evaluating and caching results to the `evaluate` method of `StrawberryAnnotation`. This introduced a regression when using future annotations for any code implicitely relying on the `annotation` attribute being an evaluated type. To fix this regression and mimick pre-192.2 behavior, this release adds an `annotation` property to `StrawberryAnnotation` that internally calls the `evaluate` method. On success the evaluated type is returned. If a `NameError` is raised due to an unresolvable annotation, the raw annotation is returned. Contributed by [San Kilkis](https://github.com/skilkis) via [PR #2925](https://github.com/strawberry-graphql/strawberry/pull/2925/) 0.193.1 - 2023-07-05 -------------------- This fixes a regression from 0.190.0 where changes to the return type of a field done by Field Extensions would not be taken in consideration by the schema. Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #2922](https://github.com/strawberry-graphql/strawberry/pull/2922/) 0.193.0 - 2023-07-04 -------------------- This release updates the API to listen to Django Channels to avoid race conditions when confirming GraphQL subscriptions. **Deprecations:** This release contains a deprecation for the Channels integration. The `channel_listen` method will be replaced with an async context manager that returns an awaitable AsyncGenerator. This method is called `listen_to_channel`. An example of migrating existing code is given below: ```py # Existing code @strawberry.type class MyDataType: name: str @strawberry.type class Subscription: @strawberry.subscription async def my_data_subscription( self, info: strawberry.Info, groups: list[str] ) -> AsyncGenerator[MyDataType | None, None]: yield None async for message in info.context["ws"].channel_listen( "my_data", groups=groups ): yield MyDataType(name=message["payload"]) ``` ```py # New code @strawberry.type class Subscription: @strawberry.subscription async def my_data_subscription( self, info: strawberry.Info, groups: list[str] ) -> AsyncGenerator[MyDataType | None, None]: async with info.context["ws"].listen_to_channel("my_data", groups=groups) as cm: yield None async for message in cm: yield MyDataType(name=message["payload"]) ``` Contributed by [Moritz Ulmer](https://github.com/moritz89) via [PR #2856](https://github.com/strawberry-graphql/strawberry/pull/2856/) 0.192.2 - 2023-07-03 -------------------- This release fixes an issue related to using `typing.Annotated` in resolver arguments following the declaration of a reserved argument such as `strawberry.types.Info`. Before this fix, the following would be converted incorrectly: ```python from __future__ import annotations import strawberry import uuid from typing_extensions import Annotated from strawberry.types import Info @strawberry.type class Query: @strawberry.field def get_testing( self, info: strawberry.Info, id_: Annotated[uuid.UUID, strawberry.argument(name="id")], ) -> str | None: return None schema = strawberry.Schema(query=Query) print(schema) ``` Resulting in the schema: ```graphql type Query { getTesting(id_: UUID!): String # ⬅️ see `id_` } scalar UUID ``` After this fix, the schema is converted correctly: ```graphql type Query { getTesting(id: UUID!): String } scalar UUID ``` Contributed by [San Kilkis](https://github.com/skilkis) via [PR #2901](https://github.com/strawberry-graphql/strawberry/pull/2901/) 0.192.1 - 2023-07-02 -------------------- Add specifications in FastAPI doc if query via GET is enabled Contributed by [guillaumeLepape](https://github.com/guillaumeLepape) via [PR #2913](https://github.com/strawberry-graphql/strawberry/pull/2913/) 0.192.0 - 2023-06-28 -------------------- This release introduces a new command called `upgrade`, this command can be used to run codemods on your codebase to upgrade to the latest version of Strawberry. At the moment we only support upgrading unions to use the new syntax with annotated, but in future we plan to add more commands to help with upgrading. Here's how you can use the command to upgrade your codebase: ```shell strawberry upgrade annotated-union . ``` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2886](https://github.com/strawberry-graphql/strawberry/pull/2886/) 0.191.0 - 2023-06-28 -------------------- This release adds support for declaring union types using `typing.Annotated` instead of `strawberry.union(name, types=...)`. Code using the old syntax will continue to work, but it will trigger a deprecation warning. Using Annotated will improve type checking and IDE support especially when using `pyright`. Before: ```python Animal = strawberry.union("Animal", (Cat, Dog)) ``` After: ```python from typing import Annotated, Union Animal = Annotated[Union[Cat, Dog], strawberry.union("Animal")] ``` 0.190.0 - 2023-06-27 -------------------- This release refactors the way we resolve field types to to make it more robust, resolving some corner cases. One case that should be fixed is when using specialized generics with future annotations. Contributed by [Alexander](https://github.com/devkral) via [PR #2868](https://github.com/strawberry-graphql/strawberry/pull/2868/) 0.189.3 - 2023-06-27 -------------------- This release removes some usage of deprecated functions from GraphQL-core. Contributed by [Kristján Valur Jónsson](https://github.com/kristjanvalur) via [PR #2894](https://github.com/strawberry-graphql/strawberry/pull/2894/) 0.189.2 - 2023-06-27 -------------------- The `graphql-transport-ws` protocol allows for subscriptions to error during execution without terminating the subscription. Non-fatal errors produced by subscriptions now produce `Next` messages containing an `ExecutionResult` with an `error` field and don't necessarily terminate the subscription. This is in accordance to the behaviour of Apollo server. Contributed by [Kristján Valur Jónsson](https://github.com/kristjanvalur) via [PR #2876](https://github.com/strawberry-graphql/strawberry/pull/2876/) 0.189.1 - 2023-06-25 -------------------- This release fixes a deprecation warning being triggered by the relay integration. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2858](https://github.com/strawberry-graphql/strawberry/pull/2858/) 0.189.0 - 2023-06-22 -------------------- This release updates `create_type` to add support for all arguments that `strawberry.type` supports. This includes: `description`, `extend`, `directives`, `is_input` and `is_interface`. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2880](https://github.com/strawberry-graphql/strawberry/pull/2880/) 0.188.0 - 2023-06-22 -------------------- This release gives codegen clients the ability to inquire about the `__typename` of a `GraphQLObjectType`. This information can be used to automatically select the proper type to hydrate when working with a union type in the response. Contributed by [Matt Gilson](https://github.com/mgilson) via [PR #2875](https://github.com/strawberry-graphql/strawberry/pull/2875/) 0.187.5 - 2023-06-21 -------------------- This release fixes a regression when comparing a `StrawberryAnnotation` instance with anything that is not also a `StrawberryAnnotation` instance, which caused it to raise a `NotImplementedError`. This reverts its behavior back to how it worked before, where it returns `NotImplemented` instead, meaning that the comparison can be delegated to the type being compared against or return `False` in case it doesn't define an `__eq__` method. Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #2879](https://github.com/strawberry-graphql/strawberry/pull/2879/) 0.187.4 - 2023-06-21 -------------------- `graphql-transport-ws` handler now uses a single dict to manage active operations. Contributed by [Kristján Valur Jónsson](https://github.com/kristjanvalur) via [PR #2699](https://github.com/strawberry-graphql/strawberry/pull/2699/) 0.187.3 - 2023-06-21 -------------------- This release fixes a typing regression on `StraberryContainer` subclasses where type checkers would not allow non `WithStrawberryObjectDefinition` types to be passed for its `of_type` argument (e.g. `StrawberryOptional(str)`) Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #2878](https://github.com/strawberry-graphql/strawberry/pull/2878/) 0.187.2 - 2023-06-21 -------------------- This release removes `get_object_definition_strict` and instead overloads `get_object_definition` to accept an extra `strct` keyword. This is a new feature so it is unlikely to break anything. Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #2877](https://github.com/strawberry-graphql/strawberry/pull/2877/) 0.187.1 - 2023-06-21 -------------------- This release bumps the minimum requirement of `typing-extensions` to 4.5 Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2872](https://github.com/strawberry-graphql/strawberry/pull/2872/) 0.187.0 - 2023-06-20 -------------------- This release renames `_type_definition` to `__strawberry_definition__`. This doesn't change the public API of Strawberry, but if you were using `_type_definition` you can still access it, but it will be removed in future. Contributed by [ניר](https://github.com/nrbnlulu) via [PR #2836](https://github.com/strawberry-graphql/strawberry/pull/2836/) 0.186.3 - 2023-06-20 -------------------- This release adds resolve_async to NodeExtension to allow it to be used together with other field async extensions/permissions. Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #2863](https://github.com/strawberry-graphql/strawberry/pull/2863/) 0.186.2 - 2023-06-19 -------------------- This release fixes an issue on StrawberryField.copy_with method not copying its extensions and overwritten `_arguments`. Also make sure that all lists/tuples in those types are copied as new lists/tuples to avoid unexpected behavior. Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #2865](https://github.com/strawberry-graphql/strawberry/pull/2865/) 0.186.1 - 2023-06-16 -------------------- In this release, we pass the default values from the strawberry.Schema through to the codegen plugins. The default python plugin now adds these default values to the objects it generates. Contributed by [Matt Gilson](https://github.com/mgilson) via [PR #2860](https://github.com/strawberry-graphql/strawberry/pull/2860/) 0.186.0 - 2023-06-15 -------------------- This release removes more parts of the Mypy plugin, since they are not needed anymore. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2852](https://github.com/strawberry-graphql/strawberry/pull/2852/) 0.185.2 - 2023-06-15 -------------------- This release fixes a bug causing a KeyError exception to be thrown during subscription cleanup. Contributed by [rjwills28](https://github.com/rjwills28) via [PR #2794](https://github.com/strawberry-graphql/strawberry/pull/2794/) 0.185.1 - 2023-06-14 -------------------- Correct a type-hinting bug with `strawberry.directive`. This may cause some consumers to have to remove a `# type: ignore` comment or unnecessary `typing.cast` in order to get `mypy` to pass. Contributed by [Matt Gilson](https://github.com/mgilson) via [PR #2847](https://github.com/strawberry-graphql/strawberry/pull/2847/) 0.185.0 - 2023-06-14 -------------------- This release removes our custom `__dataclass_transform__` decorator and replaces it with typing-extension's one. It also removes parts of the mypy plugin, since most of it is not needed anymore 🙌 This update requires typing_extensions>=4.1.0 Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2227](https://github.com/strawberry-graphql/strawberry/pull/2227/) 0.184.1 - 2023-06-13 -------------------- This release migrates our CLI to typer, all commands should work the same as before. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2569](https://github.com/strawberry-graphql/strawberry/pull/2569/) 0.184.0 - 2023-06-12 -------------------- This release improves the `relay.NodeID` annotation check by delaying it until after class initialization. This resolves issues with evaluating type annotations before they are fully defined and enables integrations to inject code for it in the type. Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #2838](https://github.com/strawberry-graphql/strawberry/pull/2838/) 0.183.8 - 2023-06-12 -------------------- This release fixes a bug in the codegen where `List` objects are currently emitted as `Optional` objects. Contributed by [Matt Gilson](https://github.com/mgilson) via [PR #2843](https://github.com/strawberry-graphql/strawberry/pull/2843/) 0.183.7 - 2023-06-12 -------------------- Refactor `ConnectionExtension` to copy arguments instead of extending them. This should fix some issues with integrations which override `arguments`, like the django one, where the inserted arguments were vanishing. Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #2839](https://github.com/strawberry-graphql/strawberry/pull/2839/) 0.183.6 - 2023-06-09 -------------------- This release fixes a bug where codegen would fail on mutations that have object arguments in the query. Additionally, it does a topological sort of the types before passing it to the plugins to ensure that dependent types are defined after their dependencies. Contributed by [Matt Gilson](https://github.com/mgilson) via [PR #2831](https://github.com/strawberry-graphql/strawberry/pull/2831/) 0.183.5 - 2023-06-08 -------------------- This release fixes an issue where Strawberry would make copies of types that were using specialized generics that were not Strawerry types. This issue combined with the use of lazy types was resulting in duplicated type errors. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2824](https://github.com/strawberry-graphql/strawberry/pull/2824/) 0.183.4 - 2023-06-07 -------------------- This release fixes an issue for parsing lazy types using forward references when they were enclosed in an `Optional[...]` type. The following now should work properly: ```python from __future__ import annotations from typing import Optional, Annotated import strawberry @strawberry.type class MyType: other_type: Optional[Annotated["OtherType", strawberry.lazy("some.module")]] # or like this other_type: Annotated["OtherType", strawberry.lazy("some.module")] | None ``` Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #2821](https://github.com/strawberry-graphql/strawberry/pull/2821/) 0.183.3 - 2023-06-07 -------------------- This release fixes a codegen bug. Prior to this fix, inline fragments would only include the last field defined within its scope and all fields common with its siblings. After this fix, all fields will be included in the generated types. Contributed by [Matt Gilson](https://github.com/mgilson) via [PR #2819](https://github.com/strawberry-graphql/strawberry/pull/2819/) 0.183.2 - 2023-06-07 -------------------- Fields with generics support directives. Contributed by [A. Coady](https://github.com/coady) via [PR #2811](https://github.com/strawberry-graphql/strawberry/pull/2811/) 0.183.1 - 2023-06-06 -------------------- This release fixes an issue of the new relay integration adding an `id: GlobalID!` argument on all objects that inherit from `relay.Node`. That should've only happened for `Query` types. Strawberry now will not force a `relay.Node` or any type that inherits it to be inject the node extension which adds the argument and a resolver for it, meaning that this code: ```python import strawberry from strawberry import relay @strawberry.type class Fruit(relay.Node): id: relay.NodeID[int] @strawberry.type class Query: node: relay.Node fruit: Fruit ``` Should now be written as: ```python import strawberry from strawberry import relay @strawberry.type class Fruit(relay.Node): id: relay.NodeID[int] @strawberry.type class Query: node: relay.Node = relay.node() # <- note the "= relay.node()" here fruit: Fruit = relay.node() ``` Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #2814](https://github.com/strawberry-graphql/strawberry/pull/2814/) 0.183.0 - 2023-06-06 -------------------- This release adds a new field extension called `InputMutationExtension`, which makes it easier to create mutations that receive a single input type called `input`, while still being able to define the arguments of that input on the resolver itself. The following example: ```python import strawberry from strawberry.field_extensions import InputMutationExtension @strawberry.type class Fruit: id: strawberry.ID name: str weight: float @strawberry.type class Mutation: @strawberry.mutation(extensions=[InputMutationExtension()]) def update_fruit_weight( self, info: strawberry.Info, id: strawberry.ID, weight: Annotated[ float, strawberry.argument(description="The fruit's new weight in grams"), ], ) -> Fruit: fruit = ... # retrieve the fruit with the given ID fruit.weight = weight ... # maybe save the fruit in the database return fruit ``` Would generate a schema like this: ```graphql input UpdateFruitInput { id: ID! """ The fruit's new weight in grams """ weight: Float! } type Fruit { id: ID! name: String! weight: Float! } type Mutation { updateFruitWeight(input: UpdateFruitInput!): Fruit! } ``` Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #2580](https://github.com/strawberry-graphql/strawberry/pull/2580/) 0.182.0 - 2023-06-06 -------------------- Initial relay spec implementation. For information on how to use it, check out the docs in here: https://strawberry.rocks/docs/guides/relay Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #2511](https://github.com/strawberry-graphql/strawberry/pull/2511/) 0.181.0 - 2023-06-06 -------------------- This release adds support for properly resolving lazy references when using forward refs. For example, this code should now work without any issues: ```python from __future__ import annotations from typing import TYPE_CHECKING, Annotated if TYPE_CHECKING: from some.module import OtherType @strawberry.type class MyType: @strawberry.field async def other_type( self, ) -> Annotated[OtherType, strawberry.lazy("some.module")]: ... ``` Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #2744](https://github.com/strawberry-graphql/strawberry/pull/2744/) 0.180.5 - 2023-06-02 -------------------- This release fixes a bug in fragment codegen to pick up type definitions from the proper place in the schema. Contributed by [Matt Gilson](https://github.com/mgilson) via [PR #2805](https://github.com/strawberry-graphql/strawberry/pull/2805/) 0.180.4 - 2023-06-02 -------------------- Custom codegen plugins will fail to write files if the plugin is trying to put the file anywhere other than the root output directory (since the child directories do not yet exist). This change will create the child directory if necessary before attempting to write the file. Contributed by [Matt Gilson](https://github.com/mgilson) via [PR #2806](https://github.com/strawberry-graphql/strawberry/pull/2806/) 0.180.3 - 2023-06-02 -------------------- This release updates the built-in GraphiQL to the current latest version 2.4.7 and improves styling for the GraphiQL Explorer Plugin. Contributed by [Kien Dang](https://github.com/kiendang) via [PR #2804](https://github.com/strawberry-graphql/strawberry/pull/2804/) 0.180.2 - 2023-06-02 -------------------- In this release codegen no longer chokes on queries that use a fragment. There is one significant limitation at the present. When a fragment is included via the spread operator in an object, it must be the only field present. Attempts to include more fields will result in a ``ValueError``. However, there are some real benefits. When a fragment is included in multiple places in the query, only a single class will be made to represent that fragment: ``` fragment Point on Bar { id x y } query GetPoints { circlePoints { ...Point } squarePoints { ...Point } } ``` Might generate the following types ```py class Point: id: str x: float y: float class GetPointsResult: circle_points: List[Point] square_points: List[Point] ``` The previous behavior would generate duplicate classes for for the `GetPointsCirclePoints` and `GetPointsSquarePoints` even though they are really identical classes. Contributed by [Matt Gilson](https://github.com/mgilson) via [PR #2802](https://github.com/strawberry-graphql/strawberry/pull/2802/) 0.180.1 - 2023-06-01 -------------------- Make StrawberryAnnotation hashable, to make it compatible to newer versions of dacite. Contributed by [Jaime Coello de Portugal](https://github.com/jaimecp89) via [PR #2790](https://github.com/strawberry-graphql/strawberry/pull/2790/) 0.180.0 - 2023-05-31 -------------------- This release updates the Django Channels integration so that it uses the same base classes used by all other integrations. **New features:** The Django Channels integration supports two new features: * Setting headers in a response * File uploads via `multipart/form-data` POST requests **Breaking changes:** This release contains a breaking change for the Channels integration. The context object is now a `dict` and it contains different keys depending on the connection protocol: 1. HTTP: `request` and `response`. The `request` object contains the full request (including the body). Previously, `request` was the `GraphQLHTTPConsumer` instance of the current connection. The consumer is now available via `request.consumer`. 2. WebSockets: `request`, `ws` and `response`. `request` and `ws` are the same `GraphQLWSConsumer` instance of the current connection. If you want to use a dataclass for the context object (like in previous releases), you can still use them by overriding the `get_context` methods. See the Channels integration documentation for an example. Contributed by [Christian Dröge](https://github.com/cdroege) via [PR #2775](https://github.com/strawberry-graphql/strawberry/pull/2775/) 0.179.0 - 2023-05-31 -------------------- This PR allows passing metadata to Strawberry arguments. Example: ```python import strawberry @strawberry.type class Query: @strawberry.field def hello( self, info, input: Annotated[str, strawberry.argument(metadata={"test": "foo"})], ) -> str: argument_definition = info.get_argument_definition("input") assert argument_definition.metadata["test"] == "foo" return f"Hi {input}" ``` Contributed by [Jonathan Kim](https://github.com/jkimbo) via [PR #2755](https://github.com/strawberry-graphql/strawberry/pull/2755/) 0.178.3 - 2023-05-31 -------------------- In this release codegen no longer chokes on queries that have a `__typename` in them. Python generated types will not have the `__typename` included in the fields. Contributed by [Matt Gilson](https://github.com/mgilson) via [PR #2797](https://github.com/strawberry-graphql/strawberry/pull/2797/) 0.178.2 - 2023-05-31 -------------------- Prevent AssertionError when using `strawberry codegen` on a query file that contains a mutation. Contributed by [Matt Gilson](https://github.com/mgilson) via [PR #2795](https://github.com/strawberry-graphql/strawberry/pull/2795/) 0.178.1 - 2023-05-30 -------------------- This release fixes a bug in experimental.pydantic whereby `Optional` type annotations weren't exactly aligned between strawberry type and pydantic model. Previously this would have caused the series field to be non-nullable in graphql. ```python from typing import Optional from pydantic import BaseModel, Field import strawberry class VehicleModel(BaseModel): series: Optional[str] = Field(default="") @strawberry.experimental.pydantic.type(model=VehicleModel, all_fields=True) class VehicleModelType: pass ``` Contributed by [Nick Butlin](https://github.com/nicholasbutlin) via [PR #2782](https://github.com/strawberry-graphql/strawberry/pull/2782/) 0.178.0 - 2023-05-22 -------------------- This release introduces the new `should_ignore` argument to the `QueryDepthLimiter` extension that provides a more general and more verbose way of specifying the rules by which a query's depth should be limited. The `should_ignore` argument should be a function that accepts a single argument of type `IgnoreContext`. The `IgnoreContext` class has the following attributes: - `field_name` of type `str`: the name of the field to be compared against - `field_args` of type `strawberry.extensions.query_depth_limiter.FieldArgumentsType`: the arguments of the field to be compared against - `query` of type `graphql.language.Node`: the query string - `context` of type `graphql.validation.ValidationContext`: the context passed to the query and returns `True` if the field should be ignored and `False` otherwise. This argument is injected, regardless of name, by the `QueryDepthLimiter` class and should not be passed by the user. Instead, the user should write business logic to determine whether a field should be ignored or not by the attributes of the `IgnoreContext` class. For example, the following query: ```python """ query { matt: user(name: "matt") { email } andy: user(name: "andy") { email address { city } pets { name owner { name } } } } """ ``` can have its depth limited by the following `should_ignore`: ```python from strawberry.extensions import IgnoreContext def should_ignore(ignore: IgnoreContext): return ignore.field_args.get("name") == "matt" query_depth_limiter = QueryDepthLimiter(should_ignore=should_ignore) ``` so that it *effectively* becomes: ```python """ query { andy: user(name: "andy") { email pets { name owner { name } } } } """ ``` Contributed by [Tommy Smith](https://github.com/tsmith023) via [PR #2505](https://github.com/strawberry-graphql/strawberry/pull/2505/) 0.177.3 - 2023-05-19 -------------------- This release adds a method on the DatadogTracingExtension class called `create_span` that can be overridden to create a custom span or add additional tags to the span. ```python from ddtrace import Span from strawberry.extensions import LifecycleStep from strawberry.extensions.tracing import DatadogTracingExtension class DataDogExtension(DatadogTracingExtension): def create_span( self, lifecycle_step: LifecycleStep, name: str, **kwargs, ) -> Span: span = super().create_span(lifecycle_step, name, **kwargs) if lifecycle_step == LifeCycleStep.OPERATION: span.set_tag("graphql.query", self.execution_context.query) return span ``` Contributed by [Ronald Williams](https://github.com/ronaldnwilliams) via [PR #2773](https://github.com/strawberry-graphql/strawberry/pull/2773/) 0.177.2 - 2023-05-18 -------------------- This release fixes an issue with optional scalars using the `or` notation with forward references on python 3.10. The following code would previously raise `TypeError` on python 3.10: ```python from __future__ import annotations import strawberry from strawberry.scalars import JSON @strawberry.type class SomeType: an_optional_json: JSON | None ``` Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #2774](https://github.com/strawberry-graphql/strawberry/pull/2774/) 0.177.1 - 2023-05-09 -------------------- This release adds support for using `enum_value` with `IntEnum`s, like this: ```python import strawberry from enum import IntEnum @strawberry.enum class Color(IntEnum): OTHER = strawberry.enum_value( -1, description="Other: The color is not red, blue, or green." ) RED = strawberry.enum_value(0, description="Red: The color red.") BLUE = strawberry.enum_value(1, description="Blue: The color blue.") GREEN = strawberry.enum_value(2, description="Green: The color green.") ``` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2761](https://github.com/strawberry-graphql/strawberry/pull/2761/) 0.177.0 - 2023-05-07 -------------------- This release adds a SentryTracingExtension that you can use to automatically add tracing information to your GraphQL queries. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2495](https://github.com/strawberry-graphql/strawberry/pull/2495/) 0.176.4 - 2023-05-07 -------------------- This release adds support for custom classes inside the OpenTelemetry integration. With this, we shouldn't see errors like this anymore: ```Invalid type dict for attribute 'graphql.param.paginator' value. Expected one of ['bool', 'str', 'bytes', 'int', 'float'] or a sequence of those types.``` Contributed by [Budida Abhinav Ramana](https://github.com/abhinavramana) via [PR #2753](https://github.com/strawberry-graphql/strawberry/pull/2753/) 0.176.3 - 2023-05-03 -------------------- Add `get_argument_definition` helper function on the Info object to get a StrawberryArgument definition by argument name from inside a resolver or Field Extension. Example: ```python import strawberry @strawberry.type class Query: @strawberry.field def field( self, info, my_input: Annotated[ str, strawberry.argument(description="Some description"), ], ) -> str: my_input_def = info.get_argument_definition("my_input") assert my_input_def.type is str assert my_input_def.description == "Some description" return my_input ``` Contributed by [Jonathan Kim](https://github.com/jkimbo) via [PR #2732](https://github.com/strawberry-graphql/strawberry/pull/2732/) 0.176.2 - 2023-05-02 -------------------- This release adds more type hints to internal APIs and public APIs. Contributed by [Alex Auritt](https://github.com/alexauritt) via [PR #2568](https://github.com/strawberry-graphql/strawberry/pull/2568/) 0.176.1 - 2023-05-02 -------------------- This release improves the `graphql-transport-ws` implementation by starting the sub-protocol timeout only when the connection handshake is completed. Contributed by [Kristján Valur Jónsson](https://github.com/kristjanvalur) via [PR #2703](https://github.com/strawberry-graphql/strawberry/pull/2703/) 0.176.0 - 2023-05-01 -------------------- This release parses the input arguments to a field earlier so that Field Extensions recieve instances of Input types rather than plain dictionaries. Example: ```python import strawberry from strawberry.extensions import FieldExtension @strawberry.input class MyInput: foo: str class MyFieldExtension(FieldExtension): def resolve( self, next_: Callable[..., Any], source: Any, info: strawberry.Info, **kwargs ): # kwargs["my_input"] is instance of MyInput ... @strawberry.type class Query: @strawberry.field def field(self, my_input: MyInput) -> str: return "hi" ``` Contributed by [Jonathan Kim](https://github.com/jkimbo) via [PR #2731](https://github.com/strawberry-graphql/strawberry/pull/2731/) 0.175.1 - 2023-04-30 -------------------- This release adds a missing parameter to `get_context` when using subscriptions with ASGI. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2739](https://github.com/strawberry-graphql/strawberry/pull/2739/) 0.175.0 - 2023-04-29 -------------------- Do not display graphiql view in fastapi doc if graphiql parameter is deactivated Contributed by [yak-toto](https://github.com/yak-toto) via [PR #2736](https://github.com/strawberry-graphql/strawberry/pull/2736/) 0.174.0 - 2023-04-25 -------------------- This PR adds a MaxTokensLimiter extension which limits the number of tokens in a GraphQL document. ## Usage example: ```python import strawberry from strawberry.extensions import MaxTokensLimiter schema = strawberry.Schema( Query, extensions=[ MaxTokensLimiter(max_token_count=1000), ], ) ``` Contributed by [reka](https://github.com/reka) via [PR #2729](https://github.com/strawberry-graphql/strawberry/pull/2729/) 0.173.1 - 2023-04-25 -------------------- This release bumps the version of typing_extensions to >= `4.0.0` to fix the error: `"cannot import Self from typing_extensions"`. Contributed by [Tien Truong](https://github.com/tienman) via [PR #2704](https://github.com/strawberry-graphql/strawberry/pull/2704/) 0.173.0 - 2023-04-25 -------------------- This releases adds an extension for [PyInstrument](https://github.com/joerick/pyinstrument). It allows to instrument your server and find slow code paths. You can use it like this: ```python import strawberry from strawberry.extensions import pyinstrument schema = strawberry.Schema( Query, extensions=[ pyinstrument.PyInstrument(report_path="pyinstrument.html"), ], ) ``` Contributed by [Peyton Duncan](https://github.com/Helithumper) via [PR #2727](https://github.com/strawberry-graphql/strawberry/pull/2727/) 0.172.0 - 2023-04-24 -------------------- This PR adds a MaxAliasesLimiter extension which limits the number of aliases in a GraphQL document. ## Usage example: ```python import strawberry from strawberry.extensions import MaxAliasesLimiter schema = strawberry.Schema( Query, extensions=[ MaxAliasesLimiter(max_alias_count=15), ], ) ``` Contributed by [reka](https://github.com/reka) via [PR #2726](https://github.com/strawberry-graphql/strawberry/pull/2726/) 0.171.3 - 2023-04-21 -------------------- This release adds missing annotations in class methods, improving our type coverage. Contributed by [Kai Benevento](https://github.com/benesgarage) via [PR #2721](https://github.com/strawberry-graphql/strawberry/pull/2721/) 0.171.2 - 2023-04-21 -------------------- `graphql_transport_ws`: Cancelling a subscription no longer blocks the connection while any subscription finalizers run. Contributed by [Kristján Valur Jónsson](https://github.com/kristjanvalur) via [PR #2718](https://github.com/strawberry-graphql/strawberry/pull/2718/) 0.171.1 - 2023-04-07 -------------------- This release fix the return value of enums when using a custom name converter for them. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2696](https://github.com/strawberry-graphql/strawberry/pull/2696/) 0.171.0 - 2023-04-06 -------------------- This release adds support for Mypy 1.2.0 Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2693](https://github.com/strawberry-graphql/strawberry/pull/2693/) 0.170.0 - 2023-04-06 -------------------- This release add support for converting the enum value names from `NameConverter`. It looks like this: ```python from enum import Enum import strawberry from strawberry.enum import EnumDefinition, EnumValue from strawberry.schema.config import StrawberryConfig from strawberry.schema.name_converter import NameConverter class EnumNameConverter(NameConverter): def from_enum_value(self, enum: EnumDefinition, enum_value: EnumValue) -> str: return f"{super().from_enum_value(enum, enum_value)}_enum_value" @strawberry.enum class MyEnum(Enum): A = "a" B = "b" @strawberry.type class Query: a_enum: MyEnum schema = strawberry.Schema( query=Query, config=StrawberryConfig(name_converter=EnumNameConverter()), ) ``` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2690](https://github.com/strawberry-graphql/strawberry/pull/2690/) 0.169.0 - 2023-04-05 -------------------- This release updates all\* the HTTP integration to use the same base class, which makes it easier to maintain and extend them in future releases. While this doesn't provide any new features (other than settings headers in Chalice and Sanic), it does make it easier to extend the HTTP integrations in the future. So, expect some new features in the next releases! **New features:** Now both Chalice and Sanic integrations support setting headers in the response. Bringing them to the same level as the other HTTP integrations. **Breaking changes:** Unfortunately, this release does contain some breaking changes, but they are minimal and should be quick to fix. 1. Flask `get_root_value` and `get_context` now receive the request 2. Sanic `get_root_value` now receives the request and it is async \* The only exception is the channels http integration, which will be updated in a future release. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2681](https://github.com/strawberry-graphql/strawberry/pull/2681/) 0.168.2 - 2023-04-03 -------------------- Fixes type hint for StrawberryTypeFromPydantic._pydantic_type to be a Type instead of an instance of the Pydantic model. As it is a private API, we still highly discourage using it, but it's now typed correctly. ```python from pydantic import BaseModel from typing import Type, List import strawberry from strawberry.experimental.pydantic.conversion_types import StrawberryTypeFromPydantic class User(BaseModel): name: str @staticmethod def foo() -> List[str]: return ["Patrick", "Pietro", "Pablo"] @strawberry.experimental.pydantic.type(model=User, all_fields=True) class UserType: @strawberry.field def foo(self: StrawberryTypeFromPydantic[User]) -> List[str]: # This is now inferred correctly as Type[User] instead of User # We still highly discourage using this private API, but it's # now typed correctly pydantic_type: Type[User] = self._pydantic_type return pydantic_type.foo() def get_users() -> UserType: user: User = User(name="Patrick") return UserType.from_pydantic(user) @strawberry.type class Query: user: UserType = strawberry.field(resolver=get_users) schema = strawberry.Schema(query=Query) ``` Contributed by [James Chua](https://github.com/thejaminator) via [PR #2683](https://github.com/strawberry-graphql/strawberry/pull/2683/) 0.168.1 - 2023-03-26 -------------------- This releases adds a new `extra` group for Starlite, preventing it from being installed by default. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2664](https://github.com/strawberry-graphql/strawberry/pull/2664/) 0.168.0 - 2023-03-26 -------------------- This release adds support for [starlite](https://starliteproject.dev/). ```python import strawberry from starlite import Request, Starlite from strawberry.starlite import make_graphql_controller from strawberry.types.info import Info def custom_context_getter(request: Request): return {"custom": "context"} @strawberry.type class Query: @strawberry.field def hello(self, info: strawberry.Info[object, None]) -> str: return info.context["custom"] schema = strawberry.Schema(Query) GraphQLController = make_graphql_controller( schema, path="/graphql", context_getter=custom_context_getter, ) app = Starlite( route_handlers=[GraphQLController], ) ``` Contributed by [Matthieu MN](https://github.com/gazorby) via [PR #2391](https://github.com/strawberry-graphql/strawberry/pull/2391/) 0.167.1 - 2023-03-26 -------------------- This release fixes and issue where you'd get a warning about using Apollo Federation directives even when using `strawberry.federation.Schema`. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2661](https://github.com/strawberry-graphql/strawberry/pull/2661/) 0.167.0 - 2023-03-25 -------------------- This releases adds more type annotations for public functions and methods. No new changes have been added to the API. Contributed by [Jad Haddad](https://github.com/JadHADDAD92) via [PR #2627](https://github.com/strawberry-graphql/strawberry/pull/2627/) 0.166.0 - 2023-03-25 -------------------- This release adds a warning when using `@strawberry.federation.type` but not using `strawberry.federation.Schema` Contributed by [Rubens O Leão](https://github.com/rubensoleao) via [PR #2572](https://github.com/strawberry-graphql/strawberry/pull/2572/) 0.165.1 - 2023-03-21 -------------------- Updates the `MaskErrors` extension to the new extension API, which was missed previously. Contributed by [Nikolai Maas](https://github.com/N-Maas) via [PR #2655](https://github.com/strawberry-graphql/strawberry/pull/2655/) 0.165.0 - 2023-03-18 -------------------- Add full support for forward references, specially when using `from __future__ import annotations`. Before the following would fail on python versions older than 3.10: ```python from __future__ import annotations import strawberry @strawberry.type class Query: foo: str | None ``` Also, this would fail in any python versions: ```python from __future__ import annotations from typing import Annotated import strawberry @strawberry.type class Query: foo: Annotated[str, "some annotation"] ``` Now both of these cases are supported. Please open an issue if you find any edge cases that are still not supported. Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #2592](https://github.com/strawberry-graphql/strawberry/pull/2592/) 0.164.1 - 2023-03-18 -------------------- Fix interface duplication leading to schema compilation error in multiple inheritance scenarios (i.e. "Diamond Problem" inheritance) Thank you @mzhu22 for the thorough bug report! Contributed by [San Kilkis](https://github.com/skilkis) via [PR #2647](https://github.com/strawberry-graphql/strawberry/pull/2647/) 0.164.0 - 2023-03-14 -------------------- This release introduces a breaking change to make pydantic default behavior consistent with normal strawberry types. This changes the schema generated for pydantic types, that are required, and have default values. Previously pydantic type with a default, would get converted to a strawberry type that is not required. This is now fixed, and the schema will now correctly show the type as required. ```python import pydantic import strawberry class UserPydantic(pydantic.BaseModel): name: str = "James" @strawberry.experimental.pydantic.type(UserPydantic, all_fields=True) class User: ... @strawberry.type class Query: a: User = strawberry.field() @strawberry.field def a(self) -> User: return User() ``` The schema is now ``` type Query { a: User! } type User { name: String! // String! rather than String previously } ``` Contributed by [James Chua](https://github.com/thejaminator) via [PR #2623](https://github.com/strawberry-graphql/strawberry/pull/2623/) 0.163.2 - 2023-03-14 -------------------- This release covers an edge case where the following would not give a nice error. ```python some_field: "Union[list[str], SomeType]]" ``` Fixes [#2591](https://github.com/strawberry-graphql/strawberry/issues/2591) Contributed by [ניר](https://github.com/nrbnlulu) via [PR #2593](https://github.com/strawberry-graphql/strawberry/pull/2593/) 0.163.1 - 2023-03-14 -------------------- Provide close reason to ASGI websocket as specified by ASGI 2.3 Contributed by [Kristján Valur Jónsson](https://github.com/kristjanvalur) via [PR #2639](https://github.com/strawberry-graphql/strawberry/pull/2639/) 0.163.0 - 2023-03-13 -------------------- This release adds support for list arguments in operation directives. The following is now supported: ```python @strawberry.directive(locations=[DirectiveLocation.FIELD]) def append_names( value: DirectiveValue[str], names: List[str] ): # note the usage of List here return f"{value} {', '.join(names)}" ``` Contributed by [chenyijian](https://github.com/hot123s) via [PR #2632](https://github.com/strawberry-graphql/strawberry/pull/2632/) 0.162.0 - 2023-03-10 -------------------- Adds support for a custom field using the approach specified in issue [#2168](abc). Field Extensions may be used to change the way how fields work and what they return. Use cases might include pagination, permissions or other behavior modifications. ```python from strawberry.extensions import FieldExtension class UpperCaseExtension(FieldExtension): async def resolve_async( self, next: Callable[..., Awaitable[Any]], source: Any, info: strawberry.Info, **kwargs ): result = await next(source, info, **kwargs) return str(result).upper() @strawberry.type class Query: @strawberry.field(extensions=[UpperCaseExtension()]) async def string(self) -> str: return "This is a test!!" ``` ```graphql query { string } ``` ```json { "string": "THIS IS A TEST!!" } ``` Contributed by [Erik Wrede](https://github.com/erikwrede) via [PR #2567](https://github.com/strawberry-graphql/strawberry/pull/2567/) 0.161.1 - 2023-03-09 -------------------- Ensure that no other messages follow a "complete" or "error" message for an operation in the graphql-transport-ws protocol. Contributed by [Kristján Valur Jónsson](https://github.com/kristjanvalur) via [PR #2600](https://github.com/strawberry-graphql/strawberry/pull/2600/) 0.161.0 - 2023-03-08 -------------------- Calling `ChannelsConsumer.channel_listen` multiple times will now pass along the messages being listened for to multiple callers, rather than only one of the callers, which was the old behaviour. This resolves an issue where creating multiple GraphQL subscriptions using a single websocket connection could result in only one of those subscriptions (in a non-deterministic order) being triggered if they are listening for channel layer messages of the same type. Contributed by [James Thorniley](https://github.com/jthorniley) via [PR #2525](https://github.com/strawberry-graphql/strawberry/pull/2525/) 0.160.0 - 2023-03-08 -------------------- Rename `Extension` to `SchemaExtension` to pave the way for FieldExtensions. Importing `Extension` from `strawberry.extensions` will now raise a deprecation warning. Before: ```python from strawberry.extensions import Extension ``` After: ```python from strawberry.extensions import SchemaExtension ``` Contributed by [Jonathan Kim](https://github.com/jkimbo) via [PR #2574](https://github.com/strawberry-graphql/strawberry/pull/2574/) 0.159.1 - 2023-03-07 -------------------- This releases adds support for Mypy 1.1.1 Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2616](https://github.com/strawberry-graphql/strawberry/pull/2616/) 0.159.0 - 2023-02-22 -------------------- This release changes how extension hooks are defined. The new style hooks are more flexible and allow to run code before and after the execution. The old style hooks are still supported but will be removed in future releases. **Before:** ```python def on_executing_start(self): # Called before the execution start ... def on_executing_end(self): # Called after the execution ends ... ``` **After** ```python def on_execute(self): # This part is called before the execution start yield # This part is called after the execution ends ``` Contributed by [ניר](https://github.com/nrbnlulu) via [PR #2428](https://github.com/strawberry-graphql/strawberry/pull/2428/) 0.158.2 - 2023-02-21 -------------------- Add a type annotation to `strawberry.fastapi.BaseContext`'s `__init__` method so that it can be used without `mypy` raising an error. Contributed by [Martin Winkel](https://github.com/SaturnFromTitan) via [PR #2581](https://github.com/strawberry-graphql/strawberry/pull/2581/) 0.158.1 - 2023-02-19 -------------------- Version 1.5.10 of GraphiQL disabled introspection for deprecated arguments because it wasn't supported by all GraphQL server versions. This PR enables it so that deprecated arguments show up again in GraphiQL. Contributed by [Jonathan Kim](https://github.com/jkimbo) via [PR #2575](https://github.com/strawberry-graphql/strawberry/pull/2575/) 0.158.0 - 2023-02-18 -------------------- Throw proper exceptions when Unions are created with invalid types Previously, using Lazy types inside of Unions would raise unexpected, unhelpful errors. Contributed by [ignormies](https://github.com/BryceBeagle) via [PR #2540](https://github.com/strawberry-graphql/strawberry/pull/2540/) 0.157.0 - 2023-02-18 -------------------- This releases adds support for Apollo Federation 2.1, 2.2 and 2.3. This includes support for `@composeDirective` and `@interfaceObject`, we expose directives for both, but we also have shortcuts, for example to use `@composeDirective` with a custom schema directive, you can do the following: ```python @strawberry.federation.schema_directive( locations=[Location.OBJECT], name="cacheControl", compose=True ) class CacheControl: max_age: int ``` The `compose=True` makes so that this directive is included in the supergraph schema. For `@interfaceObject` we introduced a new `@strawberry.federation.interface_object` decorator. This works like `@strawberry.federation.type`, but it adds, the appropriate directive, for example: ```python @strawberry.federation.interface_object(keys=["id"]) class SomeInterface: id: strawberry.ID ``` generates the following type: ```graphql type SomeInterface @key(fields: "id") @interfaceObject { id: ID! } ``` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2549](https://github.com/strawberry-graphql/strawberry/pull/2549/) 0.156.4 - 2023-02-13 -------------------- This release fixes a regression introduce in version 0.156.2 that would make Mypy throw an error in the following code: ```python import strawberry @strawberry.type class Author: name: str @strawberry.type class Query: @strawberry.field async def get_authors(self) -> list[Author]: return [Author(name="Michael Crichton")] ``` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2535](https://github.com/strawberry-graphql/strawberry/pull/2535/) 0.156.3 - 2023-02-10 -------------------- This release adds support for Mypy 1.0 Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2516](https://github.com/strawberry-graphql/strawberry/pull/2516/) 0.156.2 - 2023-02-09 -------------------- This release updates the typing for the resolver argument in `strawberry.field`i to support async resolvers. This means that now you won't get any type error from Pyright when using async resolver, like the following example: ```python import strawberry async def get_user_age() -> int: return 0 @strawberry.type class User: name: str age: int = strawberry.field(resolver=get_user_age) ``` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2528](https://github.com/strawberry-graphql/strawberry/pull/2528/) 0.156.1 - 2023-02-09 -------------------- Add `GraphQLWebsocketCommunicator` for testing websockets on channels. i.e: ```python import pytest from strawberry.channels.testing import GraphQLWebsocketCommunicator from myapp.asgi import application @pytest.fixture async def gql_communicator(): async with GraphQLWebsocketCommunicator( application=application, path="/graphql" ) as client: yield client async def test_subscribe_echo(gql_communicator): async for res in gql_communicator.subscribe( query='subscription { echo(message: "Hi") }' ): assert res.data == {"echo": "Hi"} ``` Contributed by [ניר](https://github.com/nrbnlulu) via [PR #2458](https://github.com/strawberry-graphql/strawberry/pull/2458/) 0.156.0 - 2023-02-08 -------------------- This release adds support for specialized generic types. Before, the following code would give an error, saying that `T` was not provided to the generic type: ```python @strawberry.type class Foo(Generic[T]): some_var: T @strawberry.type class IntFoo(Foo[int]): ... @strawberry.type class Query: int_foo: IntFoo ``` Also, because the type is already specialized, `Int` won't get inserted to its name, meaning it will be exported to the schema with a type name of `IntFoo` and not `IntIntFoo`. For example, this query: ```python @strawberry.type class Query: int_foo: IntFoo str_foo: Foo[str] ``` Will generate a schema like this: ```graphql type IntFoo { someVar: Int! } type StrFoo { someVar: String! } type Query { intFoo: IntFoo! strfoo: StrFoo! } ``` Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #2517](https://github.com/strawberry-graphql/strawberry/pull/2517/) 0.155.4 - 2023-02-06 -------------------- Fix file not found error when exporting schema with lazy types from CLI #2469 Contributed by [San Kilkis](https://github.com/skilkis) via [PR #2512](https://github.com/strawberry-graphql/strawberry/pull/2512/) 0.155.3 - 2023-02-01 -------------------- Fix missing custom `resolve_reference` for using pydantic with federation i.e: ```python import typing from pydantic import BaseModel import strawberry from strawberry.federation.schema_directives import Key class ProductInDb(BaseModel): upc: str name: str @strawberry.experimental.pydantic.type( model=ProductInDb, directives=[Key(fields="upc", resolvable=True)] ) class Product: upc: str name: str @classmethod def resolve_reference(cls, upc): return Product(upc=upc, name="") ``` Contributed by [filwaline](https://github.com/filwaline) via [PR #2503](https://github.com/strawberry-graphql/strawberry/pull/2503/) 0.155.2 - 2023-01-25 -------------------- This release fixes a bug in subscriptions using the graphql-transport-ws protocol where the conversion of the NextMessage object to a dictionary took an unnecessary amount of time leading to an increase in CPU usage. Contributed by [rjwills28](https://github.com/rjwills28) via [PR #2481](https://github.com/strawberry-graphql/strawberry/pull/2481/) 0.155.1 - 2023-01-24 -------------------- A link to the changelog has been added to the package metadata, so it shows up on PyPI. Contributed by [Tom Most](https://github.com/twm) via [PR #2490](https://github.com/strawberry-graphql/strawberry/pull/2490/) 0.155.0 - 2023-01-23 -------------------- This release adds a new utility function to convert a Strawberry object to a dictionary. You can use `strawberry.asdict(...)` function to convert a Strawberry object to a dictionary: ```python @strawberry.type class User: name: str age: int # should be {"name": "Lorem", "age": 25} user_dict = strawberry.asdict(User(name="Lorem", age=25)) ``` > Note: This function uses the `dataclasses.asdict` function under the hood, so > you can safely replace `dataclasses.asdict` with `strawberry.asdict` in your > code. This will make it easier to update your code to newer versions of > Strawberry if we decide to change the implementation. Contributed by [Haze Lee](https://github.com/Hazealign) via [PR #2417](https://github.com/strawberry-graphql/strawberry/pull/2417/) 0.154.1 - 2023-01-17 -------------------- Fix `DuplicatedTypeName` exception being raised on generics declared using `strawberry.lazy`. Previously the following would raise: ```python # issue_2397.py from typing import Annotated, Generic, TypeVar import strawberry T = TypeVar("T") @strawberry.type class Item: name: str @strawberry.type class Edge(Generic[T]): node: T @strawberry.type class Query: edges_normal: Edge[Item] edges_lazy: Edge[Annotated["Item", strawberry.lazy("issue_2397")]] if __name__ == "__main__": schema = strawberry.Schema(query=Query) ``` Contributed by [pre-commit-ci](https://github.com/pre-commit-ci) via [PR #2462](https://github.com/strawberry-graphql/strawberry/pull/2462/) 0.154.0 - 2023-01-13 -------------------- Support constrained float field types in Pydantic models. i.e. ```python import pydantic class Model(pydantic.BaseModel): field: pydantic.confloat(le=100.0) equivalent_field: float = pydantic.Field(le=100.0) ``` Contributed by [Etienne Wodey](https://github.com/airwoodix) via [PR #2455](https://github.com/strawberry-graphql/strawberry/pull/2455/) 0.153.0 - 2023-01-13 -------------------- This change allows clients to define connectionParams when making Subscription requests similar to the way [Apollo-Server](https://www.apollographql.com/docs/apollo-server/data/subscriptions/#operation-context) does it. With [Apollo-Client (React)](https://www.apollographql.com/docs/react/data/subscriptions/#5-authenticate-over-websocket-optional) as an example, define a Websocket Link: ``` import { GraphQLWsLink } from '@apollo/client/link/subscriptions'; import { createClient } from 'graphql-ws'; const wsLink = new GraphQLWsLink(createClient({ url: 'ws://localhost:4000/subscriptions', connectionParams: { authToken: user.authToken, }, })); ``` and the JSON passed to `connectionParams` here will appear within Strawberry's context as the `connection_params` attribute when accessing `info.context` within a Subscription resolver. Contributed by [Tommy Smith](https://github.com/tsmith023) via [PR #2380](https://github.com/strawberry-graphql/strawberry/pull/2380/) 0.152.0 - 2023-01-10 -------------------- This release adds support for updating (or adding) the query document inside an extension's `on_request_start` method. This can be useful for implementing persisted queries. The old behavior of returning a 400 error if no query is present in the request is still supported. Example usage: ```python from strawberry.extensions import Extension def get_doc_id(request) -> str: """Implement this to get the document ID using your framework's request object""" ... def load_persisted_query(doc_id: str) -> str: """Implement this load a query by document ID. For example, from a database.""" ... class PersistedQuery(Extension): def on_request_start(self): request = self.execution_context.context.request doc_id = get_doc_id(request) self.execution_context.query = load_persisted_query(doc_id) ``` Contributed by [James Thorniley](https://github.com/jthorniley) via [PR #2431](https://github.com/strawberry-graphql/strawberry/pull/2431/) 0.151.3 - 2023-01-09 -------------------- This release adds support for FastAPI 0.89.0 Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2440](https://github.com/strawberry-graphql/strawberry/pull/2440/) 0.151.2 - 2022-12-23 -------------------- This release fixes `@strawberry.experimental.pydantic.type` and adds support for the metadata attribute on fields. Example: ```python @strawberry.experimental.pydantic.type(model=User) class UserType: private: strawberry.auto = strawberry.field(metadata={"admin_only": True}) public: strawberry.auto ``` Contributed by [Huy Z](https://github.com/huyz) via [PR #2415](https://github.com/strawberry-graphql/strawberry/pull/2415/) 0.151.1 - 2022-12-20 -------------------- This release fixes an issue that prevented using generic that had a field of type enum. The following works now: ```python @strawberry.enum class EstimatedValueEnum(Enum): test = "test" testtest = "testtest" @strawberry.type class EstimatedValue(Generic[T]): value: T type: EstimatedValueEnum @strawberry.type class Query: @strawberry.field def estimated_value(self) -> Optional[EstimatedValue[int]]: return EstimatedValue(value=1, type=EstimatedValueEnum.test) ``` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2411](https://github.com/strawberry-graphql/strawberry/pull/2411/) 0.151.0 - 2022-12-13 -------------------- This PR adds a new `graphql_type` parameter to strawberry.field that allows you to explicitly set the field type. This parameter will take preference over the resolver return type and the class field type. For example: ```python @strawberry.type class Query: a: float = strawberry.field(graphql_type=str) b = strawberry.field(graphql_type=int) @strawberry.field(graphql_type=float) def c(self) -> str: return "3.4" schema = strawberry.Schema(Query) str( schema ) == """ type Query { a: String! b: Int! c: Float! } """ ``` Contributed by [Jonathan Kim](https://github.com/jkimbo) via [PR #2313](https://github.com/strawberry-graphql/strawberry/pull/2313/) 0.150.1 - 2022-12-13 -------------------- Fixed field resolvers with nested generic return types (e.g. `list`, `Optional`, `Union` etc) raising TypeErrors. This means resolver factory methods can now be correctly type hinted. For example the below would previously error unless you ommited all the type hints on `resolver_factory` and `actual_resolver` functions. ```python from typing import Callable, Optional, Type, TypeVar import strawberry @strawberry.type class Cat: name: str T = TypeVar("T") def resolver_factory(type_: Type[T]) -> Callable[[], Optional[T]]: def actual_resolver() -> Optional[T]: # load rows from database and cast to type etc ... return actual_resolver @strawberry.type class Query: cat: Cat = strawberry.field(resolver_factory(Cat)) schema = strawberry.Schema(query=Query) ``` Contributed by [Tim OSullivan](https://github.com/invokermain) via [PR #1900](https://github.com/strawberry-graphql/strawberry/pull/1900/) 0.150.0 - 2022-12-13 -------------------- This release implements the ability to use custom caching for dataloaders. It also allows to provide a `cache_key_fn` to the dataloader. This function is used to generate the cache key for the dataloader. This is useful when you want to use a custom hashing function for the cache key. Contributed by [Aman Choudhary](https://github.com/Techno-Tut) via [PR #2394](https://github.com/strawberry-graphql/strawberry/pull/2394/) 0.149.2 - 2022-12-09 -------------------- This release fixes support for generics in arguments, see the following example: ```python T = TypeVar("T") @strawberry.type class Node(Generic[T]): @strawberry.field def data(self, arg: T) -> T: # `arg` is also generic return arg ``` Contributed by [A. Coady](https://github.com/coady) via [PR #2316](https://github.com/strawberry-graphql/strawberry/pull/2316/) 0.149.1 - 2022-12-09 -------------------- This release improves the performance of rich exceptions on custom scalars by changing how frames are fetched from the call stack. Before the change, custom scalars were using a CPU intensive call to the `inspect` module to fetch frame info which could lead to serious CPU spikes. Contributed by [Paulo Amaral](https://github.com/paulopaixaoamaral) via [PR #2390](https://github.com/strawberry-graphql/strawberry/pull/2390/) 0.149.0 - 2022-12-09 -------------------- This release does some internal refactoring of the HTTP views, hopefully it doesn't affect anyone. It mostly changes the status codes returned in case of errors (e.g. bad JSON, missing queries and so on). It also improves the testing, and adds an entirely new test suite for the HTTP views, this means in future we'll be able to keep all the HTTP views in sync feature-wise. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #1840](https://github.com/strawberry-graphql/strawberry/pull/1840/) 0.148.0 - 2022-12-08 -------------------- This release changes the `get_context`, `get_root_value` and `process_result` methods of the Flask async view to be async functions. This allows you to use async code in these methods. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2388](https://github.com/strawberry-graphql/strawberry/pull/2388/) 0.147.0 - 2022-12-08 -------------------- This release introduces a `encode_json` method on all the HTTP integrations. This method allows to customize the encoding of the JSON response. By default we use `json.dumps` but you can override this method to use a different encoder. It also deprecates `json_encoder` and `json_dumps_params` in the Django and Sanic views, `encode_json` should be used instead. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2272](https://github.com/strawberry-graphql/strawberry/pull/2272/) 0.146.0 - 2022-12-05 -------------------- This release updates the Sanic integration and includes some breaking changes. You might need to update your code if you are customizing `get_context` or `process_result` ## `get_context` `get_context` now receives the request as the first argument and the response as the second argument. ## `process_result` `process_result` is now async and receives the request and the GraphQL execution result. This change is needed to align all the HTTP integrations and reduce the amount of code needed to maintain. It also makes the errors consistent with other integrations. It also brings a **new feature** and it allows to customize the HTTP status code by using `info.context["response"].status_code = YOUR_CODE`. It also removes the upper bound on the Sanic version, so you can use the latest version of Sanic with Strawberry. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2273](https://github.com/strawberry-graphql/strawberry/pull/2273/) 0.145.0 - 2022-12-04 -------------------- This release introduced improved errors! Now, when you have a syntax error in your code, you'll get a nice error message with a line number and a pointer to the exact location of the error. ✨ This is a huge improvement over the previous behavior, which was providing a stack trace with no clear indication of where the error was. 🙈 You can enable rich errors by installing Strawberry with the `cli` extra: ```bash pip install "strawberry-graphql[cli]" ``` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2027](https://github.com/strawberry-graphql/strawberry/pull/2027/) 0.144.3 - 2022-12-04 -------------------- This release fixes an issue with type duplication of generics. You can now use a lazy type with a generic even if the original type was already used with that generic in the schema. Example: ```python @strawberry.type class Query: regular: Edge[User] lazy: Edge[Annotated["User", strawberry.lazy(".user")]] ``` Contributed by [Dmitry Semenov](https://github.com/lonelyteapot) via [PR #2381](https://github.com/strawberry-graphql/strawberry/pull/2381/) 0.144.2 - 2022-12-02 -------------------- Generic types are now allowed in the schema's extra types. ```python T = TypeVar("T") @strawberry.type class Node(Generic[T]): field: T @strawberry.type class Query: name: str schema = strawberry.Schema(Query, types=[Node[int]]) ``` Contributed by [A. Coady](https://github.com/coady) via [PR #2294](https://github.com/strawberry-graphql/strawberry/pull/2294/) 0.144.1 - 2022-12-02 -------------------- This release fixes a regression that prevented Generic types from being used multiple types. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2378](https://github.com/strawberry-graphql/strawberry/pull/2378/) 0.144.0 - 2022-12-02 -------------------- Added extra validation that types used in a schema are unique. Strawberry starts to throw an exception `DuplicatedTypeName` when two types defined in a schema have the same name. Contributed by [Bartosz Polnik](https://github.com/bartekbp) via [PR #2356](https://github.com/strawberry-graphql/strawberry/pull/2356/) 0.143.0 - 2022-12-01 -------------------- Added an error to be used when overriding GraphQLError in custom extensions and added a guide on how to use it. Exposing GraphQLError from the strawberry namespace brings a better experience and will be useful in the future (when we move to something else). Contributed by [Niten Nashiki](https://github.com/nnashiki) via [PR #2360](https://github.com/strawberry-graphql/strawberry/pull/2360/) 0.142.3 - 2022-11-29 -------------------- This release updates GraphiQL to 2.2.0 and fixes an issue with the websocket URL being incorrectly set when navigating to GraphiQL with an URL with a hash. Contributed by [Shen Li](https://github.com/ericls) via [PR #2363](https://github.com/strawberry-graphql/strawberry/pull/2363/) 0.142.2 - 2022-11-15 -------------------- This release changes the dataloader batch resolution to avoid resolving futures that were canceled, and also from reusing them from the cache. Trying to resolve a future that was canceled would raise `asyncio.InvalidStateError` Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #2339](https://github.com/strawberry-graphql/strawberry/pull/2339/) 0.142.1 - 2022-11-11 -------------------- This release fixes a bug where using a custom scalar in a union would result in an unclear exception. Instead, when using a custom scalar in a union, the `InvalidUnionType` exception is raised with a clear message that you cannot use that type in a union. Contributed by [Jonathan Kim](https://github.com/jkimbo) via [PR #2336](https://github.com/strawberry-graphql/strawberry/pull/2336/) 0.142.0 - 2022-11-11 -------------------- This release adds support for `typing.Self` and `typing_extensions.Self` for types and interfaces. ```python from typing_extensions import Self @strawberry.type class Node: @strawberry.field def field(self) -> Self: return self ``` Contributed by [A. Coady](https://github.com/coady) via [PR #2295](https://github.com/strawberry-graphql/strawberry/pull/2295/) 0.141.0 - 2022-11-10 -------------------- This release adds support for an implicit `resolve_reference` method on Federation type. This method will automatically create a Strawberry instance for a federation type based on the input data received, for example, the following: ```python @strawberry.federation.type(keys=["id"]) class Something: id: str @strawberry.federation.type(keys=["upc"]) class Product: upc: str something: Something @staticmethod def resolve_reference(**data): return Product(upc=data["upc"], something=Something(id=data["something_id"])) ``` doesn't need the resolve_reference method anymore. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2332](https://github.com/strawberry-graphql/strawberry/pull/2332/) 0.140.3 - 2022-11-09 -------------------- [Internal] Update StrawberryField so that `type_annotation` is always an instance of StrawberryAnnotation. Contributed by [Jonathan Kim](https://github.com/jkimbo) via [PR #2319](https://github.com/strawberry-graphql/strawberry/pull/2319/) 0.140.2 - 2022-11-08 -------------------- This release fixes an issue that prevented using enums that were using strawberry.enum_value, like the following example: ```python from enum import Enum import strawberry @strawberry.enum class TestEnum(Enum): A = strawberry.enum_value("A") B = "B" @strawberry.type class Query: @strawberry.field def receive_enum(self, test: TestEnum) -> int: return 0 schema = strawberry.Schema(query=Query) ``` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2306](https://github.com/strawberry-graphql/strawberry/pull/2306/) 0.140.1 - 2022-11-08 -------------------- This release adds logging back for parsing and validation errors that was accidentally removed in v0.135.0. Contributed by [Jonathan Kim](https://github.com/jkimbo) via [PR #2323](https://github.com/strawberry-graphql/strawberry/pull/2323/) 0.140.0 - 2022-11-07 -------------------- This release allows to disable operation logging when running the debug server. ``` strawberry server demo --log-operations False ``` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2310](https://github.com/strawberry-graphql/strawberry/pull/2310/) 0.139.0 - 2022-11-04 -------------------- This release changes the type resolution priority to prefer the field annotation over the resolver return type. ```python def my_resolver() -> str: return "1.33" @strawberry.type class Query: a: float = strawberry.field(resolver=my_resolver) schema = strawberry.Schema(Query) # Before: str( schema ) == """ type Query { a: String! } """ # After: str( schema ) == """ type Query { a: Float! } """ ``` Contributed by [Jonathan Kim](https://github.com/jkimbo) via [PR #2312](https://github.com/strawberry-graphql/strawberry/pull/2312/) 0.138.2 - 2022-11-04 -------------------- Fix Pydantic integration for Python 3.10.0 (which was missing the `kw_only` parameter for `dataclasses.make_dataclass()`). Contributed by [Jonathan Kim](https://github.com/jkimbo) via [PR #2309](https://github.com/strawberry-graphql/strawberry/pull/2309/) 0.138.1 - 2022-10-31 -------------------- This release changes an internal implementation for FastAPI's GraphQL router. This should reduce overhead when using the context, and it shouldn't affect your code. Contributed by [Kristján Valur Jónsson](https://github.com/kristjanvalur) via [PR #2278](https://github.com/strawberry-graphql/strawberry/pull/2278/) 0.138.0 - 2022-10-31 -------------------- This release adds support for generic in arguments, see the following example: ```python T = TypeVar("T") @strawberry.type class Node(Generic[T]): @strawberry.field def data(self, arg: T) -> T: # `arg` is also generic return arg ``` Contributed by [A. Coady](https://github.com/coady) via [PR #2293](https://github.com/strawberry-graphql/strawberry/pull/2293/) 0.137.1 - 2022-10-24 -------------------- Allowed `CustomScalar | None` syntax for python >= 3.10. Contributed by [Guillaume Andreu Sabater](https://github.com/g-as) via [PR #2279](https://github.com/strawberry-graphql/strawberry/pull/2279/) 0.137.0 - 2022-10-21 -------------------- This release fixes errors when using Union-of-lazy-types Contributed by [Paulo Costa](https://github.com/paulo-raca) via [PR #2271](https://github.com/strawberry-graphql/strawberry/pull/2271/) 0.136.0 - 2022-10-21 -------------------- This release refactors the chalice integration in order to keep it consistent with the other integrations. ## Deprecation: Passing `render_graphiql` is now deprecated, please use `graphiql` instead. ## New features: - You can now return a custom status by using `info.context["response"].status_code = 418` - You can enabled/disable queries via get using `allow_queries_via_get` (defaults to `True`) ## Changes: Trying to access /graphql via a browser and with `graphiql` set to `False` will return a 404. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2266](https://github.com/strawberry-graphql/strawberry/pull/2266/) 0.135.0 - 2022-10-21 -------------------- This release adds a new `MaskErrors` extension that can be used to hide error messages from the client to prevent exposing sensitive details. By default it masks all errors raised in any field resolver. ```python import strawberry from strawberry.extensions import MaskErrors schema = strawberry.Schema( Query, extensions=[ MaskErrors(), ], ) ``` Contributed by [Jonathan Kim](https://github.com/jkimbo) via [PR #2248](https://github.com/strawberry-graphql/strawberry/pull/2248/) 0.134.5 - 2022-10-20 -------------------- This release improves the error message that you get when trying to use an enum that hasn't been decorated with `@strawberry.enum` inside a type's field. Contributed by [Rise Riyo](https://github.com/riseriyo) via [PR #2267](https://github.com/strawberry-graphql/strawberry/pull/2267/) 0.134.4 - 2022-10-20 -------------------- This release adds support for printing schema directives on an input type object, for example the following schema: ```python @strawberry.schema_directive(locations=[Location.INPUT_FIELD_DEFINITION]) class RangeInput: min: int max: int @strawberry.input class CreateUserInput: name: str age: int = strawberry.field(directives=[RangeInput(min=1, max=100)]) ``` prints the following: ```graphql directive @rangeInput(min: Int!, max: Int!) on INPUT_FIELD_DEFINITION input Input @sensitiveInput(reason: "GDPR") { firstName: String! age: Int! @rangeInput(min: 1, max: 100) } ``` Contributed by [Etty](https://github.com/estyxx) via [PR #2233](https://github.com/strawberry-graphql/strawberry/pull/2233/) 0.134.3 - 2022-10-16 -------------------- This release fixes an issue that prevented using strawberry.lazy with relative paths. The following should work now: ```python @strawberry.type class TypeA: b: Annotated["TypeB", strawberry.lazy(".type_b")] ``` Contributed by [Paulo Costa](https://github.com/paulo-raca) via [PR #2244](https://github.com/strawberry-graphql/strawberry/pull/2244/) 0.134.2 - 2022-10-16 -------------------- This release adds pyupgrade to our CI and includes some minor changes to keep our codebase modern. Contributed by [Liel Fridman](https://github.com/lielfr) via [PR #2255](https://github.com/strawberry-graphql/strawberry/pull/2255/) 0.134.1 - 2022-10-14 -------------------- This release fixes an issue that prevented using lazy types inside generic types. The following is now allowed: ```python T = TypeVar("T") TypeAType = Annotated["TypeA", strawberry.lazy("tests.schema.test_lazy.type_a")] @strawberry.type class Edge(Generic[T]): node: T @strawberry.type class Query: users: Edge[TypeAType] ``` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2254](https://github.com/strawberry-graphql/strawberry/pull/2254/) 0.134.0 - 2022-10-14 -------------------- These release allow you to define a different `url` in the `GraphQLTestClient`, the default is "/graphql/". Here's an example with Starlette client: ```python import pytest from starlette.testclient import TestClient from strawberry.asgi.test import GraphQLTestClient @pytest.fixture def graphql_client() -> GraphQLTestClient: return GraphQLTestClient( TestClient(app, base_url="http://localhost:8000"), url="/api/" ) ``` Contributed by [Etty](https://github.com/estyxx) via [PR #2238](https://github.com/strawberry-graphql/strawberry/pull/2238/) 0.133.7 - 2022-10-14 -------------------- This release fixes a type issue when passing `scalar_overrides` to `strawberry.Schema` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2251](https://github.com/strawberry-graphql/strawberry/pull/2251/) 0.133.6 - 2022-10-13 -------------------- Fix support for arguments where `arg.type=LazyType["EnumType"]` Contributed by [Paulo Costa](https://github.com/paulo-raca) via [PR #2245](https://github.com/strawberry-graphql/strawberry/pull/2245/) 0.133.5 - 2022-10-03 -------------------- Updated `unset` import, from `strawberry.arguments` to `strawberry.unset` in codebase. This will prevent strawberry from triggering its own warning on deprecated imports. Contributed by [dependabot](https://github.com/dependabot) via [PR #2219](https://github.com/strawberry-graphql/strawberry/pull/2219/) 0.133.4 - 2022-10-03 -------------------- This release fixes the type of strawberry.federation.field, this will prevent errors from mypy and pyright when doing the following: ```python @strawberry.federation.type(keys=["id"]) class Location: id: strawberry.ID # the following field was reporting an error in mypy and pylance celestial_body: CelestialBody = strawberry.federation.field( resolver=resolve_celestial_body ) ``` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2222](https://github.com/strawberry-graphql/strawberry/pull/2222/) 0.133.3 - 2022-10-03 -------------------- This release allows to create a federation schema without having to pass a `Query` type. This is useful when your schema only extends some types without adding any additional root field. ```python @strawberry.federation.type(keys=["id"]) class Location: id: strawberry.ID name: str = strawberry.federation.field(override="start") schema = strawberry.federation.Schema(types=[Location]) ``` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2220](https://github.com/strawberry-graphql/strawberry/pull/2220/) 0.133.2 - 2022-09-30 -------------------- This release fixes an issue with `strawberry.federation.field` that prevented instantiating field when passing a resolver function. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2218](https://github.com/strawberry-graphql/strawberry/pull/2218/) 0.133.1 - 2022-09-28 -------------------- This release fixes an issue that prevented using `strawberry.field` with `UNSET` as the default value. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2128](https://github.com/strawberry-graphql/strawberry/pull/2128/) 0.133.0 - 2022-09-27 -------------------- Reduce the number of required dependencies, by marking Pygments and python-multipart as optional. These dependencies are still necessary for some functionality, and so users of that functionality need to ensure they're installed, either explicitly or via an extra: - Pygments is still necessary when using Strawberry in debug mode, and is included in the `strawberry-graphql[debug-server]` extra. - python-multipart is still necessary when using `strawberry.file_uploads.Upload` with FastAPI or Starlette, and is included in the `strawberry-graphql[fastapi]` and `strawberry-graphql[asgi]` extras, respectively. There is now also the `strawberry-graphql[cli]` extra to support commands like `strawberry codegen` and `strawberry export-schema`. Contributed by [Huon Wilson](https://github.com/huonw) via [PR #2205](https://github.com/strawberry-graphql/strawberry/pull/2205/) 0.132.1 - 2022-09-23 -------------------- Improve resolving performance by avoiding extra calls for basic fields. This change improves performance of resolving a query by skipping `Info` creation and permission checking for fields that don't have a resolver or permission classes. In local benchmarks it improves performance of large results by ~14%. Contributed by [Jonathan Kim](https://github.com/jkimbo) via [PR #2194](https://github.com/strawberry-graphql/strawberry/pull/2194/) 0.132.0 - 2022-09-23 -------------------- Support storing metadata in strawberry fields. Contributed by [Paulo Costa](https://github.com/paulo-raca) via [PR #2190](https://github.com/strawberry-graphql/strawberry/pull/2190/) 0.131.5 - 2022-09-22 -------------------- Fixes false positives with the mypy plugin. Happened when `to_pydantic` was called on a type that was converted pydantic with all_fields=True. Also fixes the type signature when `to_pydantic` is defined by the user. ```python from pydantic import BaseModel from typing import Optional import strawberry class MyModel(BaseModel): email: str password: Optional[str] @strawberry.experimental.pydantic.input(model=MyModel, all_fields=True) class MyModelStrawberry: ... MyModelStrawberry(email="").to_pydantic() # previously would complain wrongly about missing email and password ``` Contributed by [James Chua](https://github.com/thejaminator) via [PR #2017](https://github.com/strawberry-graphql/strawberry/pull/2017/) 0.131.4 - 2022-09-22 -------------------- This release updates the mypy plugin and the typing for Pyright to treat all strawberry fields as keyword-only arguments. This reflects a previous change to the Strawberry API. Contributed by [Paulo Costa](https://github.com/paulo-raca) via [PR #2191](https://github.com/strawberry-graphql/strawberry/pull/2191/) 0.131.3 - 2022-09-22 -------------------- Bug fix: Do not force kw-only=False in fields specified with strawberry.field() Contributed by [Paulo Costa](https://github.com/paulo-raca) via [PR #2189](https://github.com/strawberry-graphql/strawberry/pull/2189/) 0.131.2 - 2022-09-22 -------------------- This release fixes a small issue that might happen when uploading files and not passing the operations object. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2192](https://github.com/strawberry-graphql/strawberry/pull/2192/) 0.131.1 - 2022-09-16 -------------------- Fix warnings during unit tests for Sanic's upload. Otherwise running unit tests results in a bunch of warning like this: ``` DeprecationWarning: Use 'content=<...>' to upload raw bytes/text content. ``` Contributed by [Paulo Costa](https://github.com/paulo-raca) via [PR #2178](https://github.com/strawberry-graphql/strawberry/pull/2178/) 0.131.0 - 2022-09-15 -------------------- This release improves the dataloader class with new features: - Explicitly cache invalidation, prevents old data from being fetched after a mutation - Importing data into the cache, prevents unnecessary load calls if the data has already been fetched by other means. Contributed by [Paulo Costa](https://github.com/paulo-raca) via [PR #2149](https://github.com/strawberry-graphql/strawberry/pull/2149/) 0.130.4 - 2022-09-14 -------------------- This release adds improved support for Pyright and Pylance, VSCode default language server for Python. Using `strawberry.type`, `strawberry.field`, `strawberry.input` and `strawberry.enum` will now be correctly recognized by Pyright and Pylance and won't show errors. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2172](https://github.com/strawberry-graphql/strawberry/pull/2172/) 0.130.3 - 2022-09-12 -------------------- Fix invalid deprecation warning issued on arguments annotated by a subclassed `strawberry.types.Info`. Thanks to @ThirVondukr for the bug report! Example: ```python class MyInfo(Info): pass @strawberry.type class Query: @strawberry.field def is_tasty(self, info: MyInfo) -> bool: """Subclassed ``info`` argument no longer raises deprecation warning.""" ``` Contributed by [San Kilkis](https://github.com/skilkis) via [PR #2137](https://github.com/strawberry-graphql/strawberry/pull/2137/) 0.130.2 - 2022-09-12 -------------------- This release fixes the conversion of generic aliases when using pydantic. Contributed by [Silas Sewell](https://github.com/silas) via [PR #2152](https://github.com/strawberry-graphql/strawberry/pull/2152/) 0.130.1 - 2022-09-12 -------------------- Fix version parsing issue related to dev builds of Mypy in `strawberry.ext.mypy_plugin` Contributed by [San Kilkis](https://github.com/skilkis) via [PR #2157](https://github.com/strawberry-graphql/strawberry/pull/2157/) 0.130.0 - 2022-09-12 -------------------- Convert Tuple and Sequence types to GraphQL list types. Example: ```python from collections.abc import Sequence from typing import Tuple @strawberry.type class User: pets: Sequence[Pet] favourite_ice_cream_flavours: Tuple[IceCreamFlavour] ``` Contributed by [Jonathan Kim](https://github.com/jkimbo) via [PR #2164](https://github.com/strawberry-graphql/strawberry/pull/2164/) 0.129.0 - 2022-09-11 -------------------- This release adds `strawberry.lazy` which allows you to define the type of the field and its path. This is useful when you want to define a field with a type that has a circular dependency. For example, let's say we have a `User` type that has a list of `Post` and a `Post` type that has a `User`: ```python # posts.py from typing import TYPE_CHECKING, Annotated import strawberry if TYPE_CHECKING: from .users import User @strawberry.type class Post: title: str author: Annotated["User", strawberry.lazy(".users")] ``` ```python # users.py from typing import TYPE_CHECKING, Annotated, List import strawberry if TYPE_CHECKING: from .posts import Post @strawberry.type class User: name: str posts: List[Annotated["Post", strawberry.lazy(".posts")]] ``` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2158](https://github.com/strawberry-graphql/strawberry/pull/2158/) 0.128.0 - 2022-09-05 -------------------- This release changes how dataclasses are created to make use of the new `kw_only` argument in Python 3.10 so that fields without a default value can now follow a field with a default value. This feature is also backported to all other supported Python versions. More info: https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass For example: ```python # This no longer raises a TypeError @strawberry.type class MyType: a: str = "Hi" b: int ``` ⚠️ This is a breaking change! Whenever instantiating a Strawberry type make sure that you only pass values are keyword arguments: ```python # Before: MyType("foo", 3) # After: MyType(a="foo", b=3) ``` Contributed by [Jonathan Kim](https://github.com/jkimbo) via [PR #1187](https://github.com/strawberry-graphql/strawberry/pull/1187/) 0.127.4 - 2022-08-31 -------------------- This release fixes a bug in the subscription clean up when subscribing using the graphql-transport-ws protocol, which could occasionally cause a 'finally' statement within the task to not get run, leading to leaked resources. Contributed by [rjwills28](https://github.com/rjwills28) via [PR #2141](https://github.com/strawberry-graphql/strawberry/pull/2141/) 0.127.3 - 2022-08-30 -------------------- This release fixes a couple of small styling issues with the GraphiQL explorer Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2143](https://github.com/strawberry-graphql/strawberry/pull/2143/) 0.127.2 - 2022-08-30 -------------------- This release adds support for passing schema directives to `Schema(..., types=[])`. This can be useful if using a built-inschema directive that's not supported by a gateway. For example the following: ```python import strawberry from strawberry.scalars import JSON from strawberry.schema_directive import Location @strawberry.type class Query: example: JSON @strawberry.schema_directive(locations=[Location.SCALAR], name="specifiedBy") class SpecifiedBy: name: str schema = strawberry.Schema(query=Query, types=[SpecifiedBy]) ``` will print the following SDL: ```graphql directive @specifiedBy(name: String!) on SCALAR """ The `JSON` scalar type represents JSON values as specified by [ECMA-404](https://ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf). """ scalar JSON @specifiedBy( url: "https://ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf" ) type Query { example: JSON! } ``` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2140](https://github.com/strawberry-graphql/strawberry/pull/2140/) 0.127.1 - 2022-08-30 -------------------- This release fixes an issue with the updated GraphiQL interface. Contributed by [Doctor](https://github.com/ThirVondukr) via [PR #2138](https://github.com/strawberry-graphql/strawberry/pull/2138/) 0.127.0 - 2022-08-29 -------------------- This release updates the built-in GraphiQL version to version 2.0, which means you can now enjoy all the new features that come with the latest version! Contributed by [Matt Exact](https://github.com/MattExact) via [PR #1889](https://github.com/strawberry-graphql/strawberry/pull/1889/) 0.126.2 - 2022-08-23 -------------------- This release restricts the `backports.cached_property` dependency to only be installed when Python < 3.8. Since version 3.8 `cached_property` is included in the builtin `functools`. The code is updated to use the builtin version when Python >= 3.8. Contributed by [ljnsn](https://github.com/ljnsn) via [PR #2114](https://github.com/strawberry-graphql/strawberry/pull/2114/) 0.126.1 - 2022-08-22 -------------------- Keep extra discovered types sorted so that each schema printing is always the same. Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #2115](https://github.com/strawberry-graphql/strawberry/pull/2115/) 0.126.0 - 2022-08-18 -------------------- This release adds support for adding descriptions to enum values. ### Example ```python @strawberry.enum class IceCreamFlavour(Enum): VANILLA = strawberry.enum_value("vanilla") STRAWBERRY = strawberry.enum_value( "strawberry", description="Our favourite", ) CHOCOLATE = "chocolate" @strawberry.type class Query: favorite_ice_cream: IceCreamFlavour = IceCreamFlavour.STRAWBERRY schema = strawberry.Schema(query=Query) ``` This produces the following schema ```graphql enum IceCreamFlavour { VANILLA """Our favourite.""" STRAWBERRY CHOCOLATE } type Query { favoriteIceCream: IceCreamFlavour! } ``` Contributed by [Felipe Gonzalez](https://github.com/gonzalezzfelipe) via [PR #2106](https://github.com/strawberry-graphql/strawberry/pull/2106/) 0.125.1 - 2022-08-16 -------------------- This release hides `resolvable: True` in @keys directives when using Apollo Federation 1, to preserve compatibility with older Gateways. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2099](https://github.com/strawberry-graphql/strawberry/pull/2099/) 0.125.0 - 2022-08-12 -------------------- This release adds an integration with Django Channels. The integration will allow you to use GraphQL subscriptions via Django Channels. Contributed by [Dan Sloan](https://github.com/LucidDan) via [PR #1407](https://github.com/strawberry-graphql/strawberry/pull/1407/) 0.124.0 - 2022-08-08 -------------------- This release adds full support for Apollo Federation 2.0. To opt-in you need to pass `enable_federation_2=True` to `strawberry.federation.Schema`, like in the following example: ```python @strawberry.federation.type(keys=["id"]) class User: id: strawberry.ID @strawberry.type class Query: user: User schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) ``` This release also improves type checker support for the federation. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2047](https://github.com/strawberry-graphql/strawberry/pull/2047/) 0.123.3 - 2022-08-02 -------------------- This release fixes a regression introduced in version 0.118.2 which was preventing using circular dependencies in Strawberry django and Strawberry django plus. Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #2062](https://github.com/strawberry-graphql/strawberry/pull/2062/) 0.123.2 - 2022-08-01 -------------------- This release adds support for priting custom enums used only on schema directives, for example the following schema: ```python @strawberry.enum class Reason(str, Enum): EXAMPLE = "example" @strawberry.schema_directive(locations=[Location.FIELD_DEFINITION]) class Sensitive: reason: Reason @strawberry.type class Query: first_name: str = strawberry.field(directives=[Sensitive(reason=Reason.EXAMPLE)]) ``` prints the following: ```graphql directive @sensitive(reason: Reason!) on FIELD_DEFINITION type Query { firstName: String! @sensitive(reason: EXAMPLE) } enum Reason { EXAMPLE } ``` while previously it would omit the definition of the enum. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2059](https://github.com/strawberry-graphql/strawberry/pull/2059/) 0.123.1 - 2022-08-01 -------------------- This release adds support for priting custom scalar used only on schema directives, for example the following schema: ```python SensitiveConfiguration = strawberry.scalar(str, name="SensitiveConfiguration") @strawberry.schema_directive(locations=[Location.FIELD_DEFINITION]) class Sensitive: config: SensitiveConfiguration @strawberry.type class Query: first_name: str = strawberry.field(directives=[Sensitive(config="Some config")]) ``` prints the following: ```graphql directive @sensitive(config: SensitiveConfiguration!) on FIELD_DEFINITION type Query { firstName: String! @sensitive(config: "Some config") } scalar SensitiveConfiguration ``` while previously it would omit the definition of the scalar. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2058](https://github.com/strawberry-graphql/strawberry/pull/2058/) 0.123.0 - 2022-08-01 -------------------- This PR adds support for adding schema directives to the schema of your GraphQL API. For printing the following schema: ```python @strawberry.schema_directive(locations=[Location.SCHEMA]) class Tag: name: str @strawberry.type class Query: first_name: str = strawberry.field(directives=[Tag(name="team-1")]) schema = strawberry.Schema(query=Query, schema_directives=[Tag(name="team-1")]) ``` will print the following: ```graphql directive @tag(name: String!) on SCHEMA schema @tag(name: "team-1") { query: Query } type Query { firstName: String! } """ ``` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2054](https://github.com/strawberry-graphql/strawberry/pull/2054/) 0.122.1 - 2022-07-31 -------------------- This release fixes that the AIOHTTP integration ignored the `operationName` of query operations. This behaviour is a regression introduced in version 0.107.0. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) via [PR #2055](https://github.com/strawberry-graphql/strawberry/pull/2055/) 0.122.0 - 2022-07-29 -------------------- This release adds support for printing default values for scalars like JSON. For example the following: ```python import strawberry from strawberry.scalars import JSON @strawberry.input class MyInput: j: JSON = strawberry.field(default_factory=dict) j2: JSON = strawberry.field(default_factory=lambda: {"hello": "world"}) ``` will print the following schema: ```graphql input MyInput { j: JSON! = {} j2: JSON! = {hello: "world"} } ``` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2048](https://github.com/strawberry-graphql/strawberry/pull/2048/) 0.121.1 - 2022-07-27 -------------------- This release adds a backward compatibility layer with libraries that specify a custom `get_result`. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2038](https://github.com/strawberry-graphql/strawberry/pull/2038/) 0.121.0 - 2022-07-23 -------------------- This release adds support for overriding the default resolver for fields. Currently the default resolver is `getattr`, but now you can change it to any function you like, for example you can allow returning dictionaries: ```python @strawberry.type class User: name: str @strawberry.type class Query: @strawberry.field def user(self) -> User: return {"name": "Patrick"} # type: ignore schema = strawberry.Schema( query=Query, config=StrawberryConfig(default_resolver=getitem), ) query = "{ user { name } }" result = schema.execute_sync(query) ``` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2037](https://github.com/strawberry-graphql/strawberry/pull/2037/) 0.120.0 - 2022-07-23 -------------------- This release add a new `DatadogTracingExtension` that can be used to instrument your application with Datadog. ```python import strawberry from strawberry.extensions.tracing import DatadogTracingExtension schema = strawberry.Schema( Query, extensions=[ DatadogTracingExtension, ], ) ``` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #2001](https://github.com/strawberry-graphql/strawberry/pull/2001/) 0.119.2 - 2022-07-23 -------------------- Fixed edge case where `Union` types raised an `UnallowedReturnTypeForUnion` error when returning the correct type from the resolver. This also improves performance of StrawberryUnion's `_resolve_union_type` from `O(n)` to `O(1)` in the majority of cases where `n` is the number of types in the schema. For [example the below](https://play.strawberry.rocks/?gist=f7d88898d127e65b12140fdd763f9ef2)) would previously raise the error when querying `two` as `StrawberryUnion` would incorrectly determine that the resolver returns `Container[TypeOne]`. ```python import strawberry from typing import TypeVar, Generic, Union, List, Type T = TypeVar("T") @strawberry.type class Container(Generic[T]): items: List[T] @strawberry.type class TypeOne: attr: str @strawberry.type class TypeTwo: attr: str def resolver_one(): return Container(items=[TypeOne("one")]) def resolver_two(): return Container(items=[TypeTwo("two")]) @strawberry.type class Query: one: Union[Container[TypeOne], TypeOne] = strawberry.field(resolver_one) two: Union[Container[TypeTwo], TypeTwo] = strawberry.field(resolver_two) schema = strawberry.Schema(query=Query) ``` Contributed by [Tim OSullivan](https://github.com/invokermain) via [PR #2029](https://github.com/strawberry-graphql/strawberry/pull/2029/) 0.119.1 - 2022-07-18 -------------------- An explanatory custom exception is raised when union of GraphQL input types is attempted. Contributed by [Dhanshree Arora](https://github.com/DhanshreeA) via [PR #2019](https://github.com/strawberry-graphql/strawberry/pull/2019/) 0.119.0 - 2022-07-14 -------------------- This release changes when we add the custom directives extension, previously the extension was always enabled, now it is only enabled if you pass custom directives to `strawberry.Schema`. Contributed by [bomtall](https://github.com/bomtall) via [PR #2020](https://github.com/strawberry-graphql/strawberry/pull/2020/) 0.118.2 - 2022-07-14 -------------------- This release adds an initial fix to make `strawberry.auto` work when using `from __future__ import annotations`. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #1994](https://github.com/strawberry-graphql/strawberry/pull/1994/) 0.118.1 - 2022-07-14 -------------------- Fixes issue where users without pydantic were not able to use the mypy plugin. Contributed by [James Chua](https://github.com/thejaminator) via [PR #2016](https://github.com/strawberry-graphql/strawberry/pull/2016/) 0.118.0 - 2022-07-13 -------------------- You can now pass keyword arguments to `to_pydantic` ```python from pydantic import BaseModel import strawberry class MyModel(BaseModel): email: str password: str @strawberry.experimental.pydantic.input(model=MyModel) class MyModelStrawberry: email: strawberry.auto # no password field here MyModelStrawberry(email="").to_pydantic(password="hunter") ``` Also if you forget to pass password, mypy will complain ```python MyModelStrawberry(email="").to_pydantic() # error: Missing named argument "password" for "to_pydantic" of "MyModelStrawberry" ``` Contributed by [James Chua](https://github.com/thejaminator) via [PR #2012](https://github.com/strawberry-graphql/strawberry/pull/2012/) 0.117.1 - 2022-07-07 -------------------- Allow to add alias to fields generated from pydantic with `strawberry.field(name="ageAlias")`. ``` class User(pydantic.BaseModel): age: int @strawberry.experimental.pydantic.type(User) class UserType: age: strawberry.auto = strawberry.field(name="ageAlias") ``` Contributed by [Alex](https://github.com/benzolium) via [PR #1986](https://github.com/strawberry-graphql/strawberry/pull/1986/) 0.117.0 - 2022-07-06 -------------------- This release fixes an issue that required installing opentelemetry when trying to use the ApolloTracing extension Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #1977](https://github.com/strawberry-graphql/strawberry/pull/1977/) 0.116.4 - 2022-07-04 -------------------- Fix regression caused by the new resolver argument handling mechanism introduced in v0.115.0. This release restores the ability to use unhashable default values in resolvers such as dict and list. See example below: ```python @strawberry.type class Query: @strawberry.field def field(self, x: List[str] = ["foo"], y: JSON = {"foo": 42}) -> str: # noqa: B006 return f"{x} {y}" ``` Thanks to @coady for the regression report! Contributed by [San Kilkis](https://github.com/skilkis) via [PR #1985](https://github.com/strawberry-graphql/strawberry/pull/1985/) 0.116.3 - 2022-07-04 -------------------- This release fixes the following error when trying to use Strawberry with Apollo Federation: ``` Error: A valid schema couldn't be composed. The following composition errors were found: [burro-api] Unknown type _FieldSet ``` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #1988](https://github.com/strawberry-graphql/strawberry/pull/1988/) 0.116.2 - 2022-07-03 -------------------- Reimplement `StrawberryResolver.annotations` property after removal in v0.115. Library authors who previously relied on the public `annotations` property can continue to do so after this fix. Contributed by [San Kilkis](https://github.com/skilkis) via [PR #1990](https://github.com/strawberry-graphql/strawberry/pull/1990/) 0.116.1 - 2022-07-03 -------------------- This release fixes a breaking internal error in mypy plugin for the following case. - using positional arguments to pass a resolver for `strawberry.field()` or `strawberry.mutation()` ```python failed: str = strawberry.field(resolver) successed: str = strawberry.field(resolver=resolver) ``` now mypy returns an error with `"field()" or "mutation()" only takes keyword arguments` message rather than an internal error. Contributed by [cake-monotone](https://github.com/cake-monotone) via [PR #1987](https://github.com/strawberry-graphql/strawberry/pull/1987/) 0.116.0 - 2022-07-03 -------------------- This release adds a link from generated GraphQLCore types to the Strawberry type that generated them. From a GraphQLCore type you can now access the Strawberry type by doing: ```python strawberry_type: TypeDefinition = graphql_core_type.extensions[ GraphQLCoreConverter.DEFINITION_BACKREF ] ``` Contributed by [Paulo Costa](https://github.com/paulo-raca) via [PR #1766](https://github.com/strawberry-graphql/strawberry/pull/1766/) 0.115.0 - 2022-07-01 -------------------- This release changes how we declare the `info` argument in resolvers and the `value` argument in directives. Previously we'd use the name of the argument to determine its value. Now we use the type annotation of the argument to determine its value. Here's an example of how the old syntax works: ```python def some_resolver(info) -> str: return info.context.get("some_key", "default") @strawberry.type class Example: a_field: str = strawberry.resolver(some_resolver) ``` and here's an example of how the new syntax works: ```python from strawberry.types import Info def some_resolver(info: strawberry.Info) -> str: return info.context.get("some_key", "default") @strawberry.type class Example: a_field: str = strawberry.resolver(some_resolver) ``` This means that you can now use a different name for the `info` argument in your resolver and the `value` argument in your directive. Here's an example that uses a custom name for both the value and the info parameter in directives: ```python from strawberry.types import Info from strawberry.directive import DirectiveLocation, DirectiveValue @strawberry.type class Cake: frosting: Optional[str] = None flavor: str = "Chocolate" @strawberry.type class Query: @strawberry.field def cake(self) -> Cake: return Cake() @strawberry.directive( locations=[DirectiveLocation.FIELD], description="Add frosting with ``value`` to a cake.", ) def add_frosting(value: str, v: DirectiveValue[Cake], my_info: strawberry.Info): # Arbitrary argument name when using `DirectiveValue` is supported! assert isinstance(v, Cake) if ( value in my_info.context["allergies"] ): # Info can now be accessed from directives! raise AllergyError("You are allergic to this frosting!") else: v.frosting = value # Value can now be used as a GraphQL argument name! return v ``` **Note:** the old way of passing arguments by name is deprecated and will be removed in future releases of Strawberry. Contributed by [San Kilkis](https://github.com/skilkis) via [PR #1713](https://github.com/strawberry-graphql/strawberry/pull/1713/) 0.114.7 - 2022-07-01 -------------------- Allow use of implicit `Any` in `strawberry.Private` annotated Generic types. For example the following is now supported: ```python from __future__ import annotations from typing import Generic, Sequence, TypeVar import strawberry T = TypeVar("T") @strawberry.type class Foo(Generic[T]): private_field: strawberry.Private[Sequence] # instead of Sequence[Any] @strawberry.type class Query: @strawberry.field def foo(self) -> Foo[str]: return Foo(private_field=[1, 2, 3]) ``` See Issue [#1938](https://github.com/strawberry-graphql/strawberry/issues/1938) for details. Contributed by [San Kilkis](https://github.com/skilkis) via [PR #1939](https://github.com/strawberry-graphql/strawberry/pull/1939/) 0.114.6 - 2022-06-30 -------------------- The federation decorator now allows for a list of additional arbitrary schema directives extending the key/shareable directives used for federation. Example Python: ```python import strawberry from strawberry.schema.config import StrawberryConfig from strawberry.schema_directive import Location @strawberry.schema_directive(locations=[Location.OBJECT]) class CacheControl: max_age: int @strawberry.federation.type( keys=["id"], shareable=True, extend=True, directives=[CacheControl(max_age=42)] ) class FederatedType: id: strawberry.ID schema = strawberry.Schema(query=Query, config=StrawberryConfig(auto_camel_case=False)) ``` Resulting GQL Schema: ```graphql directive @CacheControl(max_age: Int!) on OBJECT directive @key(fields: _FieldSet!, resolvable: Boolean) on OBJECT | INTERFACE directive @shareable on FIELD_DEFINITION | OBJECT extend type FederatedType @key(fields: "id") @shareable @CacheControl(max_age: 42) { id: ID! } type Query { federatedType: FederatedType! } ``` Contributed by [Jeffrey DeFond](https://github.com/defond0) via [PR #1945](https://github.com/strawberry-graphql/strawberry/pull/1945/) 0.114.5 - 2022-06-23 -------------------- This release adds support in Mypy for using strawberry.mutation while passing a resolver, the following now doesn't make Mypy return an error: ```python import strawberry def set_name(self, name: str) -> None: self.name = name @strawberry.type class Mutation: set_name: None = strawberry.mutation(resolver=set_name) ``` Contributed by [Etty](https://github.com/estyxx) via [PR #1966](https://github.com/strawberry-graphql/strawberry/pull/1966/) 0.114.4 - 2022-06-23 -------------------- This release fixes the type annotation of `Response.errors` used in the `GraphQLTestClient` to be a `List` of `GraphQLFormattedError`. Contributed by [Etty](https://github.com/estyxx) via [PR #1961](https://github.com/strawberry-graphql/strawberry/pull/1961/) 0.114.3 - 2022-06-21 -------------------- This release fixes the type annotation of `Response.errors` used in the `GraphQLTestClient` to be a `List` of `GraphQLError`. Contributed by [Etty](https://github.com/estyxx) via [PR #1959](https://github.com/strawberry-graphql/strawberry/pull/1959/) 0.114.2 - 2022-06-15 -------------------- This release fixes an issue in the `GraphQLTestClient` when using both variables and files together. Contributed by [Etty](https://github.com/estyxx) via [PR #1576](https://github.com/strawberry-graphql/strawberry/pull/1576/) 0.114.1 - 2022-06-09 -------------------- Fix crash in Django's `HttpResponse.__repr__` by handling `status_code=None` in `TemporalHttpResponse.__repr__`. Contributed by [Daniel Hahler](https://github.com/blueyed) via [PR #1950](https://github.com/strawberry-graphql/strawberry/pull/1950/) 0.114.0 - 2022-05-27 -------------------- Improve schema directives typing and printing after latest refactor. - Support for printing schema directives for non-scalars (e.g. types) and null values. - Also print the schema directive itself and any extra types defined in it - Fix typing for apis expecting directives (e.g. `strawberry.field`, `strawberry.type`, etc) to expect an object instead of a `StrawberrySchemaDirective`, which is now an internal type. Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #1723](https://github.com/strawberry-graphql/strawberry/pull/1723/) 0.113.0 - 2022-05-19 -------------------- This release adds support for Starlette 0.18 to 0.20 It also removes upper bound dependencies limit for starlette, allowing you to install the latest version without having to wait for a new release of Strawberry Contributed by [Timothy Pansino](https://github.com/TimPansino) via [PR #1594](https://github.com/strawberry-graphql/strawberry/pull/1594/) 0.112.0 - 2022-05-15 -------------------- This release adds a new flask view to allow for aysnc dispatching of requests. This is especially useful when using dataloaders with flask. ```python from strawberry.flask.views import AsyncGraphQLView ... app.add_url_rule( "/graphql", view_func=AsyncGraphQLView.as_view("graphql_view", schema=schema, **kwargs), ) ``` Contributed by [Scott Weitzner](https://github.com/scottweitzner) via [PR #1907](https://github.com/strawberry-graphql/strawberry/pull/1907/) 0.111.2 - 2022-05-09 -------------------- This release fixes resolvers using functions with generic type variables raising a `MissingTypesForGenericError` error. For example a resolver factory like the below can now be used: ```python import strawberry from typing import Type, TypeVar T = TypeVar("T") # or TypeVar("T", bound=StrawberryType) etc def resolver_factory(strawberry_type: Type[T]): def resolver(id: strawberry.ID) -> T: # some actual logic here return strawberry_type(...) return resolver ``` Contributed by [Tim OSullivan](https://github.com/invokermain) via [PR #1891](https://github.com/strawberry-graphql/strawberry/pull/1891/) 0.111.1 - 2022-05-03 -------------------- Rename internal variable `custom_getter` in FastAPI router implementation. Contributed by [Gary Donovan](https://github.com/garyd203) via [PR #1875](https://github.com/strawberry-graphql/strawberry/pull/1875/) 0.111.0 - 2022-05-02 -------------------- This release adds support for Apollo Federation 2 directives: - @shareable - @tag - @override - @inaccessible This release does **not** add support for the @link directive. This release updates the @key directive to align with Apollo Federation 2 updates. See the below code snippet and/or the newly-added test cases for examples on how to use the new directives. The below snippet demonstrates the @override directive. ```python import strawberry from typing import List @strawberry.interface class SomeInterface: id: strawberry.ID @strawberry.federation.type(keys=["upc"], extend=True) class Product(SomeInterface): upc: str = strawberry.federation.field(external=True, override=["mySubGraph"]) @strawberry.federation.type class Query: @strawberry.field def top_products(self, first: int) -> List[Product]: return [] schema = strawberry.federation.Schema(query=Query) ``` should return: ```graphql extend type Product implements SomeInterface @key(fields: "upc", resolvable: "True") { id: ID! upc: String! @external @override(from: "mySubGraph") } type Query { _service: _Service! _entities(representations: [_Any!]!): [_Entity]! topProducts(first: Int!): [Product!]! } interface SomeInterface { id: ID! } scalar _Any union _Entity = Product type _Service { sdl: String! } ``` Contributed by [Matt Skillman](https://github.com/mtskillman) via [PR #1874](https://github.com/strawberry-graphql/strawberry/pull/1874/) 0.110.0 - 2022-05-02 -------------------- This release adds support for passing a custom name to schema directives fields, by using `strawberry.directive_field`. ```python import strawberry @strawberry.schema_directive(locations=[Location.FIELD_DEFINITION]) class Sensitive: reason: str = strawberry.directive_field(name="as") real_age_2: str = strawberry.directive_field(name="real_age") @strawberry.type class Query: first_name: str = strawberry.field( directives=[Sensitive(reason="GDPR", real_age_2="42")] ) ``` should return: ```graphql type Query { firstName: String! @sensitive(as: "GDPR", real_age: "42") } ``` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #1871](https://github.com/strawberry-graphql/strawberry/pull/1871/) 0.109.1 - 2022-04-28 -------------------- This release adds support for Mypy 0.950 Contributed by [dependabot](https://github.com/dependabot) via [PR #1855](https://github.com/strawberry-graphql/strawberry/pull/1855/) 0.109.0 - 2022-04-23 -------------------- Changed the location of `UNSET` from `arguments.py` to `unset.py`. `UNSET` can now also be imported directly from `strawberry`. Deprecated the `is_unset` method in favor of the builtin `is` operator: ```python from strawberry import UNSET from strawberry.arguments import is_unset # old a = UNSET assert a is UNSET # new assert is_unset(a) # old ``` Further more a new subsection to the docs was added explaining this. Contributed by [Dominique Garmier](https://github.com/DominiqueGarmier) via [PR #1813](https://github.com/strawberry-graphql/strawberry/pull/1813/) 0.108.3 - 2022-04-22 -------------------- Fixes a bug when converting pydantic models with NewTypes in a List. This no longers causes an exception. ```python from typing import List, NewType from pydantic import BaseModel import strawberry password = NewType("password", str) class User(BaseModel): passwords: List[password] @strawberry.experimental.pydantic.type(User) class UserType: passwords: strawberry.auto ``` Contributed by [James Chua](https://github.com/thejaminator) via [PR #1770](https://github.com/strawberry-graphql/strawberry/pull/1770/) 0.108.2 - 2022-04-21 -------------------- Fixes mypy type inference when using @strawberry.experimental.pydantic.input and @strawberry.experimental.pydantic.interface decorators Contributed by [James Chua](https://github.com/thejaminator) via [PR #1832](https://github.com/strawberry-graphql/strawberry/pull/1832/) 0.108.1 - 2022-04-20 -------------------- Refactoring: Move enum deserialization logic from convert_arguments to CustomGraphQLEnumType Contributed by [Paulo Costa](https://github.com/paulo-raca) via [PR #1765](https://github.com/strawberry-graphql/strawberry/pull/1765/) 0.108.0 - 2022-04-19 -------------------- Added support for deprecating Enum values with `deprecation_reason` while using `strawberry.enum_value` instead of string definition. ```python @strawberry.enum class IceCreamFlavour(Enum): VANILLA = strawberry.enum_value("vanilla") STRAWBERRY = strawberry.enum_value("strawberry", deprecation_reason="We ran out") CHOCOLATE = "chocolate" ``` Contributed by [Mateusz Sobas](https://github.com/msobas) via [PR #1720](https://github.com/strawberry-graphql/strawberry/pull/1720/) 0.107.1 - 2022-04-18 -------------------- This release fixes an issue in the previous release where requests using query params did not support passing variable values. Variables passed by query params are now parsed from a string to a dictionary. Contributed by [Matt Exact](https://github.com/MattExact) via [PR #1820](https://github.com/strawberry-graphql/strawberry/pull/1820/) 0.107.0 - 2022-04-18 -------------------- This release adds support in all our integration for queries via GET requests. This behavior is enabled by default, but you can disable it by passing `allow_queries_via_get=False` to the constructor of the integration of your choice. For security reason only queries are allowed via `GET` requests. Contributed by [Matt Exact](https://github.com/MattExact) via [PR #1686](https://github.com/strawberry-graphql/strawberry/pull/1686/) 0.106.3 - 2022-04-15 -------------------- Correctly parse Decimal scalar types to avoid floating point errors Contributed by [Marco Acierno](https://github.com/marcoacierno) via [PR #1811](https://github.com/strawberry-graphql/strawberry/pull/1811/) 0.106.2 - 2022-04-14 -------------------- Allow all data types in `Schema(types=[...])` Contributed by [Paulo Costa](https://github.com/paulo-raca) via [PR #1714](https://github.com/strawberry-graphql/strawberry/pull/1714/) 0.106.1 - 2022-04-14 -------------------- This release fixes a number of problems with single-result-operations over `graphql-transport-ws` protocol - operation **IDs** now share the same namespace as streaming operations meaning that they cannot be reused while the others are in operation - single-result-operations now run as *tasks* meaning that messages related to them can be overlapped with other messages on the websocket. - single-result-operations can be cancelled with the `complete` message. - IDs for single result and streaming result operations are now released once the operation is done, allowing them to be re-used later, as well as freeing up resources related to previous requests. Contributed by [Kristján Valur Jónsson](https://github.com/kristjanvalur) via [PR #1792](https://github.com/strawberry-graphql/strawberry/pull/1792/) 0.106.0 - 2022-04-14 -------------------- This release adds an implementation of the `GraphQLTestClient` for the `aiohttp` integration (in addition to the existing `asgi` and `Django` support). It hides the HTTP request's details and verifies that there are no errors in the response (this behavior can be disabled by passing `asserts_errors=False`). This makes it easier to test queries and makes your tests cleaner. If you are using `pytest` you can add a fixture in `conftest.py` ```python import pytest from strawberry.aiohttp.test.client import GraphQLTestClient @pytest.fixture def graphql_client(aiohttp_client, myapp): yield GraphQLTestClient(aiohttp_client(myapp)) ``` And use it everywhere in your tests ```python def test_strawberry(graphql_client): query = """ query Hi($name: String!) { hi(name: $name) } """ result = graphql_client.query(query, variables={"name": "🍓"}) assert result.data == {"hi": "Hi 🍓!"} ``` Contributed by [Etty](https://github.com/estyxx) via [PR #1604](https://github.com/strawberry-graphql/strawberry/pull/1604/) 0.105.1 - 2022-04-12 -------------------- This release fixes a bug in the codegen that marked optional unions as non optional. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #1806](https://github.com/strawberry-graphql/strawberry/pull/1806/) 0.105.0 - 2022-04-05 -------------------- This release adds support for passing `json_encoder` and `json_dumps_params` to Sanic's view. ```python from strawberry.sanic.views import GraphQLView from api.schema import Schema app = Sanic(__name__) app.add_route( GraphQLView.as_view( schema=schema, graphiql=True, json_encoder=CustomEncoder, json_dumps_params={}, ), "/graphql", ) ``` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #1797](https://github.com/strawberry-graphql/strawberry/pull/1797/) 0.104.4 - 2022-04-05 -------------------- Allow use of `AsyncIterator` and `AsyncIterable` generics to annotate return type of subscription resolvers. Contributed by [San Kilkis](https://github.com/skilkis) via [PR #1771](https://github.com/strawberry-graphql/strawberry/pull/1771/) 0.104.3 - 2022-04-03 -------------------- Exeptions from handler functions in graphql_transport_ws are no longer incorrectly caught and classified as message parsing errors. Contributed by [Kristján Valur Jónsson](https://github.com/kristjanvalur) via [PR #1761](https://github.com/strawberry-graphql/strawberry/pull/1761/) 0.104.2 - 2022-04-02 -------------------- Drop support for Django < 3.2. Contributed by [Guillaume Andreu Sabater](https://github.com/g-as) via [PR #1787](https://github.com/strawberry-graphql/strawberry/pull/1787/) 0.104.1 - 2022-03-28 -------------------- This release adds support for aliased fields when doing codegen. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #1772](https://github.com/strawberry-graphql/strawberry/pull/1772/) 0.104.0 - 2022-03-28 -------------------- Add `is_auto` utility for checking if a type is `strawberry.auto`, considering the possibility of it being a `StrawberryAnnotation` or even being used inside `Annotated`. Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) via [PR #1721](https://github.com/strawberry-graphql/strawberry/pull/1721/) 0.103.9 - 2022-03-23 -------------------- This release moves the console plugin for the codegen command to be last one, allowing to run code before writing files to disk. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #1760](https://github.com/strawberry-graphql/strawberry/pull/1760/) 0.103.8 - 2022-03-18 -------------------- This release adds a `python_type` to the codegen `GraphQLEnum` class to allow access to the original python enum when generating code Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #1752](https://github.com/strawberry-graphql/strawberry/pull/1752/) 0.103.7 - 2022-03-18 -------------------- Fix an issue where there was no clean way to mark a Pydantic field as deprecated, add permission classes, or add directives. Now you can use the short field syntax to do all three. ```python import pydantic import strawberry class MyModel(pydantic.BaseModel): age: int name: str @strawberry.experimental.pydantic.type(MyModel) class MyType: age: strawberry.auto name: strawberry.auto = strawberry.field( deprecation_reason="Because", permission_classes=[MyPermission], directives=[MyDirective], ) ``` Contributed by [Matt Allen](https://github.com/Matt343) via [PR #1748](https://github.com/strawberry-graphql/strawberry/pull/1748/) 0.103.6 - 2022-03-18 -------------------- This release adds a missing `__init__.py` inside `cli/commands` Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #1751](https://github.com/strawberry-graphql/strawberry/pull/1751/) 0.103.5 - 2022-03-18 -------------------- This release fixes an issue that prevented using generic types with interfaces. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #1701](https://github.com/strawberry-graphql/strawberry/pull/1701/) 0.103.4 - 2022-03-18 -------------------- This release fixes a couple of more issues with codegen: 1. Adds support for boolean values in input fields 2. Changes how we unwrap types in order to add full support for LazyTypes, Optionals and Lists 3. Improve also how we generate types for unions, now we don't generate a Union type if the selection is for only one type Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #1746](https://github.com/strawberry-graphql/strawberry/pull/1746/) 0.103.3 - 2022-03-17 -------------------- The return type annotation for `DataLoader.load` and `load_many` no longer includes any exceptions directly returned by the `load_fn`. The ability to handle errors by returning them as elements from `load_fn` is now documented too. Contributed by [Huon Wilson](https://github.com/huonw) via [PR #1737](https://github.com/strawberry-graphql/strawberry/pull/1737/) 0.103.2 - 2022-03-17 -------------------- This release add supports for `LazyType`s in the codegen command Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #1745](https://github.com/strawberry-graphql/strawberry/pull/1745/) 0.103.1 - 2022-03-15 -------------------- This release adds support for MyPy 0.941 under Python 3.10 Contributed by [dependabot](https://github.com/dependabot) via [PR #1728](https://github.com/strawberry-graphql/strawberry/pull/1728/) 0.103.0 - 2022-03-14 -------------------- This release adds an experimental codegen feature for queries. It allows to combine a graphql query and Strawberry schema to generate Python types or TypeScript types. You can use the following command: ```bash strawberry codegen --schema schema --output-dir ./output -p python query.graphql ``` to generate python types that correspond to your GraphQL query. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #1655](https://github.com/strawberry-graphql/strawberry/pull/1655/) 0.102.3 - 2022-03-14 -------------------- This release makes StrawberryOptional and StrawberryList hashable, allowing to use strawberry types with libraries like dacite and dataclasses_json. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #1726](https://github.com/strawberry-graphql/strawberry/pull/1726/) 0.102.2 - 2022-03-08 -------------------- Add support for postponed evaluation of annotations ([PEP-563](https://www.python.org/dev/peps/pep-0563/)) to `strawberry.Private` annotated fields. ## Example This release fixes Issue #1586 using schema-conversion time filtering of `strawberry.Private` fields for PEP-563. This means the following is now supported: ```python @strawberry.type class Query: foo: "strawberry.Private[int]" ``` Forward references are supported as well: ```python from __future__ import annotations from dataclasses import dataclass @strawberry.type class Query: private_foo: strawberry.Private[SensitiveData] @strawberry.field def foo(self) -> int: return self.private_foo.visible @dataclass class SensitiveData: visible: int not_visible: int ``` Contributed by [San Kilkis](https://github.com/skilkis) via [PR #1684](https://github.com/strawberry-graphql/strawberry/pull/1684/) 0.102.1 - 2022-03-07 -------------------- This PR improves the support for scalars when using MyPy. Contributed by [Patrick Arminio](https://github.com/patrick91) via [PR #1205](https://github.com/strawberry-graphql/strawberry/pull/1205/) 0.102.0 - 2022-03-07 -------------------- Added the response object to `get_context` on the `flask` view. This means that in fields, something like this can be used; ```python @strawberry.field def response_check(self, info: strawberry.Info) -> bool: response: Response = info.context["response"] response.status_code = 401 return True ``` 0.101.0 - 2022-03-06 -------------------- This release adds support for `graphql-transport-ws` single result operations. Single result operations allow clients to execute queries and mutations over an existing WebSocket connection. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) [PR #1698](https://github.com/strawberry-graphql/strawberry/pull/1698/) 0.100.0 - 2022-03-05 -------------------- Change `strawberry.auto` to be a type instead of a sentinel. This not only removes the dependency on sentinel from the project, but also fixes some related issues, like the fact that only types can be used with `Annotated`. Also, custom scalars will now trick static type checkers into thinking they returned their wrapped type. This should fix issues with pyright 1.1.224+ where it doesn't allow non-type objects to be used as annotations for dataclasses and dataclass-alike classes (which is strawberry's case). The change to `strawberry.auto` also fixes this issue for it. Contributed by [Thiago Bellini Ribeiro](https://github.com/bellini666) [PR #1690](https://github.com/strawberry-graphql/strawberry/pull/1690/) 0.99.3 - 2022-03-05 ------------------- This release adds support for flask 2.x and also relaxes the requirements for Django, allowing to install newer version of Django without having to wait for Strawberry to update its supported dependencies list. Contributed by [Guillaume Andreu Sabater](https://github.com/g-as) [PR #1687](https://github.com/strawberry-graphql/strawberry/pull/1687/) 0.99.2 - 2022-03-04 ------------------- This fixes the schema printer to add support for schema directives on input types. Contributed by [Patrick Arminio](https://github.com/patrick91) [PR #1697](https://github.com/strawberry-graphql/strawberry/pull/1697/) 0.99.1 - 2022-03-02 ------------------- This release fixed a false positive deprecation warning related to our AIOHTTP class based view. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) [PR #1691](https://github.com/strawberry-graphql/strawberry/pull/1691/) 0.99.0 - 2022-02-28 ------------------- This release adds the following scalar types: - `JSON` - `Base16` - `Base32` - `Base64` they can be used like so: ```python from strawberry.scalar import Base16, Base32, Base64, JSON @strawberry.type class Example: a: Base16 b: Base32 c: Base64 d: JSON ``` Contributed by [Paulo Costa](https://github.com/paulo-raca) [PR #1647](https://github.com/strawberry-graphql/strawberry/pull/1647/) 0.98.2 - 2022-02-24 ------------------- Adds support for converting pydantic conlist. Note that constraint is not enforced in the graphql type. Thus, we recommend always working on the pydantic type such that the validation is enforced. ```python import strawberry from pydantic import BaseModel, conlist class Example(BaseModel): friends: conlist(str, min_items=1) @strawberry.experimental.pydantic.input(model=Example, all_fields=True) class ExampleGQL: ... @strawberry.type class Query: @strawberry.field() def test(self, example: ExampleGQL) -> None: # friends may be an empty list here print(example.friends) # calling to_pydantic() runs the validation and raises # an error if friends is empty print(example.to_pydantic().friends) schema = strawberry.Schema(query=Query) ``` The converted graphql type is ``` input ExampleGQL { friends: [String!]! } ``` Contributed by [James Chua](https://github.com/thejaminator) [PR #1656](https://github.com/strawberry-graphql/strawberry/pull/1656/) 0.98.1 - 2022-02-24 ------------------- This release wasn't published on PyPI 0.98.0 - 2022-02-23 ------------------- This release updates `graphql-core` to `3.2.0` Make sure you take a look at [`graphql-core`'s release notes](https://github.com/graphql-python/graphql-core/releases/tag/v3.2.0) for any potential breaking change that might affect you if you're importing things from the `graphql` package directly. Contributed by [Patrick Arminio](https://github.com/patrick91) [PR #1601](https://github.com/strawberry-graphql/strawberry/pull/1601/) 0.97.0 - 2022-02-17 ------------------- Support "void" functions It is now possible to have a resolver that returns "None". Strawberry will automatically assign the new `Void` scalar in the schema and will always send `null` in the response ## Exampe ```python @strawberry.type class Mutation: @strawberry.field def do_something(self, arg: int) -> None: return ``` results in this schema: ```graphql type Mutation { doSomething(arg: Int!): Void } ``` Contributed by [Paulo Costa](https://github.com/paulo-raca) [PR #1648](https://github.com/strawberry-graphql/strawberry/pull/1648/) 0.96.0 - 2022-02-07 ------------------- Add better support for custom Pydantic conversion logic and standardize the behavior when not using `strawberry.auto` as the type. See https://strawberry.rocks/docs/integrations/pydantic#custom-conversion-logic for details and examples. Note that this release fixes a bug related to Pydantic aliases in schema generation. If you have a field with the same name as an aliased Pydantic field but with a different type than `strawberry.auto`, the generated field will now use the alias name. This may cause schema changes on upgrade in these cases, so care should be taken. The alias behavior can be disabled by setting the `use_pydantic_alias` option of the decorator to false. Contributed by [Matt Allen](https://github.com/Matt343) [PR #1629](https://github.com/strawberry-graphql/strawberry/pull/1629/) 0.95.5 - 2022-02-07 ------------------- Adds support for `use_pydantic_alias` parameter in pydantic model conversion. Decides if the all the GraphQL field names for the generated type should use the alias name or not. ```python from pydantic import BaseModel, Field import strawberry class UserModel(BaseModel): id: int = Field(..., alias="my_alias_name") @strawberry.experimental.pydantic.type(UserModel, use_pydantic_alias=False) class User: id: strawberry.auto ``` If `use_pydantic_alias` is `False`, the GraphQL type User will use `id` for the name of the `id` field coming from the Pydantic model. ``` type User { id: Int! } ``` With `use_pydantic_alias` set to `True` (the default behaviour) the GraphQL type user will use `myAliasName` for the `id` field coming from the Pydantic models (since the field has a `alias` specified`) ``` type User { myAliasName: Int! } ``` `use_pydantic_alias` is set to `True` for backwards compatibility. Contributed by [James Chua](https://github.com/thejaminator) [PR #1546](https://github.com/strawberry-graphql/strawberry/pull/1546/) 0.95.4 - 2022-02-06 ------------------- This release adds compatibility with uvicorn 0.17 Contributed by [dependabot](https://github.com/dependabot) [PR #1627](https://github.com/strawberry-graphql/strawberry/pull/1627/) 0.95.3 - 2022-02-03 ------------------- This release fixes an issue with FastAPI context dependency injection that causes class-based custom contexts to no longer be permitted. Contributed by [Tommy Smith](https://github.com/tsmith023) [PR #1564](https://github.com/strawberry-graphql/strawberry/pull/1564/) 0.95.2 - 2022-02-02 ------------------- This release fixes an issue with the name generation for nested generics, the following: ```python T = TypeVar("T") K = TypeVar("K") V = TypeVar("V") @strawberry.type class Value(Generic[T]): value: T @strawberry.type class DictItem(Generic[K, V]): key: K value: V @strawberry.type class Query: d: Value[List[DictItem[int, str]]] ``` now yields the correct schema: ```graphql type IntStrDictItem { key: Int! value: String! } type IntStrDictItemListValue { value: [IntStrDictItem!]! } type Query { d: IntStrDictItemListValue! } ``` Contributed by [Patrick Arminio](https://github.com/patrick91) [PR #1621](https://github.com/strawberry-graphql/strawberry/pull/1621/) 0.95.1 - 2022-01-26 ------------------- Fix bug #1504 in the Pydantic integration, where it was impossible to define both an input and output type based on the same Pydantic base class. Contributed by [Matt Allen](https://github.com/Matt343) [PR #1592](https://github.com/strawberry-graphql/strawberry/pull/1592/) 0.95.0 - 2022-01-22 ------------------- Adds `to_pydantic` and `from_pydantic` type hints for IDE support. Adds mypy extension support as well. ```python from pydantic import BaseModel import strawberry class UserPydantic(BaseModel): age: int @strawberry.experimental.pydantic.type(UserPydantic) class UserStrawberry: age: strawberry.auto reveal_type(UserStrawberry(age=123).to_pydantic()) ``` Mypy will infer the type as "UserPydantic". Previously it would be "Any" Contributed by [James Chua](https://github.com/thejaminator) [PR #1544](https://github.com/strawberry-graphql/strawberry/pull/1544/) 0.94.0 - 2022-01-18 ------------------- This release replaces `cached_property` with `backports.cached_property` to improve the typing of the library. Contributed by [Rishi Kumar Ray](https://github.com/RishiKumarRay) [PR #1582](https://github.com/strawberry-graphql/strawberry/pull/1582/) 0.93.23 - 2022-01-11 -------------------- Improve typing of `@strawberry.enum()` by: 1. Using a `TypeVar` bound on `EnumMeta` instead of `EnumMeta`, which allows type-checkers (like pyright) to detect the fields of the enum being decorated. For example, for the following enum: ```python @strawberry.enum class IceCreamFlavour(Enum): VANILLA = "vanilla" STRAWBERRY = "strawberry" CHOCOLATE = "chocolate" ``` Prior to this change, pyright would complain if you tried to access `IceCreamFlavour.VANILLA`, since the type information of `IceCreamFlavour` was being erased by the `EnumMeta` typing . 2. Overloading it so that type-checkers (like pyright) knows in what cases it returns a decorator (when it's called with keyword arguments, e.g. `@strawberry.enum(name="IceCreamFlavor")`), versus when it returns the original enum type (without keyword arguments. Contributed by [Tim Joseph Dumol](https://github.com/TimDumol) [PR #1568](https://github.com/strawberry-graphql/strawberry/pull/1568/) 0.93.22 - 2022-01-09 -------------------- This release adds `load_many` to `DataLoader`. Contributed by [Silas Sewell](https://github.com/silas) [PR #1528](https://github.com/strawberry-graphql/strawberry/pull/1528/) 0.93.21 - 2022-01-07 -------------------- This release adds `deprecation_reason` support to arguments and mutations. Contributed by [Silas Sewell](https://github.com/silas) [PR #1527](https://github.com/strawberry-graphql/strawberry/pull/1527/) 0.93.20 - 2022-01-07 -------------------- This release checks for AutoFieldsNotInBaseModelError when converting from pydantic models. It is raised when strawberry.auto is used, but the pydantic model does not have the particular field defined. ```python class User(BaseModel): age: int @strawberry.experimental.pydantic.type(User) class UserType: age: strawberry.auto password: strawberry.auto ``` Previously no errors would be raised, and the password field would not appear on graphql schema. Such mistakes could be common during refactoring. Now, AutoFieldsNotInBaseModelError is raised. Contributed by [James Chua](https://github.com/thejaminator) [PR #1551](https://github.com/strawberry-graphql/strawberry/pull/1551/) 0.93.19 - 2022-01-06 -------------------- Fixes TypeError when converting a pydantic BaseModel with NewType field Contributed by [James Chua](https://github.com/thejaminator) [PR #1547](https://github.com/strawberry-graphql/strawberry/pull/1547/) 0.93.18 - 2022-01-05 -------------------- This release allows setting http headers and custom http status codes with FastAPI GraphQLRouter. Contributed by [David Němec](https://github.com/davidnemec) [PR #1537](https://github.com/strawberry-graphql/strawberry/pull/1537/) 0.93.17 - 2022-01-05 -------------------- Fix compatibility with Sanic 21.12 Contributed by [Artjoms Iskovs](https://github.com/mildbyte) [PR #1520](https://github.com/strawberry-graphql/strawberry/pull/1520/) 0.93.16 - 2022-01-04 -------------------- Add support for piping `StrawberryUnion` and `None` when annotating types. For example: ```python @strawberry.type class Cat: name: str @strawberry.type class Dog: name: str Animal = strawberry.union("Animal", (Cat, Dog)) @strawberry.type class Query: animal: Animal | None # This line no longer triggers a TypeError ``` Contributed by [Yossi Rozantsev](https://github.com/Apakottur) [PR #1540](https://github.com/strawberry-graphql/strawberry/pull/1540/) 0.93.15 - 2022-01-04 -------------------- This release fixes the conversion of pydantic models with a default_factory field. Contributed by [James Chua](https://github.com/thejaminator) [PR #1538](https://github.com/strawberry-graphql/strawberry/pull/1538/) 0.93.14 - 2022-01-03 -------------------- This release allows conversion of pydantic models with mutable default fields into strawberry types. Also fixes bug when converting a pydantic model field with default_factory. Previously it would raise an exception when fields with a default_factory were declared before fields without defaults. Contributed by [James Chua](https://github.com/thejaminator) [PR #1491](https://github.com/strawberry-graphql/strawberry/pull/1491/) 0.93.13 - 2021-12-25 -------------------- This release updates the Decimal and UUID scalar parsers to exclude the original_error exception and format the error message similar to other builtin scalars. Contributed by [Silas Sewell](https://github.com/silas) [PR #1507](https://github.com/strawberry-graphql/strawberry/pull/1507/) 0.93.12 - 2021-12-24 -------------------- Fix mypy plugin crushes when _get_type_for_expr is used on var nodes Contributed by [Andrii Kohut](https://github.com/andriykohut) [PR #1513](https://github.com/strawberry-graphql/strawberry/pull/1513/) 0.93.11 - 2021-12-24 -------------------- This release fixes a bug in the annotation parser that prevents using strict typinh for Optional arguments which have their default set to UNSET. Contributed by [Sarah Henkens](https://github.com/sarahhenkens) [PR #1467](https://github.com/strawberry-graphql/strawberry/pull/1467/) 0.93.10 - 2021-12-21 -------------------- This release adds support for mypy 0.920. Contributed by [Yossi Rozantsev](https://github.com/Apakottur) [PR #1503](https://github.com/strawberry-graphql/strawberry/pull/1503/) 0.93.9 - 2021-12-21 ------------------- This releases fixes a bug with the opentracing extension where the tracer wasn't replacing the field name correctly. 0.93.8 - 2021-12-20 ------------------- This release modifies the internal utility function `await_maybe` towards updating mypy to 0.920. Contributed by [Yossi Rozantsev](https://github.com/Apakottur) [PR #1505](https://github.com/strawberry-graphql/strawberry/pull/1505/) 0.93.7 - 2021-12-18 ------------------- Change `context_getter` in `strawberry.fastapi.GraphQLRouter` to merge, rather than overwrite, default and custom getters. This mean now you can always access the `request` instance from `info.context`, even when using a custom context getter. Contributed by [Tommy Smith](https://github.com/tsmith023) [PR #1494](https://github.com/strawberry-graphql/strawberry/pull/1494/) 0.93.6 - 2021-12-18 ------------------- This release changes when we fetch the event loop in dataloaders to prevent using the wrong event loop in some occasions. Contributed by [Patrick Arminio](https://github.com/patrick91) [PR #1498](https://github.com/strawberry-graphql/strawberry/pull/1498/) 0.93.5 - 2021-12-16 ------------------- This release fixes an issue that prevented from lazily importing enum types using LazyType. Contributed by [Patrick Arminio](https://github.com/patrick91) [PR #1501](https://github.com/strawberry-graphql/strawberry/pull/1501/) 0.93.4 - 2021-12-10 ------------------- This release allows running strawberry as a script, for example, you can start the debug server with the following command: ```shell python -m strawberry server schema ``` Contributed by [YogiLiu](https://github.com/YogiLiu) [PR #1481](https://github.com/strawberry-graphql/strawberry/pull/1481/) 0.93.3 - 2021-12-08 ------------------- This release adds support for uvicorn 0.16 Contributed by [dependabot](https://github.com/dependabot) [PR #1487](https://github.com/strawberry-graphql/strawberry/pull/1487/) 0.93.2 - 2021-12-08 ------------------- This fixes the previous release that introduced a direct dependency on Django. Contributed by [Guillaume Andreu Sabater](https://github.com/g-as) [PR #1489](https://github.com/strawberry-graphql/strawberry/pull/1489/) 0.93.1 - 2021-12-08 ------------------- This release adds support for Django 4.0 Contributed by [Guillaume Andreu Sabater](https://github.com/g-as) [PR #1484](https://github.com/strawberry-graphql/strawberry/pull/1484/) 0.93.0 - 2021-12-07 ------------------- This release `operation_type` to the `ExecutionContext` type that is available in extensions. It also gets the `operation_name` from the query if one isn't provided by the client. Contributed by [Jonathan Kim](https://github.com/jkimbo) [PR #1286](https://github.com/strawberry-graphql/strawberry/pull/1286/) 0.92.2 - 2021-12-06 ------------------- This release adds support for passing `json_encoder` and `json_dumps_params` to Django [`JsonResponse`](https://docs.djangoproject.com/en/stable/ref/request-response/#jsonresponse-objects) via a view. ```python from json import JSONEncoder from django.urls import path from strawberry.django.views import AsyncGraphQLView from .schema import schema # Pass the JSON params to `.as_view` urlpatterns = [ path( "graphql", AsyncGraphQLView.as_view( schema=schema, json_encoder=JSONEncoder, json_dumps_params={"separators": (",", ":")}, ), ), ] # … or set them in a custom view class CustomAsyncGraphQLView(AsyncGraphQLView): json_encoder = JSONEncoder json_dumps_params = {"separators": (",", ":")} ``` Contributed by [Illia Volochii](https://github.com/illia-v) [PR #1472](https://github.com/strawberry-graphql/strawberry/pull/1472/) 0.92.1 - 2021-12-04 ------------------- Fix cross-module type resolving for fields and resolvers The following two issues are now fixed: - A field with a generic (typeless) resolver looks up the type relative to the resolver and not the class the field is defined in. (#1448) - When inheriting fields from another class the origin of the fields are set to the inheriting class and not the class the field is defined in. Both these issues could lead to a rather undescriptive error message: > TypeError: (...) fields cannot be resolved. Unexpected type 'None' Contributed by [Michael P. Jung](https://github.com/bikeshedder) [PR #1449](https://github.com/strawberry-graphql/strawberry/pull/1449/) 0.92.0 - 2021-12-04 ------------------- This releases fixes an issue where you were not allowed to return a non-strawberry type for fields that return an interface. Now this works as long as each type implementing the interface implements an `is_type_of` classmethod. Previous automatic duck typing on types that implement an interface now requires explicit resolution using this classmethod. Contributed by [Etty](https://github.com/estyxx) [PR #1299](https://github.com/strawberry-graphql/strawberry/pull/1299/) 0.91.0 - 2021-12-04 ------------------- This release adds a `GraphQLTestClient`. It hides the http request's details and asserts that there are no errors in the response (you can always disable this behavior by passing `asserts_errors=False`). This makes it easier to test queries and makes your tests cleaner. If you are using `pytest` you can add a fixture in `conftest.py` ```python import pytest from django.test.client import Client from strawberry.django.test import GraphQLTestClient @pytest.fixture def graphql_client(): yield GraphQLTestClient(Client()) ``` And use it everywere in your test methods ```python def test_strawberry(graphql_client): query = """ query Hi($name: String!) { hi(name: $name) } """ result = graphql_client.query(query, variables={"name": "Marcotte"}) assert result.data == {"hi": "Hi Marcotte!"} ``` It can be used to test the file uploads as well ```python from django.core.files.uploadedfile import SimpleUploadedFile def test_upload(graphql_client): f = SimpleUploadedFile("file.txt", b"strawberry") query = """ mutation($textFile: Upload!) { readText(textFile: $textFile) } """ response = graphql_client.query( query=query, variables={"textFile": None}, files={"textFile": f}, ) assert response.data["readText"] == "strawberry" ``` Contributed by [Etty](https://github.com/estyxx) [PR #1225](https://github.com/strawberry-graphql/strawberry/pull/1225/) 0.90.3 - 2021-12-02 ------------------- This release fixes an issue that prevented using enums as arguments for generic types inside unions. Contributed by [Patrick Arminio](https://github.com/patrick91) [PR #1463](https://github.com/strawberry-graphql/strawberry/pull/1463/) 0.90.2 - 2021-11-28 ------------------- This release fixes the message of `InvalidFieldArgument` to properly show the field's name in the error message. Contributed by [Etty](https://github.com/estyxx) [PR #1322](https://github.com/strawberry-graphql/strawberry/pull/1322/) 0.90.1 - 2021-11-27 ------------------- This release fixes an issue that prevented using `classmethod`s and `staticmethod`s as resolvers ```python import strawberry @strawberry.type class Query: @strawberry.field @staticmethod def static_text() -> str: return "Strawberry" @strawberry.field @classmethod def class_name(cls) -> str: return cls.__name__ ``` Contributed by [Illia Volochii](https://github.com/illia-v) [PR #1430](https://github.com/strawberry-graphql/strawberry/pull/1430/) 0.90.0 - 2021-11-26 ------------------- This release improves type checking support for `strawberry.union` and now allows to use unions without any type issue, like so: ```python @strawberry.type class User: name: str @strawberry.type class Error: message: str UserOrError = strawberry.union("UserOrError", (User, Error)) x: UserOrError = User(name="Patrick") ``` Contributed by [Patrick Arminio](https://github.com/patrick91) [PR #1438](https://github.com/strawberry-graphql/strawberry/pull/1438/) 0.89.2 - 2021-11-26 ------------------- Fix init of Strawberry types from pydantic by skipping fields that have resolvers. Contributed by [Nina](https://github.com/nina-j) [PR #1451](https://github.com/strawberry-graphql/strawberry/pull/1451/) 0.89.1 - 2021-11-25 ------------------- This release fixes an issubclass test failing for `Literal`s in the experimental `pydantic` integration. Contributed by [Nina](https://github.com/nina-j) [PR #1445](https://github.com/strawberry-graphql/strawberry/pull/1445/) 0.89.0 - 2021-11-24 ------------------- This release changes how `strawberry.Private` is implemented to improve support for type checkers. Contributed by [Patrick Arminio](https://github.com/patrick91) [PR #1437](https://github.com/strawberry-graphql/strawberry/pull/1437/) 0.88.0 - 2021-11-24 ------------------- This release adds support for AWS Chalice. A framework for deploying serverless applications using AWS. A view for aws chalice has been added to the strawberry codebase. This view embedded in a chalice app allows anyone to get a GraphQL API working and hosted on AWS in minutes using a serverless architecture. Contributed by [Mark Sheehan](https://github.com/mcsheehan) [PR #923](https://github.com/strawberry-graphql/strawberry/pull/923/) 0.87.3 - 2021-11-23 ------------------- This release fixes the naming generation of generics when passing a generic type to another generic, like so: ```python @strawberry.type class Edge(Generic[T]): node: T @strawberry.type class Connection(Generic[T]): edges: List[T] Connection[Edge[int]] ``` Contributed by [Patrick Arminio](https://github.com/patrick91) [PR #1436](https://github.com/strawberry-graphql/strawberry/pull/1436/) 0.87.2 - 2021-11-19 ------------------- This releases updates the `typing_extension` dependency to latest version. Contributed by [dependabot](https://github.com/dependabot) [PR #1417](https://github.com/strawberry-graphql/strawberry/pull/1417/) 0.87.1 - 2021-11-15 ------------------- This release renames an internal exception from `NotAnEnum` to `ObjectIsNotAnEnumError`. Contributed by [Patrick Arminio](https://github.com/patrick91) [PR #1317](https://github.com/strawberry-graphql/strawberry/pull/1317/) 0.87.0 - 2021-11-15 ------------------- This release changes how we handle GraphQL names. It also introduces a new configuration option called `name_converter`. This option allows you to specify a custom `NameConverter` to be used when generating GraphQL names. This is currently not documented because the API will change slightly in future as we are working on renaming internal types. This release also fixes an issue when creating concrete types from generic when passing list objects. Contributed by [Patrick Arminio](https://github.com/patrick91) [PR #1394](https://github.com/strawberry-graphql/strawberry/pull/1394/) 0.86.1 - 2021-11-12 ------------------- This release fixes our MyPy plugin and re-adds support for typechecking classes created with the apollo federation decorator. Contributed by [Patrick Arminio](https://github.com/patrick91) [PR #1414](https://github.com/strawberry-graphql/strawberry/pull/1414/) 0.86.0 - 2021-11-12 ------------------- Add `on_executing_*` hooks to extensions to allow you to override the execution phase of a GraphQL operation. Contributed by [Jonathan Kim](https://github.com/jkimbo) [PR #1400](https://github.com/strawberry-graphql/strawberry/pull/1400/) 0.85.1 - 2021-10-26 ------------------- This release fixes an issue with schema directives not being printed correctly. Contributed by [Patrick Arminio](https://github.com/patrick91) [PR #1376](https://github.com/strawberry-graphql/strawberry/pull/1376/) 0.85.0 - 2021-10-23 ------------------- This release introduces initial support for schema directives and updates the federation support to use that. Full support will be implemented in future releases. Contributed by [Patrick Arminio](https://github.com/patrick91) [PR #815](https://github.com/strawberry-graphql/strawberry/pull/815/) 0.84.4 - 2021-10-23 ------------------- Field definition uses output of `default_factory` as the GraphQL `default_value`. ```python a_field: list[str] = strawberry.field(default_factory=list) ``` ```graphql aField: [String!]! = [] ``` Contributed by [A. Coady](https://github.com/coady) [PR #1371](https://github.com/strawberry-graphql/strawberry/pull/1371/) 0.84.3 - 2021-10-19 ------------------- This release fixed the typing support for Pyright. Contributed by [Patrick Arminio](https://github.com/patrick91) [PR #1363](https://github.com/strawberry-graphql/strawberry/pull/1363/) 0.84.2 - 2021-10-17 ------------------- This release adds an extra dependency for FastAPI to prevent it being downloaded even when not needed. To install Strawberry with FastAPI support you can do: ``` pip install 'strawberry-graphql[fastapi]' ``` Contributed by [Patrick Arminio](https://github.com/patrick91) [PR #1366](https://github.com/strawberry-graphql/strawberry/pull/1366/) 0.84.1 - 2021-10-17 ------------------- This release fixes the `merge_types` type signature. Contributed by [Guillaume Andreu Sabater](https://github.com/g-as) [PR #1348](https://github.com/strawberry-graphql/strawberry/pull/1348/) 0.84.0 - 2021-10-16 ------------------- This release adds support for FastAPI integration using APIRouter. ```python import strawberry from fastapi import FastAPI from strawberry.fastapi import GraphQLRouter @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "Hello World" schema = strawberry.Schema(Query) graphql_app = GraphQLRouter(schema) app = FastAPI() app.include_router(graphql_app, prefix="/graphql") ``` Contributed by [Jiří Bireš](https://github.com/jiribires) [PR #1291](https://github.com/strawberry-graphql/strawberry/pull/1291/) 0.83.6 - 2021-10-16 ------------------- Improve help texts for CLI to work better on ZSH. Contributed by [Magnus Markling](https://github.com/memark) [PR #1360](https://github.com/strawberry-graphql/strawberry/pull/1360/) 0.83.5 - 2021-10-16 ------------------- Errors encountered in subscriptions will now be logged to the `strawberry.execution` logger as errors encountered in Queries and Mutations are. <3 Contributed by [Michael Ossareh](https://github.com/ossareh) [PR #1316](https://github.com/strawberry-graphql/strawberry/pull/1316/) 0.83.4 - 2021-10-13 ------------------- Add logic to convert arguments of type LazyType. Contributed by [Luke Murray](https://github.com/lukesmurray) [PR #1350](https://github.com/strawberry-graphql/strawberry/pull/1350/) 0.83.3 - 2021-10-13 ------------------- This release fixes a bug where passing scalars in the `scalar_overrides` parameter wasn't being applied consistently. Contributed by [Jonathan Kim](https://github.com/jkimbo) [PR #1212](https://github.com/strawberry-graphql/strawberry/pull/1212/) 0.83.2 - 2021-10-13 ------------------- Pydantic fields' `description` are now copied to the GraphQL schema ```python import pydantic import strawberry class UserModel(pydantic.BaseModel): age: str = pydantic.Field(..., description="Description") @strawberry.experimental.pydantic.type(UserModel) class User: age: strawberry.auto ``` ``` type User { """Description""" age: String! } ``` Contributed by [Guillaume Andreu Sabater](https://github.com/g-as) [PR #1332](https://github.com/strawberry-graphql/strawberry/pull/1332/) 0.83.1 - 2021-10-12 ------------------- We now run our tests against Windows during CI! Contributed by [Michael Ossareh](https://github.com/ossareh) [PR #1321](https://github.com/strawberry-graphql/strawberry/pull/1321/) 0.83.0 - 2021-10-12 ------------------- Add a shortcut to merge queries, mutations. E.g.: ```python import strawberry from strawberry.tools import merge_types @strawberry.type class QueryA: ... @strawberry.type class QueryB: ... ComboQuery = merge_types("ComboQuery", (QueryB, QueryA)) schema = strawberry.Schema(query=ComboQuery) ``` Contributed by [Alexandru Mărășteanu](https://github.com/alexei) [PR #1273](https://github.com/strawberry-graphql/strawberry/pull/1273/) 0.82.2 - 2021-10-12 ------------------- Makes the GraphQLSchema instance accessible from resolvers via the `info` parameter. Contributed by [Aryan Iyappan](https://github.com/codebyaryan) [PR #1311](https://github.com/strawberry-graphql/strawberry/pull/1311/) 0.82.1 - 2021-10-11 ------------------- Fix bug where errors thrown in the on_parse_* extension hooks were being swallowed instead of being propagated. Contributed by [Jonathan Kim](https://github.com/jkimbo) [PR #1324](https://github.com/strawberry-graphql/strawberry/pull/1324/) 0.82.0 - 2021-10-11 ------------------- Adds support for the `auto` type annotation described in #1192 to the Pydantic integration, which allows a user to define the list of fields without having to re-specify the type themselves. This gives better editor and type checker support. If you want to expose every field you can instead pass `all_fields=True` to the decorators and leave the body empty. ```python import pydantic import strawberry from strawberry.experimental.pydantic import auto class User(pydantic.BaseModel): age: int password: str @strawberry.experimental.pydantic.type(User) class UserType: age: auto password: auto ``` Contributed by [Matt Allen](https://github.com/Matt343) [PR #1280](https://github.com/strawberry-graphql/strawberry/pull/1280/) 0.81.0 - 2021-10-04 ------------------- This release adds a safety check on `strawberry.type`, `strawberry.input` and `strawberry.interface` decorators. When you try to use them with an object that is not a class, you will get a nice error message: `strawberry.type can only be used with classes` Contributed by [dependabot](https://github.com/dependabot) [PR #1278](https://github.com/strawberry-graphql/strawberry/pull/1278/) 0.80.2 - 2021-10-01 ------------------- Add `Starlette` to the integrations section on the documentation. Contributed by [Marcelo Trylesinski](https://github.com/Kludex) [PR #1287](https://github.com/strawberry-graphql/strawberry/pull/1287/) 0.80.1 - 2021-10-01 ------------------- This release add support for the upcoming python 3.10 and it adds support for the new union syntax, allowing to declare unions like this: ```python import strawberry @strawberry.type class User: name: str @strawberry.type class Error: code: str @strawberry.type class Query: find_user: User | Error ``` Contributed by [Patrick Arminio](https://github.com/patrick91) [PR #719](https://github.com/strawberry-graphql/strawberry/pull/719/) 0.80.0 - 2021-09-30 ------------------- This release adds support for the `graphql-transport-ws` GraphQL over WebSocket protocol. Previously Strawberry only supported the legacy `graphql-ws` protocol. Developers can decide which protocols they want to accept. The following example shows how to do so using the ASGI integration. By default, both protocols are accepted. Take a look at our GraphQL subscription documentation to learn more. ```python from strawberry.asgi import GraphQL from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL from api.schema import schema app = GraphQL(schema, subscription_protocols=[GRAPHQL_TRANSPORT_WS_PROTOCOL]) ``` Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) [PR #1256](https://github.com/strawberry-graphql/strawberry/pull/1256/) 0.79.0 - 2021-09-29 ------------------- Nests the resolver under the correct span; prior to this change your span would have looked something like: ``` GraphQL Query GraphQL Parsing GraphQL Validation my_resolver my_span_of_interest #1 my_sub_span_of_interest #2 ``` After this change you'll have: ``` GraphQL Query GraphQL Parsing GraphQL Validation GraphQL Resolving: my_resolver my_span_of_interest #1 my_sub_span_of_interest #2 ``` Contributed by [Michael Ossareh](https://github.com/ossareh) [PR #1281](https://github.com/strawberry-graphql/strawberry/pull/1281/) 0.78.2 - 2021-09-27 ------------------- Enhances strawberry.extensions.tracing.opentelemetry to include spans for the Parsing and Validation phases of request handling. These occur before your resovler is called, so now you can see how much time those phases take up! Contributed by [Michael Ossareh](https://github.com/ossareh) [PR #1274](https://github.com/strawberry-graphql/strawberry/pull/1274/) 0.78.1 - 2021-09-26 ------------------- Fix `extensions` argument type definition on `strawberry.Schema` Contributed by [Guillaume Andreu Sabater](https://github.com/g-as) [PR #1276](https://github.com/strawberry-graphql/strawberry/pull/1276/) 0.78.0 - 2021-09-22 ------------------- This release introduces some brand new extensions to help improve the performance of your GraphQL server: * `ParserCache` - Cache the parsing of a query in memory * `ValidationCache` - Cache the validation step of execution For complicated queries these 2 extensions can improve performance by over 50%! Example: ```python import strawberry from strawberry.extensions import ParserCache, ValidationCache schema = strawberry.Schema( Query, extensions=[ ParserCache(), ValidationCache(), ], ) ``` This release also removes the `validate_queries` and `validation_rules` parameters on the `schema.execute*` methods in favour of using the `DisableValidation` and `AddValidationRule` extensions. Contributed by [Jonathan Kim](https://github.com/jkimbo) [PR #1196](https://github.com/strawberry-graphql/strawberry/pull/1196/) 0.77.12 - 2021-09-20 -------------------- This release adds support for Sanic v21 Contributed by [dependabot](https://github.com/dependabot) [PR #1105](https://github.com/strawberry-graphql/strawberry/pull/1105/) 0.77.11 - 2021-09-19 -------------------- Fixes returning "500 Internal Server Error" responses to requests with malformed json when running with ASGI integration. Contributed by [Olesia Grydzhuk](https://github.com/Zlira) [PR #1260](https://github.com/strawberry-graphql/strawberry/pull/1260/) 0.77.10 - 2021-09-16 -------------------- This release adds `python_name` to the `Info` type. Contributed by [Joe Freeman](https://github.com/joefreeman) [PR #1257](https://github.com/strawberry-graphql/strawberry/pull/1257/) 0.77.9 - 2021-09-16 ------------------- Fix the Pydantic conversion method for Enum values, and add a mechanism to specify an interface type when converting from Pydantic. The Pydantic interface is really a base dataclass for the subclasses to extend. When you do the conversion, you have to use `strawberry.experimental.pydantic.interface` to let us know that this type is an interface. You also have to use your converted interface type as the base class for the sub types as normal. Contributed by [Matt Allen](https://github.com/Matt343) [PR #1241](https://github.com/strawberry-graphql/strawberry/pull/1241/) 0.77.8 - 2021-09-14 ------------------- Fixes a bug with the `selected_fields` property on `info` when an operation variable is not defined. Issue [#1248](https://github.com/strawberry-graphql/strawberry/issues/1248). Contributed by [Jonathan Kim](https://github.com/jkimbo) [PR #1249](https://github.com/strawberry-graphql/strawberry/pull/1249/) 0.77.7 - 2021-09-14 ------------------- Fix issues ([#1158][issue1158] and [#1104][issue1104]) where Generics using LazyTypes and Enums would not be properly resolved These now function as expected: # Enum ```python T = TypeVar("T") @strawberry.enum class VehicleMake(Enum): FORD = "ford" TOYOTA = "toyota" HONDA = "honda" @strawberry.type class GenericForEnum(Generic[T]): generic_slot: T @strawberry.type class SomeType: field: GenericForEnum[VehicleMake] ``` # LazyType `another_file.py` ```python @strawberry.type class TypeFromAnotherFile: something: bool ``` `this_file.py` ```python T = TypeVar("T") @strawberry.type class GenericType(Generic[T]): item: T @strawberry.type class RealType: lazy: GenericType[LazyType["TypeFromAnotherFile", "another_file.py"]] ``` [issue1104]: https://github.com/strawberry-graphql/strawberry/issues/1104 [issue1158]: https://github.com/strawberry-graphql/strawberry/issues/1158 Contributed by [ignormies](https://github.com/BryceBeagle) [PR #1235](https://github.com/strawberry-graphql/strawberry/pull/1235/) 0.77.6 - 2021-09-13 ------------------- This release adds fragment and input variable information to the `selected_fields` attribute on the `Info` object. Contributed by [Jonathan Kim](https://github.com/jkimbo) [PR #1213](https://github.com/strawberry-graphql/strawberry/pull/1213/) 0.77.5 - 2021-09-11 ------------------- Fixes a bug in the Pydantic conversion code around `Union` values. Contributed by [Matt Allen](https://github.com/Matt343) [PR #1231](https://github.com/strawberry-graphql/strawberry/pull/1231/) 0.77.4 - 2021-09-11 ------------------- Fixes a bug in the `export-schema` command around the handling of local modules. Contributed by [Matt Allen](https://github.com/Matt343) [PR #1233](https://github.com/strawberry-graphql/strawberry/pull/1233/) 0.77.3 - 2021-09-10 ------------------- Fixes a bug in the Pydantic conversion code around complex `Optional` values. Contributed by [Matt Allen](https://github.com/Matt343) [PR #1229](https://github.com/strawberry-graphql/strawberry/pull/1229/) 0.77.2 - 2021-09-10 ------------------- This release adds a new exception called `InvalidFieldArgument` which is raised when a Union or Interface is used as an argument type. For example this will raise an exception: ```python import strawberry @strawberry.type class Noun: text: str @strawberry.type class Verb: text: str Word = strawberry.union("Word", types=(Noun, Verb)) @strawberry.field def add_word(word: Word) -> bool: ... ``` Contributed by [Mohammad Hossein Yazdani](https://github.com/MAM-SYS) [PR #1222](https://github.com/strawberry-graphql/strawberry/pull/1222/) 0.77.1 - 2021-09-10 ------------------- Fix type resolution when inheriting from types from another module using deferred annotations. Contributed by [Daniel Bowring](https://github.com/dbowring) [PR #1010](https://github.com/strawberry-graphql/strawberry/pull/1010/) 0.77.0 - 2021-09-10 ------------------- This release adds support for Pyright and Pylance, improving the integration with Visual Studio Code! Contributed by [Patrick Arminio](https://github.com/patrick91) [PR #922](https://github.com/strawberry-graphql/strawberry/pull/922/) 0.76.1 - 2021-09-09 ------------------- Change the version constraint of opentelemetry-sdk and opentelemetry-api to <2 Contributed by [Michael Ossareh](https://github.com/ossareh) [PR #1226](https://github.com/strawberry-graphql/strawberry/pull/1226/) 0.76.0 - 2021-09-06 ------------------- This release adds support for enabling subscriptions in GraphiQL on Django by setting a flag `subscriptions_enabled` on the BaseView class. ```python from strawberry.django.views import AsyncGraphQLView from .schema import schema urlpatterns = [ path( "graphql", AsyncGraphQLView.as_view( schema=schema, graphiql=True, subscriptions_enabled=True ), ) ] ``` Contributed by [lijok](https://github.com/lijok) [PR #1215](https://github.com/strawberry-graphql/strawberry/pull/1215/) 0.75.1 - 2021-09-03 ------------------- This release fixes an issue with the MyPy plugin that prevented using TextChoices from django in `strawberry.enum`. Contributed by [Patrick Arminio](https://github.com/patrick91) [PR #1202](https://github.com/strawberry-graphql/strawberry/pull/1202/) 0.75.0 - 2021-09-01 ------------------- This release improves how we deal with custom scalars. Instead of being global they are now scoped to the schema. This allows you to have multiple schemas in the same project with different scalars. Also you can now override the built in scalars with your own custom implementation. Out of the box Strawberry provides you with custom scalars for common Python types like `datetime` and `Decimal`. If you require a custom implementation of one of these built in scalars you can now pass a map of overrides to your schema: ```python from datetime import datetime, timezone import strawberry EpochDateTime = strawberry.scalar( datetime, serialize=lambda value: int(value.timestamp()), parse_value=lambda value: datetime.fromtimestamp(int(value), timezone.utc), ) @strawberry.type class Query: @strawberry.field def current_time(self) -> datetime: return datetime.now() schema = strawberry.Schema( Query, scalar_overrides={ datetime: EpochDateTime, }, ) result = schema.execute_sync("{ currentTime }") assert result.data == {"currentTime": 1628683200} ``` Contributed by [Jonathan Kim](https://github.com/jkimbo) [PR #1147](https://github.com/strawberry-graphql/strawberry/pull/1147/) 0.74.1 - 2021-08-27 ------------------- This release allows to install Strawberry along side `click` version 8. Contributed by [Patrick Arminio](https://github.com/patrick91) [PR #1181](https://github.com/strawberry-graphql/strawberry/pull/1181/) 0.74.0 - 2021-08-27 ------------------- This release add full support for async directives and fixes and issue when using directives and async extensions. ```python @strawberry.type class Query: name: str = "Banana" @strawberry.directive( locations=[DirectiveLocation.FIELD], description="Make string uppercase" ) async def uppercase(value: str): return value.upper() schema = strawberry.Schema(query=Query, directives=[uppercase]) ``` Contributed by [Patrick Arminio](https://github.com/patrick91) [PR #1179](https://github.com/strawberry-graphql/strawberry/pull/1179/) 0.73.9 - 2021-08-26 ------------------- Fix issue where `strawberry.Private` fields on converted Pydantic types were not added to the resulting dataclass. Contributed by [Paul Sud](https://github.com/paul-sud) [PR #1173](https://github.com/strawberry-graphql/strawberry/pull/1173/) 0.73.8 - 2021-08-26 ------------------- This releases fixes a MyPy issue that prevented from using types created with `create_type` as base classes. This is now allowed and doesn't throw any error: ```python import strawberry from strawberry.tools import create_type @strawberry.field def name() -> str: return "foo" MyType = create_type("MyType", [name]) class Query(MyType): ... ``` Contributed by [Patrick Arminio](https://github.com/patrick91) [PR #1175](https://github.com/strawberry-graphql/strawberry/pull/1175/) 0.73.7 - 2021-08-25 ------------------- This release fixes an import error when trying to import `create_type` without having `opentelemetry` installed. Contributed by [Patrick Arminio](https://github.com/patrick91) [PR #1171](https://github.com/strawberry-graphql/strawberry/pull/1171/) 0.73.6 - 2021-08-24 ------------------- This release adds support for the latest version of the optional opentelemetry dependency. Contributed by [Patrick Arminio](https://github.com/patrick91) [PR #1170](https://github.com/strawberry-graphql/strawberry/pull/1170/) 0.73.5 - 2021-08-24 ------------------- This release adds support for the latest version of the optional opentelemetry dependency. Contributed by [Joe Freeman](https://github.com/joefreeman) [PR #1169](https://github.com/strawberry-graphql/strawberry/pull/1169/) 0.73.4 - 2021-08-24 ------------------- This release allows background tasks to be set with the ASGI integration. Tasks can be set on the response in the context, and will then get run after the query result is returned. ```python from starlette.background import BackgroundTask @strawberry.mutation def create_flavour(self, info: strawberry.Info) -> str: info.context["response"].background = BackgroundTask(...) ``` Contributed by [Joe Freeman](https://github.com/joefreeman) [PR #1168](https://github.com/strawberry-graphql/strawberry/pull/1168/) 0.73.3 - 2021-08-24 ------------------- This release caches attributes on the `Info` type which aren't delegated to the core info object. Contributed by [A. Coady](https://github.com/coady) [PR #1167](https://github.com/strawberry-graphql/strawberry/pull/1167/) 0.73.2 - 2021-08-23 ------------------- This releases fixes an issue where you were not allowed to use duck typing and return a different type that the type declared on the field when the type was implementing an interface. Now this works as long as you return a type that has the same shape as the field type. Contributed by [Patrick Arminio](https://github.com/patrick91) [PR #1150](https://github.com/strawberry-graphql/strawberry/pull/1150/) 0.73.1 - 2021-08-23 ------------------- This release improves execution performance significantly by lazy loading attributes on the `Info` type 🏎 Contributed by [Jonathan Kim](https://github.com/jkimbo) [PR #1165](https://github.com/strawberry-graphql/strawberry/pull/1165/) 0.73.0 - 2021-08-22 ------------------- This release adds support for asynchronous hooks to the Strawberry extension-system. All available hooks can now be implemented either synchronously or asynchronously. It's also possible to mix both synchronous and asynchronous hooks within one extension. ```python from strawberry.extensions import Extension class MyExtension(Extension): async def on_request_start(self): print("GraphQL request start") def on_request_end(self): print("GraphQL request end") ``` Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) [PR #1142](https://github.com/strawberry-graphql/strawberry/pull/1142/) 0.72.3 - 2021-08-22 ------------------- This release refactors the reload feature of the built-in debug server. The refactor made the debug server more responsive and allowed us to remove `hupper` from the dependencies. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) [PR #1114](https://github.com/strawberry-graphql/strawberry/pull/1114/) 0.72.2 - 2021-08-22 ------------------- This releases pins graphql-core to only accept patch versions in order to prevent breaking changes since graphql-core doesn't properly follow semantic versioning. Contributed by [Jonathan Kim](https://github.com/jkimbo) [PR #1162](https://github.com/strawberry-graphql/strawberry/pull/1162/) 0.72.1 - 2021-08-18 ------------------- This release improves the default logging format for errors to include more information about the errors. For example it will show were an error was originated in a request: ``` GraphQL request:2:5 1 | query { 2 | example | ^ 3 | } ``` Contributed by [Ivan Gonzalez](https://github.com/scratchmex) [PR #1152](https://github.com/strawberry-graphql/strawberry/pull/1152/) 0.72.0 - 2021-08-18 ------------------- This release adds support for asynchronous permission classes. The only difference to their synchronous counterpart is that the `has_permission` method is asynchronous. ```python from strawberry.permission import BasePermission class IsAuthenticated(BasePermission): message = "User is not authenticated" async def has_permission(self, source, info, **kwargs): return True ``` Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) [PR #1125](https://github.com/strawberry-graphql/strawberry/pull/1125/) 0.71.3 - 2021-08-11 ------------------- Get a field resolver correctly when extending from a pydantic model Contributed by [Jonathan Kim](https://github.com/jkimbo) [PR #1116](https://github.com/strawberry-graphql/strawberry/pull/1116/) 0.71.2 - 2021-08-10 ------------------- This release adds `asgi` as an extra dependencies group for Strawberry. Now you can install the required dependencies needed to use Strawberry with ASGI by running: ``` pip install 'strawberry-graphql[asgi]' ``` Contributed by [A. Coady](https://github.com/coady) [PR #1036](https://github.com/strawberry-graphql/strawberry/pull/1036/) 0.71.1 - 2021-08-09 ------------------- This releases adds `selected_fields` on the `info` objects and it allows to introspect the fields that have been selected in a GraphQL operation. This can become useful to run optimisation based on the queried fields. Contributed by [A. Coady](https://github.com/coady) [PR #874](https://github.com/strawberry-graphql/strawberry/pull/874/) 0.71.0 - 2021-08-08 ------------------- This release adds a query depth limit validation rule so that you can guard against malicious queries: ```python import strawberry from strawberry.schema import default_validation_rules from strawberry.tools import depth_limit_validator # Add the depth limit validator to the list of default validation rules validation_rules = default_validation_rules + [depth_limit_validator(3)] result = schema.execute_sync( """ query MyQuery { user { pets { owner { pets { name } } } } } """, validation_rules=validation_rules, ) assert len(result.errors) == 1 assert result.errors[0].message == "'MyQuery' exceeds maximum operation depth of 3" ``` Contributed by [Jonathan Kim](https://github.com/jkimbo) [PR #1021](https://github.com/strawberry-graphql/strawberry/pull/1021/) 0.70.4 - 2021-08-07 ------------------- Addition of `app.add_websocket_route("/subscriptions", graphql_app)` to FastAPI example docs Contributed by [Anton Melser](https://github.com/AntonOfTheWoods) [PR #1103](https://github.com/strawberry-graphql/strawberry/pull/1103/) 0.70.3 - 2021-08-06 ------------------- This release changes how we map Pydantic fields to types to allow using older version of Pydantic. Contributed by [Patrick Arminio](https://github.com/patrick91) [PR #1071](https://github.com/strawberry-graphql/strawberry/pull/1071/) 0.70.2 - 2021-08-04 ------------------- This release makes the `strawberry server` command inform the user about missing dependencies required by the builtin debug server. Also `hupper` a package only used by said command has been made optional. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) [PR #1107](https://github.com/strawberry-graphql/strawberry/pull/1107/) 0.70.1 - 2021-08-01 ------------------- Switch CDN used to load GraphQLi dependencies from jsdelivr.com to unpkg.com Contributed by [Tim Savage](https://github.com/timsavage) [PR #1096](https://github.com/strawberry-graphql/strawberry/pull/1096/) 0.70.0 - 2021-07-23 ------------------- This release adds support for disabling auto camel casing. It does so by introducing a new configuration parameter to the schema. You can use it like so: ```python @strawberry.type class Query: example_field: str = "Example" schema = strawberry.Schema(query=Query, config=StrawberryConfig(auto_camel_case=False)) ``` Contributed by [Patrick Arminio](https://github.com/patrick91) [PR #798](https://github.com/strawberry-graphql/strawberry/pull/798/) 0.69.4 - 2021-07-23 ------------------- Fix for regression when defining inherited types with explicit fields. Contributed by [A. Coady](https://github.com/coady) [PR #1076](https://github.com/strawberry-graphql/strawberry/pull/1076/) 0.69.3 - 2021-07-21 ------------------- This releases improves the MyPy plugin to be more forgiving of settings like follow_imports = skip which would break the type checking. This is a continuation of the previous release and fixes for type checking issues. Contributed by [Patrick Arminio](https://github.com/patrick91) [PR #1078](https://github.com/strawberry-graphql/strawberry/pull/1078/) 0.69.2 - 2021-07-21 ------------------- This releases improves the MyPy plugin to be more forgiving of settings like `follow_imports = skip` which would break the type checking. Contributed by [Patrick Arminio](https://github.com/patrick91) [PR #1077](https://github.com/strawberry-graphql/strawberry/pull/1077/) 0.69.1 - 2021-07-20 ------------------- This release removes a `TypeGuard` import to prevent errors when using older versions of `typing_extensions`. Contributed by [Patrick Arminio](https://github.com/patrick91) [PR #1074](https://github.com/strawberry-graphql/strawberry/pull/1074/) 0.69.0 - 2021-07-20 ------------------- Refactor of the library's typing internals. Previously, typing was handled individually by fields, arguments, and objects with a hodgepodge of functions to tie it together. This change creates a unified typing system that the object, fields, and arguments each hook into. Mainly replaces the attributes that were stored on StrawberryArgument and StrawberryField with a hierarchy of StrawberryTypes. Introduces `StrawberryAnnotation`, as well as `StrawberryType` and some subclasses, including `StrawberryList`, `StrawberryOptional`, and `StrawberryTypeVar`. This is a breaking change if you were calling the constructor for `StrawberryField`, `StrawberryArgument`, etc. and using arguments such as `is_optional` or `child`. `@strawberry.field` no longer takes an argument called `type_`. It instead takes a `StrawberryAnnotation` called `type_annotation`. Contributed by [ignormies](https://github.com/BryceBeagle) [PR #906](https://github.com/strawberry-graphql/strawberry/pull/906/) 0.68.4 - 2021-07-19 ------------------- This release fixes an issue with the federation printer that prevented using federation directives with types that were implementing interfaces. This is now allowed: ```python @strawberry.interface class SomeInterface: id: strawberry.ID @strawberry.federation.type(keys=["upc"], extend=True) class Product(SomeInterface): upc: str = strawberry.federation.field(external=True) ``` Contributed by [Patrick Arminio](https://github.com/patrick91) [PR #1068](https://github.com/strawberry-graphql/strawberry/pull/1068/) 0.68.3 - 2021-07-15 ------------------- This release changes our `graphiql.html` template to use a specific version of `js-cookie` to prevent a JavaScript error, see: https://github.com/js-cookie/js-cookie/issues/698 Contributed by [星](https://github.com/star2000) [PR #1062](https://github.com/strawberry-graphql/strawberry/pull/1062/) 0.68.2 - 2021-07-07 ------------------- This release fixes a regression that broke strawberry-graphql-django. `Field.get_results` now always receives the `info` argument. Contributed by [Lauri Hintsala](https://github.com/la4de) [PR #1047](https://github.com/strawberry-graphql/strawberry/pull/1047/) 0.68.1 - 2021-07-05 ------------------- This release only changes some internal code to make future improvements easier. Contributed by [Patrick Arminio](https://github.com/patrick91) [PR #1044](https://github.com/strawberry-graphql/strawberry/pull/1044/) 0.68.0 - 2021-07-03 ------------------- Matching the behaviour of `graphql-core`, passing an incorrect ISO string value for a Time, Date or DateTime scalar now raises a `GraphQLError` instead of the original parsing error. The `GraphQLError` will include the error message raised by the string parser, e.g. `Value cannot represent a DateTime: "2021-13-01T09:00:00". month must be in 1..12` 0.67.1 - 2021-06-22 ------------------- Fixes [#1022](https://github.com/strawberry-graphql/strawberry/issues/1022) by making starlette an optional dependency. Contributed by [Marcel Wiegand](https://github.com/mawiegand) [PR #1027](https://github.com/strawberry-graphql/strawberry/pull/1027/) 0.67.0 - 2021-06-17 ------------------- Add ability to specific the graphql name for a resolver argument. E.g., ```python from typing import Annotated import strawberry @strawberry.input class HelloInput: name: str = "world" @strawberry.type class Query: @strawberry.field def hello( self, input_: Annotated[HelloInput, strawberry.argument(name="input")] ) -> str: return f"Hi {input_.name}" ``` Contributed by [Daniel Bowring](https://github.com/dbowring) [PR #1024](https://github.com/strawberry-graphql/strawberry/pull/1024/) 0.66.0 - 2021-06-15 ------------------- This release fixes a bug that was preventing the use of an enum member as the default value for an argument. For example: ```python @strawberry.enum class IceCreamFlavour(Enum): VANILLA = "vanilla" STRAWBERRY = "strawberry" CHOCOLATE = "chocolate" PISTACHIO = "pistachio" @strawberry.mutation def create_flavour(self, flavour: IceCreamFlavour = IceCreamFlavour.STRAWBERRY) -> str: return f"{flavour.name}" ``` Contributed by [Jonathan Kim](https://github.com/jkimbo) [PR #1015](https://github.com/strawberry-graphql/strawberry/pull/1015/) 0.65.5 - 2021-06-15 ------------------- This release reverts the changes made in v0.65.4 that caused an issue leading to circular imports when using the `strawberry-graphql-django` extension package. Contributed by [Lauri Hintsala](https://github.com/la4de) [PR #1019](https://github.com/strawberry-graphql/strawberry/pull/1019/) 0.65.4 - 2021-06-14 ------------------- This release fixes the IDE integration where package `strawberry.django` could not be find by some editors like vscode. Contributed by [Lauri Hintsala](https://github.com/la4de) [PR #994](https://github.com/strawberry-graphql/strawberry/pull/994/) 0.65.3 - 2021-06-09 ------------------- This release fixes the ASGI subscription implementation by handling disconnecting clients properly. Additionally, the ASGI implementation has been internally refactored to match the AIOHTTP implementation. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) [PR #1002](https://github.com/strawberry-graphql/strawberry/pull/1002/) 0.65.2 - 2021-06-06 ------------------- This release fixes a bug in the subscription implementations that prevented clients from selecting one of multiple subscription operations from a query. Client sent messages like the following one are now handled as expected. ```json { "type": "GQL_START", "id": "DEMO", "payload": { "query": "subscription Sub1 { sub1 } subscription Sub2 { sub2 }", "operationName": "Sub2" } } ``` Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) [PR #1000](https://github.com/strawberry-graphql/strawberry/pull/1000/) 0.65.1 - 2021-06-02 ------------------- This release fixes the upload of nested file lists. Among other use cases, having an input type like shown below is now working properly. ```python import typing import strawberry from strawberry.file_uploads import Upload @strawberry.input class FolderInput: files: typing.List[Upload] ``` Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) [PR #989](https://github.com/strawberry-graphql/strawberry/pull/989/) 0.65.0 - 2021-06-01 ------------------- This release extends the file upload support of all integrations to support the upload of file lists. Here is an example how this would work with the ASGI integration. ```python import typing import strawberry from strawberry.file_uploads import Upload @strawberry.type class Mutation: @strawberry.mutation async def read_files(self, files: typing.List[Upload]) -> typing.List[str]: contents = [] for file in files: content = (await file.read()).decode() contents.append(content) return contents ``` Check out the documentation to learn how the same can be done with other integrations. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) [PR #979](https://github.com/strawberry-graphql/strawberry/pull/979/) 0.64.5 - 2021-05-28 ------------------- This release fixes that AIOHTTP subscription requests were not properly separated. This could lead to subscriptions terminating each other. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) [PR #970](https://github.com/strawberry-graphql/strawberry/pull/970/) 0.64.4 - 2021-05-28 ------------------- * Remove usages of `undefined` in favour of `UNSET` * Change the signature of `StrawberryField` to make it easier to instantiate directly. Also change `default_value` argument to `default` * Rename `default_value` to `default` in `StrawberryArgument` Contributed by [Jonathan Kim](https://github.com/jkimbo) [PR #916](https://github.com/strawberry-graphql/strawberry/pull/916/) 0.64.3 - 2021-05-26 ------------------- This release integrates the `strawberry-graphql-django` package into Strawberry core so that it's possible to use the Django extension package directly via the `strawberry.django` namespace. You still need to install `strawberry-graphql-django` if you want to use the extended Django support. See: https://github.com/strawberry-graphql/strawberry-graphql-django Contributed by [Lauri Hintsala](https://github.com/la4de) [PR #949](https://github.com/strawberry-graphql/strawberry/pull/949/) 0.64.2 - 2021-05-26 ------------------- This release fixes that enum values yielded from async generators were not resolved properly. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) [PR #969](https://github.com/strawberry-graphql/strawberry/pull/969/) 0.64.1 - 2021-05-23 ------------------- This release fixes a max recursion depth error in the AIOHTTP subscription implementation. Contributed by [Jonathan Ehwald](https://github.com/DoctorJohn) [PR #966](https://github.com/strawberry-graphql/strawberry/pull/966/) 0.64.0 - 2021-05-22 ------------------- This release adds an extensions field to the `GraphQLHTTPResponse` type and also exposes it in the view's response. This field gets populated by Strawberry extensions: https://strawberry.rocks/docs/guides/extensions#get-results Contributed by [lijok](https://github.com/lijok) [PR #903](https://github.com/strawberry-graphql/strawberry/pull/903/) 0.63.2 - 2021-05-22 ------------------- Add `root_value` to `ExecutionContext` type so that it can be accessed in extensions. Example: ```python import strawberry from strawberry.extensions import Extension class MyExtension(Extension): def on_request_end(self): root_value = self.execution_context.root_value # do something with the root_value ``` Contributed by [Jonathan Kim](https://github.com/jkimbo) [PR #959](https://github.com/strawberry-graphql/strawberry/pull/959/) 0.63.1 - 2021-05-20 ------------------- New deployment process to release new Strawberry releases [Marco Acierno](https://github.com/marcoacierno) [PR #957](https://github.com/strawberry-graphql/strawberry/pull/957/) 0.63.0 - 2021-05-19 ------------------- This release adds extra values to the ExecutionContext object so that it can be used by extensions and the `Schema.process_errors` function. The full ExecutionContext object now looks like this: ```python from graphql import ExecutionResult as GraphQLExecutionResult from graphql.error.graphql_error import GraphQLError from graphql.language import DocumentNode as GraphQLDocumentNode @dataclasses.dataclass class ExecutionContext: query: str context: Any = None variables: Optional[Dict[str, Any]] = None operation_name: Optional[str] = None graphql_document: Optional[GraphQLDocumentNode] = None errors: Optional[List[GraphQLError]] = None result: Optional[GraphQLExecutionResult] = None ``` and can be accessed in any of the extension hooks: ```python from strawberry.extensions import Extension class MyExtension(Extension): def on_request_end(self): result = self.execution_context.result # Do something with the result schema = strawberry.Schema(query=Query, extensions=[MyExtension]) ``` --- Note: This release also removes the creation of an ExecutionContext object in the web framework views. If you were relying on overriding the `get_execution_context` function then you should change it to `get_request_data` and use the `strawberry.http.parse_request_data` function to extract the pieces of data needed from the incoming request. 0.62.1 - 2021-05-19 ------------------- This releases fixes an issue with the debug server that prevented the usage of dataloaders, see: https://github.com/strawberry-graphql/strawberry/issues/940 0.62.0 - 2021-05-19 ------------------- This release adds support for GraphQL subscriptions to the AIOHTTP integration. Subscription support works out of the box and does not require any additional configuration. Here is an example how to get started with subscriptions in general. Note that by specification GraphQL schemas must always define a query, even if only subscriptions are used. ```python import asyncio import typing import strawberry @strawberry.type class Subscription: @strawberry.subscription async def count(self, target: int = 100) -> typing.AsyncGenerator[int, None]: for i in range(target): yield i await asyncio.sleep(0.5) @strawberry.type class Query: @strawberry.field def _unused(self) -> str: return "" schema = strawberry.Schema(subscription=Subscription, query=Query) ``` 0.61.3 - 2021-05-13 ------------------- Fix `@requires(fields: ["email"])` and `@provides(fields: ["name"])` usage on a Federation field You can use `@requires` to specify which fields you need to resolve a field ```python import strawberry @strawberry.federation.type(keys=["id"], extend=True) class Product: id: strawberry.ID = strawberry.federation.field(external=True) code: str = strawberry.federation.field(external=True) @classmethod def resolve_reference(cls, id: strawberry.ID, code: str): return cls(id=id, code=code) @strawberry.federation.field(requires=["code"]) def my_code(self) -> str: return self.code ``` `@provides` can be used to specify what fields are going to be resolved by the service itself without having the Gateway to contact the external service to resolve them. 0.61.2 - 2021-05-08 ------------------- This release adds support for the info param in resolve_reference: ```python @strawberry.federation.type(keys=["upc"]) class Product: upc: str info: str @classmethod def resolve_reference(cls, info, upc): return Product(upc, info) ``` > Note: resolver reference is used when using Federation, similar to [Apollo server's __resolveReference](https://apollographql.com/docs/federation/api/apollo-federation/#__resolvereference) 0.61.1 - 2021-05-05 ------------------- This release extends the `strawberry server` command to allow the specification of a schema symbol name within a module: ```sh strawberry server mypackage.mymodule:myschema ``` The schema symbol name defaults to `schema` making this change backwards compatible. 0.61.0 - 2021-05-04 ------------------- This release adds file upload support to the [Sanic](https://sanicframework.org) integration. No additional configuration is required to enable file upload support. The following example shows how a file upload based mutation could look like: ```python import strawberry from strawberry.file_uploads import Upload @strawberry.type class Mutation: @strawberry.mutation def read_text(self, text_file: Upload) -> str: return text_file.read().decode() ``` 0.60.0 - 2021-05-04 ------------------- This release adds an `export-schema` command to the Strawberry CLI. Using the command you can print your schema definition to your console. Pipes and redirection can be used to store the schema in a file. Example usage: ```sh strawberry export-schema mypackage.mymodule:myschema > schema.graphql ``` 0.59.1 - 2021-05-04 ------------------- This release fixes an issue that prevented using `source` as name of an argument 0.59.0 - 2021-05-03 ------------------- This release adds an [aiohttp](https://github.com/aio-libs/aiohttp) integration for Strawberry. The integration provides a `GraphQLView` class which can be used to integrate Strawberry with aiohttp: ```python import strawberry from aiohttp import web from strawberry.aiohttp.views import GraphQLView @strawberry.type class Query: pass schema = strawberry.Schema(query=Query) app = web.Application() app.router.add_route("*", "/graphql", GraphQLView(schema=schema)) ``` 0.58.0 - 2021-05-03 ------------------- This release adds a function called `create_type` to create a Strawberry type from a list of fields. ```python import strawberry from strawberry.tools import create_type @strawberry.field def hello(info) -> str: return "World" def get_name(info) -> str: return info.context.user.name my_name = strawberry.field(name="myName", resolver=get_name) Query = create_type("Query", [hello, my_name]) schema = strawberry.Schema(query=Query) ``` 0.57.4 - 2021-04-28 ------------------- This release fixes an issue when using nested lists, this now works properly: ```python def get_polygons() -> List[List[float]]: return [[2.0, 6.0]] @strawberry.type class Query: polygons: List[List[float]] = strawberry.field(resolver=get_polygons) schema = strawberry.Schema(query=Query) query = "{ polygons }" result = schema.execute_sync(query, root_value=Query()) ``` 0.57.3 - 2021-04-27 ------------------- This release fixes support for generic types so that now we can also use generics for input types: ```python T = typing.TypeVar("T") @strawberry.input class Input(typing.Generic[T]): field: T @strawberry.type class Query: @strawberry.field def field(self, input: Input[str]) -> str: return input.field ``` 0.57.2 - 2021-04-19 ------------------- This release fixes a bug that prevented from extending a generic type when passing a type, like here: ```python T = typing.TypeVar("T") @strawberry.interface class Node(typing.Generic[T]): id: strawberry.ID def _resolve(self) -> typing.Optional[T]: return None @strawberry.type class Book(Node[str]): name: str @strawberry.type class Query: @strawberry.field def books(self) -> typing.List[Book]: return list() ``` 0.57.1 - 2021-04-17 ------------------- Fix converting pydantic objects to strawberry types using `from_pydantic` when having a falsy value like 0 or ''. 0.57.0 - 2021-04-14 ------------------- Add a `process_errors` method to `strawberry.Schema` which logs all exceptions during execution to a `strawberry.execution` logger. 0.56.3 - 2021-04-13 ------------------- This release fixes the return type value from info argument of resolver. 0.56.2 - 2021-04-07 ------------------- This release improves Pydantic support to support default values and factories. 0.56.1 - 2021-04-06 ------------------- This release fixes the pydantic integration where you couldn't convert objects to pydantic instance when they didn't have a default value. 0.56.0 - 2021-04-05 ------------------- Add --app-dir CLI option to specify where to find the schema module to load when using the debug server. For example if you have a _schema_ module in a _my_app_ package under ./src, then you can run the debug server with it using: ```bash strawberry server --app-dir src my_app.schema ``` 0.55.0 - 2021-04-05 ------------------- Add support for `default` and `default_factory` arguments in `strawberry.field` ```python @strawberry.type class Droid: name: str = strawberry.field(default="R2D2") aka: List[str] = strawberry.field(default_factory=lambda: ["Artoo"]) ``` 0.54.0 - 2021-04-03 ------------------- Internal refactoring. * Renamed `StrawberryArgument` to `StrawberryArgumentAnnotation` * Renamed `ArgumentDefinition` to `StrawberryArgument` * Renamed `ArgumentDefinition(type: ...)` argument to `StrawberryArgument(type_: ...)` 0.53.4 - 2021-04-03 ------------------- Fixed issue with django multipart/form-data uploads 0.53.3 - 2021-04-02 ------------------- Fix issue where StrawberryField.graphql_name would always be camelCased 0.53.2 - 2021-04-01 ------------------- This release fixes an issue with the generated `__eq__` and `__repr__` methods when defining fields with resolvers. This now works properly: ```python @strawberry.type class Query: a: int @strawberry.field def name(self) -> str: return "A" assert Query(1) == Query(1) assert Query(1) != Query(2) ``` 0.53.1 - 2021-03-31 ------------------- Gracefully handle user-induced subscription errors. 0.53.0 - 2021-03-30 ------------------- * `FieldDefinition` has been absorbed into `StrawberryField` and now no longer exists. * `FieldDefinition.origin_name` and `FieldDefinition.name` have been replaced with `StrawberryField.python_name` and `StrawberryField.graphql_name`. This should help alleviate some backend confusion about which should be used for certain situations. * `strawberry.types.type_resolver.resolve_type` has been split into `resolve_type_argument` and `_resolve_type` (for arguments) until `StrawberryType` is implemented to combine them back together. This was done to reduce the scope of this PR and defer changing `ArgumentDefinition` (future `StrawberryArgument`) until a different PR. > Note: The constructor signature for `StrawberryField` has `type_` as an argument > instead of `type` as was the case for `FieldDefinition`. This is done to prevent > shadowing of builtins. > Note: `StrawberryField.name` still exists because of the way dataclass `Field`s work, but is an alias for `StrawberryField.python_name`. 0.52.1 - 2021-03-28 ------------------- Include `field_nodes` in Strawberry info object. 0.52.0 - 2021-03-23 ------------------- Change `get_context` to be async for sanic integration 0.51.1 - 2021-03-22 ------------------- Configures GraphiQL to attach CSRF cookies as request headers sent to the GQL server. 0.51.0 - 2021-03-22 ------------------- Expose Strawberry Info object instead of GraphQLResolveInfo in resolvers 0.50.3 - 2021-03-22 ------------------- Django 3.2 support 0.50.2 - 2021-03-22 ------------------- Raise exception when un-serializable payload is provided to the Django view. 0.50.1 - 2021-03-18 ------------------- This release fixes a regression with the django sending the wrong content type. 0.50.0 - 2021-03-18 ------------------- This release updates get_context in the django integration to also receive a temporal response object that can be used to set headers, cookies and status code. ``` @strawberry.type class Query: @strawberry.field def abc(self, info: strawberry.Info) -> str: info.context.response.status_code = 418 return "ABC" ``` 0.49.2 - 2021-03-18 ------------------- This releases changes how we define resolvers internally, now we have one single resolver for async and sync code. 0.49.1 - 2021-03-14 ------------------- Fix bug when using arguments inside a type that uses typing.Generics 0.49.0 - 2021-03-12 ------------------- This releases updates the ASGI class to make it easier to override `get_http_response`. `get_http_response` has been now removed from strawberry.asgi.http and been moved to be a method on the ASGI class. A new `get_graphiql_response` method has been added to make it easier to provide a different GraphiQL interface. 0.48.3 - 2021-03-11 ------------------- This release updates `get_context` in the asgi integration to also receive a temporal response object that can be used to set headers and cookies. 0.48.2 - 2021-03-09 ------------------- This release fixes a bug when using the debug server and upload a file 0.48.1 - 2021-03-03 ------------------- Fix DataLoader docs typo. 0.48.0 - 2021-03-02 ------------------- # New Features Added support for sanic webserver. # Changelog `ExecutionResult` was erroneously defined twice in the repository. The entry in `strawberry.schema.base` has been removed. If you were using it, switch to using `strawberry.types.ExecutionResult` instead: ```python from strawberry.types import ExecutionResult ``` 0.47.1 - 2021-03-02 ------------------- Enable using .get for django context as well as for the square brackets notation. 0.47.0 - 2021-02-28 ------------------- Enable dot notation for django context request 0.46.0 - 2021-02-26 ------------------- Supporting multipart file uploads on Flask 0.45.4 - 2021-02-16 ------------------- Expose execution info under `strawberry.types.Info` 0.45.3 - 2021-02-08 ------------------- Fixes mypy failing when casting in enum decorator 0.45.2 - 2021-02-08 ------------------- Suggest installing the debug server on the getting started docs, so examples can work without import errors of uvicorn 0.45.1 - 2021-01-31 ------------------- Fix Generic name generation to use the custom name specified in Strawberry if available ```python @strawberry.type(name="AnotherName") class EdgeName: node: str @strawberry.type class Connection(Generic[T]): edge: T ``` will result in `AnotherNameConnection`, and not `EdgeNameConnection` as before. 0.45.0 - 2021-01-27 ------------------- This release add the ability to disable query validation by setting `validate_queries` to `False` ```python import strawberry @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "Hello" schema = strawberry.Schema(Query, validate_queries=validate_queries) ``` 0.44.12 - 2021-01-23 -------------------- This release adds support for MyPy==0.800 0.44.11 - 2021-01-22 -------------------- Fix for a duplicated input types error. 0.44.10 - 2021-01-22 -------------------- Internal codebase refactor. Clean up, consolidate, and standardize the conversion layer between Strawberry types and GraphQL Core types; with room for further future abstraction to support other GraphQL backends. 0.44.9 - 2021-01-22 ------------------- Improves typing when decorating an enum with kwargs like description and name. Adds more mypy tests. 0.44.8 - 2021-01-20 ------------------- This releases fixes a wrong dependency issue 0.44.7 - 2021-01-13 ------------------- Supporting multipart uploads as described here: https://github.com/jaydenseric/graphql-multipart-request-spec for ASGI. 0.44.6 - 2021-01-02 ------------------- Fix Strawberry to handle multiple subscriptions at the same time 0.44.5 - 2020-12-28 ------------------- Pass `execution_context_class` to Schema creation 0.44.4 - 2020-12-27 ------------------- Add support for converting more pydantic types - pydantic.EmailStr - pydantic.AnyUrl - pydantic.AnyHttpUrl - pydantic.HttpUrl - pydantic.PostgresDsn - pydantic.RedisDsn 0.44.3 - 2020-12-16 ------------------- This releases fixes an issue where methods marked as field were removed from the class. 0.44.2 - 2020-11-22 ------------------- Validate the schema when it is created instead of at runtime. 0.44.1 - 2020-11-20 ------------------- This release adds support for strawberry.federation.field under mypy. 0.44.0 - 2020-11-19 ------------------- Creation of a `[debug-server]` extra, which is required to get going quickly with this project! ``` pip install strawberry-graphql ``` Will now install the primary portion of of the framework, allowing you to build your GraphQL schema using the dataclasses pattern. To get going quickly, you can install `[debug-server]` which brings along a server which allows you to develop your API dynamically, assuming your schema is defined in the `app` module: ``` pip install 'strawberry-graphql[debug-server]' strawberry server app ``` Typically, in a production environment, you'd want to bring your own server :) 0.43.2 - 2020-11-19 ------------------- This release fixes an issue when using unions inside generic types, this is now supported: ```python @strawberry.type class Dog: name: str @strawberry.type class Cat: name: str @strawberry.type class Connection(Generic[T]): nodes: List[T] @strawberry.type class Query: connection: Connection[Union[Dog, Cat]] ``` 0.43.1 - 2020-11-18 ------------------- This releases fixes an issue with Strawberry requiring Pydantic even when not used. 0.43.0 - 2020-11-18 ------------------- This release adds support for creating types from Pydantic models. Here's an example: ```python import strawberry from datetime import datetime from typing import List, Optional from pydantic import BaseModel class UserModel(BaseModel): id: int name = "John Doe" signup_ts: Optional[datetime] = None friends: List[int] = [] @strawberry.experimental.pydantic.type( model=UserModel, fields=["id", "name", "friends"] ) class UserType: pass ``` 0.42.7 - 2020-11-18 ------------------- Add some checks to make sure the types passed to `.union` are valid. 0.42.6 - 2020-11-18 ------------------- Fix issue preventing reusing the same resolver for multiple fields, like here: ```python def get_name(self) -> str: return "Name" @strawberry.type class Query: name: str = strawberry.field(resolver=get_name) name_2: str = strawberry.field(resolver=get_name) ``` 0.42.5 - 2020-11-18 ------------------- Another small improvement for mypy, this should prevent mypy from crashing when it can't find a type 0.42.4 - 2020-11-18 ------------------- This release fixes another issue with mypy where it wasn't able to identify strawberry fields. It also now knows that fields with resolvers aren't put in the __init__ method of the class. 0.42.3 - 2020-11-17 ------------------- This release type improves support for strawberry.field in mypy, now we don't get `Attributes without a default cannot follow attributes with one` when using strawberry.field before a type without a default. 0.42.2 - 2020-11-17 ------------------- Bugfix to allow the use of `UNSET` as a default value for arguments. ```python import strawberry from strawberry.arguments import UNSET, is_unset @strawberry.type class Query: @strawberry.field def hello(self, name: Optional[str] = UNSET) -> str: if is_unset(name): return "Hi there" return "Hi {name}" schema = strawberry.Schema(query=Query) result = schema.execute_async("{ hello }") assert result.data == {"hello": "Hi there"} result = schema.execute_async('{ hello(name: "Patrick" }') assert result.data == {"hello": "Hi Patrick"} ``` SDL: ```graphql type Query { hello(name: String): String! } ``` 0.42.1 - 2020-11-17 ------------------- This release improves mypy support for strawberry.field 0.42.0 - 2020-11-17 ------------------- * Completely revamped how resolvers are created, stored, and managed by StrawberryField. Now instead of monkeypatching a `FieldDefinition` object onto the resolver function itself, all resolvers are wrapped inside of a `StrawberryResolver` object with the useful properties. * `arguments.get_arguments_from_resolver` is now the `StrawberryResolver.arguments` property * Added a test to cover a situation where a field is added to a StrawberryType manually using `dataclasses.field` but not annotated. This was previously uncaught. 0.41.1 - 2020-11-14 ------------------- This release fixes an issue with forward types 0.41.0 - 2020-11-06 ------------------- This release adds a built-in dataloader. Example: ```python async def app(): async def idx(keys): return keys loader = DataLoader(load_fn=idx) [value_a, value_b, value_c] = await asyncio.gather( loader.load(1), loader.load(2), loader.load(3), ) assert value_a == 1 assert value_b == 2 assert value_c == 3 ``` 0.40.2 - 2020-11-05 ------------------- Allow interfaces to implement other interfaces. This may be useful if you are using the relay pattern or if you want to model base interfaces that can be extended. Example: ```python import strawberry @strawberry.interface class Error: message: str @strawberry.interface class FieldError(Error): message: str field: str @strawberry.type class PasswordTooShort(FieldError): message: str field: str fix: str ``` Produces the following SDL: ```graphql interface Error { message: String! } interface FieldError implements Error { message: String! field: String! } type PasswordTooShort implements FieldError & Error { message: String! field: String! fix: String! } ``` 0.40.1 - 2020-11-05 ------------------- Fix mypy plugin to handle bug where the `types` argument to `strawberry.union` is passed in as a keyword argument instead of a position one. ```python MyUnion = strawberry.union(types=(TypeA, TypeB), name="MyUnion") ``` 0.40.0 - 2020-11-03 ------------------- This release adds a new AsyncGraphQLView for django. 0.39.4 - 2020-11-02 ------------------- Improve typing for `field` and `StrawberryField`. 0.39.3 - 2020-10-30 ------------------- This release disable implicit re-export of modules. This fixes Strawberry for you if you were using `implicit_reexport = False` in your MyPy config. 0.39.2 - 2020-10-29 ------------------- This fixes the prettier pre-lint check. 0.39.1 - 2020-10-28 ------------------- Fix issue when using `strawberry.enum(module.EnumClass)` in mypy 0.39.0 - 2020-10-27 ------------------- This release adds support to mark a field as deprecated via `deprecation_reason` 0.38.1 - 2020-10-27 ------------------- Set default value to null in the schema when it's set to None 0.38.0 - 2020-10-27 ------------------- Register UUID's as a custom scalar type instead of the ID type. ⚠️ This is a potential breaking change because inputs of type UUID are now parsed as instances of uuid.UUID instead of strings as they were before. 0.37.7 - 2020-10-27 ------------------- This release fixes a bug when returning list in async resolvers 0.37.6 - 2020-10-23 ------------------- This release improves how we check for enums 0.37.5 - 2020-10-23 ------------------- This release improves how we handle enum values when returning lists of enums. 0.37.4 - 2020-10-22 ------------------- This releases adds a workaround to prevent mypy from crashing in specific occasions 0.37.3 - 2020-10-22 ------------------- This release fixes an issue preventing to return enums in lists 0.37.2 - 2020-10-21 ------------------- This release improves support for strawberry.enums when type checking with mypy. 0.37.1 - 2020-10-20 ------------------- Fix ASGI view to call `get_context` during a websocket request 0.37.0 - 2020-10-18 ------------------- Add support for adding a description to field arguments using the [`Annotated`](https://docs.python.org/3/library/typing.html#typing.Annotated) type: ```python from typing import Annotated @strawberry.type class Query: @strawberry.field def user_by_id( id: Annotated[str, strawberry.argument(description="The ID of the user")], ) -> User: ... ``` which results in the following schema: ```graphql type Query { userById( """The ID of the user""" id: String ): User! } ``` **Note:** if you are not using Python v3.9 or greater you will need to import `Annotated` from `typing_extensions` 0.36.4 - 2020-10-17 ------------------- This release adds support for using strawberry.enum as a function with MyPy, this is now valid typed code: ```python from enum import Enum import strawberry class IceCreamFlavour(Enum): VANILLA = "vanilla" STRAWBERRY = "strawberry" CHOCOLATE = "chocolate" Flavour = strawberry.enum(IceCreamFlavour) ``` 0.36.3 - 2020-10-16 ------------------- Add `__str__` to `Schema` to allow printing schema sdl with `str(schema)` 0.36.2 - 2020-10-12 ------------------- Extend support for parsing isoformat datetimes, adding a dependency on the `dateutil` library. For example: "2020-10-12T22:00:00.000Z" can now be parsed as a datetime with a UTC timezone. 0.36.1 - 2020-10-11 ------------------- Add `schema.introspect()` method to return introspection result of the schema. This might be useful for tools like `apollo codegen` or `graphql-voyager` which expect a full json representation of the schema 0.36.0 - 2020-10-06 ------------------- This releases adds a new extension for OpenTelemetry. ```python import asyncio from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, SimpleExportSpanProcessor, ) import strawberry from strawberry.extensions.tracing import OpenTelemetryExtension trace.set_tracer_provider(TracerProvider()) trace.get_tracer_provider().add_span_processor( SimpleExportSpanProcessor(ConsoleSpanExporter()) ) @strawberry.type class User: name: str @strawberry.type class Query: @strawberry.field async def user(self, name: str) -> User: await asyncio.sleep(0.1) return User(name) schema = strawberry.Schema(Query, extensions=[OpenTelemetryExtension]) ``` 0.35.5 - 2020-10-05 ------------------- This release disables tracing for default resolvers and introspection queries 0.35.4 - 2020-10-05 ------------------- This releases allows UNSET to be used anywhere and prevents mypy to report an error. 0.35.3 - 2020-10-05 ------------------- This releases adds support for strawberry.union inside mypy. 0.35.2 - 2020-10-04 ------------------- This release fixes an issue with the extension runner and async resolvers 0.35.1 - 2020-10-02 ------------------- Fixed bug where you couldn't use the same Union type multiple times in a schema. 0.35.0 - 2020-10-02 ------------------- Added `strawberry.Private` type to mark fields as "private" so they don't show up in the GraphQL schema. Example: ```python import strawberry @strawberry.type class User: age: strawberry.Private[int] @strawberry.field def age_in_months(self) -> int: return self.age * 12 ``` 0.34.2 - 2020-10-01 ------------------- Fix typo in type_resolver.py 0.34.1 - 2020-09-30 ------------------- This release fixes an issue with mypy when doing the following: ```python import strawberry @strawberry.type class User: name: str = strawberry.field(description="Example") ``` 0.34.0 - 2020-09-30 ------------------- This release adds support for Apollo Tracing and support for creating Strawberry extensions, here's how you can enable Apollo tracing: ```python from strawberry.extensions.tracing import ApolloTracingExtension schema = strawberry.Schema(query=Query, extensions=[ApolloTracingExtension]) ``` And here's an example of custom extension: ```python from strawberry.extensions import Extension class MyExtension(Extension): def get_results(self): return {"example": "this is an example for an extension"} schema = strawberry.Schema(query=Query, extensions=[MyExtension]) ``` 0.33.1 - 2020-09-25 ------------------- This release fixes an issue when trying to print a type with a UNSET default value 0.33.0 - 2020-09-24 ------------------- * `UnionDefinition` has been renamed to `StrawberryUnion` * `strawberry.union` now returns an instance of `StrawberryUnion` instead of a dynamically generated class instance with a `_union_definition` attribute of type `UnionDefinition`. 0.32.4 - 2020-09-22 ------------------- This release adds the `py.typed` file for better mypy support. 0.32.3 - 2020-09-07 ------------------- This release fixes another issue with extending types. 0.32.2 - 2020-09-07 ------------------- This releases fixes an issue when extending types, now fields should work as they were working before even when extending an existing type. 0.32.1 - 2020-09-06 ------------------- Improves tooling by adding `flake8-eradicate` to `flake8` `pre-commit` hook.. 0.32.0 - 2020-09-06 ------------------- Previously, `strawberry.field` had redundant arguments for the resolver, one for when `strawberry.field` was used as a decorator, and one for when it was used as a function. These are now combined into a single argument. The `f` argument of `strawberry.field` no longer exists. This is a backwards-incompatible change, but should not affect many users. The `f` argument was the first argument for `strawberry.field` and its use was only documented without the keyword. The fix is very straight-forward: replace any `f=` kwarg with `resolver=`. ```python @strawberry.type class Query: my_int: int = strawberry.field(f=lambda: 5) # becomes my_int: int = strawberry.field(resolver=lambda: 5) # no change @strawberry.field def my_float(self) -> float: return 5.5 ``` Other (minor) breaking changes * `MissingArgumentsAnnotationsError`'s message now uses the original Python field name instead of the GraphQL field name. The error can only be thrown while instantiating a strawberry.field, so the Python field name should be more helpful. * As a result, `strawberry.arguments.get_arguments_from_resolver()` now only takes one field -- the `resolver` Callable. * `MissingFieldAnnotationError` is now thrown when a strawberry.field is not type-annotated but also has no resolver to determine its type 0.31.1 - 2020-08-26 ------------------- This release fixes the Flask view that was returning 400 when there were errors in the GraphQL results. Now it always returns 200. 0.31.0 - 2020-08-26 ------------------- Add `process_result` to views for Django, Flask and ASGI. They can be overridden to provide a custom response and also to process results and errors. It also removes `request` from Flask view's `get_root_value` and `get_context` since request in Flask is a global. Django example: ```python # views.py from django.http import HttpRequest from strawberry.django.views import GraphQLView as BaseGraphQLView from strawberry.http import GraphQLHTTPResponse from strawberry.types import ExecutionResult class GraphQLView(BaseGraphQLView): def process_result( self, request: HttpRequest, result: ExecutionResult ) -> GraphQLHTTPResponse: return {"data": result.data, "errors": result.errors or []} ``` Flask example: ```python # views.py from strawberry.flask.views import GraphQLView as BaseGraphQLView from strawberry.http import GraphQLHTTPResponse from strawberry.types import ExecutionResult class GraphQLView(BaseGraphQLView): def process_result(self, result: ExecutionResult) -> GraphQLHTTPResponse: return {"data": result.data, "errors": result.errors or []} ``` ASGI example: ```python from strawberry.asgi import GraphQL as BaseGraphQL from strawberry.http import GraphQLHTTPResponse from strawberry.types import ExecutionResult from starlette.requests import Request from .schema import schema class GraphQL(BaseGraphQLView): async def process_result( self, request: Request, result: ExecutionResult ) -> GraphQLHTTPResponse: return {"data": result.data, "errors": result.errors or []} ``` 0.30.1 - 2020-08-17 ------------------- This releases fixes the check for unset values. 0.30.0 - 2020-08-16 ------------------- Add functions `get_root_value` and `get_context` to views for Django, Flask and ASGI. They can be overridden to provide custom values per request. Django example: ```python # views.py from strawberry.django.views import GraphQLView as BaseGraphQLView class GraphQLView(BaseGraphQLView): def get_context(self, request): return { "request": request, "custom_context_value": "Hi!", } def get_root_value(self, request): return { "custom_root_value": "🍓", } # urls.py from django.urls import path from .views import GraphQLView from .schema import schema urlpatterns = [ path("graphql/", GraphQLView.as_view(schema=schema)), ] ``` Flask example: ```python # views.py from strawberry.flask.views import GraphQLView as BaseGraphQLView class GraphQLView(BaseGraphQLView): def get_context(self, request): return { "request": request, "custom_context_value": "Hi!", } def get_root_value(self, request): return { "custom_root_value": "🍓", } # app.py from flask import Flask from .views import GraphQLView from .schema import schema app = Flask(__name__) app.add_url_rule( "/graphql", view_func=GraphQLView.as_view("graphql_view", schema=schema), ) ``` ASGI example: ```python # app.py from strawberry.asgi import GraphQL as BaseGraphQL from .schema import schema class GraphQL(BaseGraphQLView): async def get_context(self, request): return { "request": request, "custom_context_value": "Hi!", } async def get_root_value(self, request): return { "custom_root_value": "🍓", } app = GraphQL(schema) ``` 0.29.1 - 2020-08-07 ------------------- Support for `default_value` on inputs. Usage: ```python class MyInput: s: Optional[str] = None i: int = 0 ``` ```graphql input MyInput { s: String = null i: Int! = 0 } ``` 0.29.0 - 2020-08-03 ------------------- This release adds support for file uploads within Django. We follow the following spec: https://github.com/jaydenseric/graphql-multipart-request-spec Example: ```python import strawberry from strawberry.file_uploads import Upload @strawberry.type class Mutation: @strawberry.mutation def read_text(self, text_file: Upload) -> str: return text_file.read().decode() ``` 0.28.5 - 2020-08-01 ------------------- Fix issue when reusing an interface 0.28.4 - 2020-07-28 ------------------- Fix issue when using generic types with federation 0.28.3 - 2020-07-27 ------------------- Add support for using lazy types inside generics. 0.28.2 - 2020-07-26 ------------------- This releae add support for UUID as field types. They will be represented as GraphQL ID in the GraphQL schema. 0.28.1 - 2020-07-25 ------------------- This release fixes support for PEP-563, now you can safely use `from __future__ import annotations`, like the following example: ```python from __future__ import annotations @strawberry.type class Query: me: MyType = strawberry.field(name="myself") @strawberry.type class MyType: id: strawberry.ID ``` 0.28.0 - 2020-07-24 ------------------- This releases brings a much needed internal refactor of how we generate GraphQL types from class definitions. Hopefully this will make easier to extend Strawberry in future. There are some internal breaking changes, so if you encounter any issue let us know and well try to help with the migration. In addition to the internal refactor we also fixed some bugs and improved the public api for the schema class. Now you can run queries directly on the schema by running `schema.execute`, `schema.execute_sync` and `schema.subscribe` on your schema. 0.27.5 - 2020-07-22 ------------------- Add websocket object to the subscription context. 0.27.4 - 2020-07-14 ------------------- This PR fixes a bug when declaring multiple non-named union types 0.27.3 - 2020-07-10 ------------------- Optimized signature reflection and added benchmarks. 0.27.2 - 2020-06-11 ------------------- This release fixes an issue when using named union types in generic types, for example using an optional union. This is now properly supported: ```python @strawberry.type class A: a: int @strawberry.type class B: b: int Result = strawberry.union("Result", (A, B)) @strawberry.type class Query: ab: Optional[Result] = None ``` 0.27.1 - 2020-06-11 ------------------- Fix typo in Decimal description 0.27.0 - 2020-06-10 ------------------- This release adds support for decimal type, example: ```python @strawberry.type class Query: @strawberry.field def example_decimal(self) -> Decimal: return Decimal("3.14159") ``` 0.26.3 - 2020-06-10 ------------------- This release disables subscription in GraphiQL where it is not supported. 0.26.2 - 2020-06-03 ------------------- Fixes a bug when using unions and lists together 0.26.1 - 2020-05-22 ------------------- Argument conversion doesn't populate missing args with defaults. ```python @strawberry.field def hello(self, null_or_unset: Optional[str] = UNSET, nullable: str = None) -> None: pass ``` 0.26.0 - 2020-05-21 ------------------- This releases adds experimental support for apollo federation. Here's an example: ```python import strawberry @strawberry.federation.type(extend=True, keys=["id"]) class Campaign: id: strawberry.ID = strawberry.federation.field(external=True) @strawberry.field def title(self) -> str: return f"Title for {self.id}" @classmethod def resolve_reference(cls, id): return Campaign(id) @strawberry.federation.type(extend=True) class Query: @strawberry.field def strawberry(self) -> str: return "🍓" schema = strawberry.federation.Schema(query=Query, types=[Campaign]) ``` 0.25.6 - 2020-05-19 ------------------- Default values make input arguments nullable when the default is None. ```python class Query: @strawberry.field def hello(self, i: int = 0, s: str = None) -> str: return s ``` ```graphql type Query { hello(i: Int! = 0, s: String): String! } ``` 0.25.5 - 2020-05-18 ------------------- Added sentinel value for input parameters that aren't sent by the clients. It checks for when a field is unset. 0.25.4 - 2020-05-18 ------------------- Support for `default_value` on inputs and arguments. Usage: ```python class MyInput: s: Optional[str] i: int = 0 ``` ```graphql input MyInput { s: String i: Int! = 0 } ``` 0.25.3 - 2020-05-17 ------------------- Improves tooling by updating `pre-commit` hooks and adding `pre-commit` to `pyproject.toml`. 0.25.2 - 2020-05-11 ------------------- Add support for setting `root_value` in asgi. Usage: ```python schema = strawberry.Schema(query=Query) app = strawberry.asgi.GraphQL(schema, root_value=Query()) ``` 0.25.1 - 2020-05-08 ------------------- Fix error when a subscription accepted input arguments 0.25.0 - 2020-05-05 ------------------- This release add supports for named unions, now you can create a new union type by writing: ```python Result = strawberry.union("Result", (A, B), description="Example Result") ``` This also improves the support for Union and Generic types, as it was broken before. 0.24.1 - 2020-04-29 ------------------- This release fixes a bug introduced by 0.24.0 0.24.0 - 2020-04-29 ------------------- This releases allows to use resolver without having to specify root and info arguments: ```python def function_resolver() -> str: return "I'm a function resolver" def function_resolver_with_params(x: str) -> str: return f"I'm {x}" @strawberry.type class Query: hello: str = strawberry.field(resolver=function_resolver) hello_with_params: str = strawberry.field(resolver=function_resolver_with_params) @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "I'm a function resolver" @strawberry.field def hello_with_params(self, x: str) -> str: return f"I'm {x}" ``` This makes it easier to reuse existing functions and makes code cleaner when not using info or root. 0.23.3 - 2020-04-29 ------------------- This release fixes the dependency of GraphQL-core 0.23.2 - 2020-04-25 ------------------- This releases updates the _debug server_ to serve the API on '/' as well as '/graphql'. 0.23.1 - 2020-04-20 ------------------- Removes the need for duplicate graphiql template file. 0.23.0 - 2020-04-19 ------------------- This releases replaces the playground with GraphiQL including the GraphiQL explorer plugin. 0.22.0 - 2020-04-19 ------------------- This release adds support for generic types, allowing to reuse types, here's an example: ```python T = typing.TypeVar("T") @strawberry.type class Edge(typing.Generic[T]): cursor: strawberry.ID node: T @strawberry.type class Query: @strawberry.field def int_edge(self, info, **kwargs) -> Edge[int]: return Edge(cursor=strawberry.ID("1"), node=1) ``` 0.21.1 - 2020-03-25 ------------------- Update version of graphql-core to 3.1.0b2 0.21.0 - 2020-02-13 ------------------- Added a Flask view that allows you to query the schema and interact with it via GraphiQL. Usage: ```python # app.py from strawberry.flask.views import GraphQLView from your_project.schema import schema app = Flask(__name__) app.add_url_rule( "/graphql", view_func=GraphQLView.as_view("graphql_view", schema=schema) ) if __name__ == "__main__": app.run(debug=True) ``` 0.20.3 - 2020-02-11 ------------------- Improve datetime, date and time types conversion. Removes aniso dependency and also adds support for python types, so now we can do use python datetime's types instead of our custom scalar types. 0.20.2 - 2020-01-22 ------------------- This version adds support for Django 3.0 0.20.1 - 2020-01-15 ------------------- Fix directives bugs: - Fix autogenerated `return` argument bug - Fix include and skip failure bug 0.20.0 - 2020-01-02 ------------------- This release improves support for permissions (it is a breaking change). Now you will receive the source and the arguments in the `has_permission` method, so you can run more complex permission checks. It also allows to use permissions on fields, here's an example: ```python import strawberry from strawberry.permission import BasePermission class IsAdmin(BasePermission): message = "You are not authorized" def has_permission(self, source, info): return source.name.lower() == "Patrick" or _is_admin(info) @strawberry.type class User: name: str email: str = strawberry.field(permission_classes=[IsAdmin]) @strawberry.type class Query: @strawberry.field(permission_classes=[IsAdmin]) def user(self, info) -> str: return User(name="Patrick", email="example@email.com") ``` 0.19.1 - 2019-12-20 ------------------- This releases removes support for async resolver in django as they causes issues when accessing the databases. 0.19.0 - 2019-12-19 ------------------- This release improves support for django and asgi integration. It allows to use async resolvers when using django. It also changes the status code from 400 to 200 even if there are errors this makes it possible to still use other fields even if one raised an error. We also moved strawberry.contrib.django to strawberry.django, so if you're using the django view make sure you update the paths. 0.18.3 - 2019-12-09 ------------------- Fix missing support for booleans when converting arguments 0.18.2 - 2019-12-09 ------------------- This releases fixes an issue when converting complex input types, now it should support lists of complex types properly. 0.18.1 - 2019-11-03 ------------------- Set `is_type_of` only when the type implements an interface, this allows to return different (but compatible) types in basic cases. 0.18.0 - 2019-10-31 ------------------- Refactored CLI folder structure, importing click commands from a subfolder. Follows click's complex example. 0.17.0 - 2019-10-30 ------------------- Add support for custom GraphQL scalars. 0.16.10 - 2019-10-30 -------------------- Tests are now run on GitHub actions on both python 3.7 and python3.8 🐍 0.16.9 - 2019-10-30 ------------------- Fixed some typos in contributing.md . 0.16.8 - 2019-10-29 ------------------- Fixed some typos in readme.md and contributing.md. 0.16.7 - 2019-10-28 ------------------- Minimal support for registering types without fields and abstract interface querying. 0.16.6 - 2019-10-27 ------------------- Grammar fixes - changed 'corresponding tests, if tests' to 'corresponding tests. If tests' and removed extraneous period from 'Provide specific examples to demonstrate the steps..'. Also made 'Enhancement' lowercase to stay consistent with its usage in documentation and changed 'on the Strawberry' to 'to Strawberry'. 0.16.5 - 2019-10-16 ------------------- Added issue template files (bug_report.md, feature_request.md, other_issues.md) and a pull request template file. 0.16.4 - 2019-10-14 ------------------- Fix execution of async resolvers. 0.16.3 - 2019-10-14 ------------------- Typo fix - changed the spelling from 'follwing' to 'following'. 0.16.2 - 2019-10-03 ------------------- Updated docs to provide reference on how to use Django view. 0.16.1 - 2019-09-29 ------------------- Removed custom representation for Strawberry types, this should make using types much nicer. 0.16.0 - 2019-09-13 ------------------- Switched from `graphql-core-next` dependency to `graphql-core@^3.0.0a0`. 0.15.6 - 2019-09-11 ------------------- Fixes MYPY plugin 0.15.5 - 2019-09-10 ------------------- Add the flake8-bugbear linting plugin to catch likely bugs 0.15.4 - 2019-09-06 ------------------- Fixed conversion of enum when value was falsy. 0.15.3 - 2019-09-06 ------------------- Fixed issue when trying to convert optional arguments to a type 0.15.2 - 2019-09-06 ------------------- Fix issue with converting arguments with optional fields. Thanks to [@sciyoshi](https://github.com/sciyoshi) for the fix! 0.15.1 - 2019-09-05 ------------------- Added a Django view that allows you to query the schema and interact with it via GraphiQL Usage: ```python # Install # pip install "strawberry-graphql[django]" # settings.py INSTALLED_APPS = [ ..., "strawberry.django", ] # urls.py from strawberry.django.views import GraphQLView from your_project.schema import schema urlpatterns = [ path("graphql/", GraphQLView.as_view(schema=schema)), ] ``` 0.15.0 - 2019-09-04 ------------------- This release doesn't add any feature or fixes, but it fixes an issue with checking for release files when submitting PRs ✨. 0.14.4 - 2019-09-01 ------------------- Fixes the conversion of Enums in resolvers, arguments and input types. 0.14.3 - 2019-09-01 ------------------- Add a mypy plugin that enables typechecking Strawberry types 0.14.2 - 2019-08-31 ------------------- Fix List types being converted to Optional GraphQL lists. 0.14.1 - 2019-08-25 ------------------- This release doesn't add any feature or fixes, it only introduces a GitHub Action to let people know how to add a RELEASE.md file when submitting a PR. 0.14.0 - 2019-08-14 ------------------- Added support for defining query directives, example: ```python import strawberry from strawberry.directive import DirectiveLocation @strawberry.type class Query: cake: str = "made_in_switzerland" @strawberry.directive( locations=[DirectiveLocation.FIELD], description="Make string uppercase" ) def uppercase(value: str, example: str): return value.upper() schema = strawberry.Schema(query=Query, directives=[uppercase]) ``` 0.13.4 - 2019-08-01 ------------------- Improve dict_to_type conversion by checking if the field has a different name or case 0.13.3 - 2019-07-23 ------------------- Fix field initialization not allowed when using `strawberry.field` in an `input` type ```python @strawberry.input class Say: what = strawberry.field(is_input=True) ``` 0.13.2 - 2019-07-18 ------------------- Allow the usage of Union types in the mutations ```python @strawberry.type class A: x: int @strawberry.type class B: y: int @strawberry.type class Mutation: @strawberry.mutation def hello(self, info) -> Union[A, B]: return B(y=5) schema = strawberry.Schema(query=A, mutation=Mutation) query = """ mutation { hello { __typename ... on A { x } ... on B { y } } } """ ``` 0.13.1 - 2019-07-17 ------------------- Fix missing fields when extending a class, now we can do this: ```python @strawberry.type class Parent: cheese: str = "swiss" @strawberry.field def friend(self, info) -> str: return "food" @strawberry.type class Schema(Parent): cake: str = "made_in_swiss" ``` 0.13.0 - 2019-07-16 ------------------- This release adds field support for permissions ```python import strawberry from strawberry.permission import BasePermission class IsAdmin(BasePermission): message = "You are not authorized" def has_permission(self, info): return False @strawberry.type class Query: @strawberry.field(permission_classes=[IsAdmin]) def hello(self, info) -> str: return "Hello" ``` 0.12.0 - 2019-06-25 ------------------- This releases adds support for ASGI 3.0 ```python from strawberry.asgi import GraphQL from starlette.applications import Starlette graphql_app = GraphQL(schema_module.schema, debug=True) app = Starlette(debug=True) app.add_route("/graphql", graphql_app) app.add_websocket_route("/graphql", graphql_app) ``` 0.11.0 - 2019-06-07 ------------------- Added support for optional fields with default arguments in the GraphQL schema when default arguments are passed to the resolver. Example: ```python @strawberry.type class Query: @strawberry.field def hello(self, info, name: str = "world") -> str: return name ``` ```graphql type Query { hello(name: String = "world"): String } ``` 0.10.0 - 2019-05-28 ------------------- Fixed issue that was prevent usage of InitVars. Now you can safely use InitVar to prevent fields from showing up in the schema: ```python @strawberry.type class Category: name: str id: InitVar[str] @strawberry.type class Query: @strawberry.field def category(self, info) -> Category: return Category(name="example", id="123") ``` 0.9.1 - 2019-05-25 ------------------ Fixed logo on PyPI 0.9.0 - 2019-05-24 ------------------ Added support for passing resolver functions ```python def resolver(root, info, par: str) -> str: return f"hello {par}" @strawberry.type class Query: example: str = strawberry.field(resolver=resolver) ``` Also we updated some of the dependencies of the project 0.8.0 - 2019-05-09 ------------------ Added support for renaming fields. Example usage: ```python @strawberry.type class Query: example: str = strawberry.field(name="test") ``` 0.7.0 - 2019-05-09 ------------------ Added support for declaring interface by using `@strawberry.interface` Example: ```python @strawberry.interface class Node: id: strawberry.ID ``` 0.6.0 - 2019-05-02 ------------------ This changes field to be lazy by default, allowing to use circular dependencies when declaring types. 0.5.6 - 2019-04-30 ------------------ Improve listing on pypi.org strawberry-graphql-0.287.0/CONTRIBUTING.md000066400000000000000000000142211511033167500177630ustar00rootroot00000000000000# Contributing to Strawberry First off, thanks for taking the time to contribute! The following is a set of guidelines for contributing to Strawberry on GitHub. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. #### Table of contents [How to contribute](#how-to-contribute) - [Reporting bugs](#reporting-bugs) - [Suggesting enhancements](#suggesting-enhancements) - [Contributing to code](#contributing-to-code) ## How to contribute ### Reporting bugs This section guides you through submitting a bug report for Strawberry. Following these guidelines helps maintainers and the community understand your report, reproduce the behavior, and find related reports. Before creating bug reports, please check [this list](#before-submitting-a-bug-report) to be sure that you need to create one. When you are creating a bug report, please include as many details as possible. Make sure you include the Python and Strawberry versions. > **Note:** If you find a **Closed** issue that seems like it is the same thing > that you're experiencing, open a new issue and include a link to the original > issue in the body of your new one. #### Before submitting a bug report - Check that your issue does not already exist in the issue tracker on GitHub. #### How do I submit a bug report? Bugs are tracked on the issue tracker on GitHub where you can create a new one. Explain the problem and include additional details to help maintainers reproduce the problem: - **Use a clear and descriptive title** for the issue to identify the problem. - **Describe the exact steps which reproduce the problem** in as many details as possible. - **Provide specific examples to demonstrate the steps to reproduce the issue**. Include links to files or GitHub projects, or copy-paste-able snippets, which you use in those examples. - **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior. - **Explain which behavior you expected to see instead and why.** Provide more context by answering these questions: - **Did the problem start happening recently** (e.g. after updating to a new version of Strawberry) or was this always a problem? - If the problem started happening recently, **can you reproduce the problem in an older version of Strawberry?** What's the most recent version in which the problem doesn't happen? - **Can you reliably reproduce the issue?** If not, provide details about how often the problem happens and under which conditions it normally happens. Include details about your configuration and environment: - **Which version of Strawberry are you using?** - **Which Python version Strawberry has been installed for?** - **What's the name and version of the OS you're using?** ### Suggesting enhancements This section guides you through submitting an enhancement suggestion for Strawberry, including completely new features and minor improvements to existing functionality. Following these guidelines helps maintainers and the community understand your suggestion and find related suggestions. Before creating enhancement suggestions, please check [this list](#before-submitting-an-enhancement-suggestion) as you might find out that you don't need to create one. When you are creating an enhancement suggestion, please [include as many details as possible](#how-do-i-submit-an-enhancement-suggestion). #### Before submitting an enhancement suggestion - Check that your issue does not already exist in the issue tracker on GitHub. #### How do I submit an enhancement suggestion? Enhancement suggestions are tracked on the project's issue tracker on GitHub where you can create a new one and provide the following information: - **Use a clear and descriptive title** for the issue to identify the suggestion. - **Provide a step-by-step description of the suggested enhancement** in as many details as possible. - **Provide specific examples to demonstrate the steps**. - **Describe the current behavior** and **explain which behavior you expected to see instead** and why. ### Contributing to code > This section is about contributing to [Strawberry Python library](https://github.com/strawberry-graphql/strawberry). #### Local development You will need Poetry to start contributing to Strawberry. Refer to the [documentation](https://poetry.eustace.io/docs/#introduction) to start using Poetry. You will first need to clone the repository using `git` and place yourself in its directory: ```shell $ git clone git@github.com:strawberry-graphql/strawberry.git $ cd strawberry ``` Now, you will need to install the required dependencies for Strawberry and be sure that the current tests are passing on your machine: ```shell $ poetry install $ poetry run pytest $ poetry run mypy ``` Some tests are known to be inconsistent. (The fix is in progress.) These tests are marked with the `pytest.mark.flaky` marker. Strawberry uses the [black](https://github.com/ambv/black) coding style and you must ensure that your code follows it. If not, the CI will fail and your Pull Request will not be merged. To make sure that you don't accidentally commit code that does not follow the coding style, you can install a pre-commit hook that will check that everything is in order: ```shell $ poetry run pre-commit install ``` Your code must always be accompanied by corresponding tests. If tests are not present, your code will not be merged. #### Pull requests - Be sure that your pull request contains tests that cover the changed or added code. - If your changes warrant a documentation change, the pull request must also update the documentation. ##### RELEASE.md files When you submit a PR, make sure to include a RELEASE.md file. We use that to automatically do releases here on GitHub and, most importantly, to PyPI! So as soon as your PR is merged, a release will be made. Here's an example of RELEASE.md: ```text Release type: patch Description of the changes, ideally with some examples, if adding a new feature. ``` Release type can be one of patch, minor or major. We use [semver](https://semver.org/), so make sure to pick the appropriate type. If in doubt feel free to ask :) strawberry-graphql-0.287.0/LICENSE000066400000000000000000000020601511033167500165350ustar00rootroot00000000000000MIT License Copyright (c) 2018 Patrick Arminio Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. strawberry-graphql-0.287.0/README.md000066400000000000000000000074161511033167500170210ustar00rootroot00000000000000 # Strawberry GraphQL > Python GraphQL library based on dataclasses [![CircleCI](https://img.shields.io/circleci/token/307b40d5e152e074d34f84d30d226376a15667d5/project/github/strawberry-graphql/strawberry/main.svg?style=for-the-badge)](https://circleci.com/gh/strawberry-graphql/strawberry/tree/main) [![Discord](https://img.shields.io/discord/689806334337482765?label=discord&logo=discord&logoColor=white&style=for-the-badge&color=blue)](https://discord.gg/ZkRTEJQ) [![PyPI](https://img.shields.io/pypi/v/strawberry-graphql?logo=pypi&logoColor=white&style=for-the-badge)](https://pypi.org/project/strawberry-graphql/) ## Installation ( Quick Start ) The quick start method provides a server and CLI to get going quickly. Install with: ```shell pip install "strawberry-graphql[cli]" ``` ## Getting Started Create a file called `app.py` with the following code: ```python import strawberry @strawberry.type class User: name: str age: int @strawberry.type class Query: @strawberry.field def user(self) -> User: return User(name="Patrick", age=100) schema = strawberry.Schema(query=Query) ``` This will create a GraphQL schema defining a `User` type and a single query field `user` that will return a hardcoded user. To serve the schema using the dev server run the following command: ```shell strawberry dev app ``` Open the dev server by clicking on the following link: [http://0.0.0.0:8000/graphql](http://0.0.0.0:8000/graphql) This will open GraphiQL where you can test the API. ### Type-checking Strawberry comes with a [mypy] plugin that enables statically type-checking your GraphQL schema. To enable it, add the following lines to your `mypy.ini` configuration: ```ini [mypy] plugins = strawberry.ext.mypy_plugin ``` [mypy]: http://www.mypy-lang.org/ ### Django Integration A Django view is provided for adding a GraphQL endpoint to your application. 1. Add the app to your `INSTALLED_APPS`. ```python INSTALLED_APPS = [ ..., # your other apps "strawberry.django", ] ``` 2. Add the view to your `urls.py` file. ```python from strawberry.django.views import GraphQLView from .schema import schema urlpatterns = [ ..., path("graphql", GraphQLView.as_view(schema=schema)), ] ``` ## Examples * [Various examples on how to use Strawberry](https://github.com/strawberry-graphql/examples) * [Full stack example using Starlette, SQLAlchemy, Typescript codegen and Next.js](https://github.com/jokull/python-ts-graphql-demo) * [Quart + Strawberry tutorial](https://github.com/rockyburt/Ketchup) ## Contributing We use [poetry](https://github.com/sdispater/poetry) to manage dependencies, to get started follow these steps: ```shell git clone https://github.com/strawberry-graphql/strawberry cd strawberry poetry install poetry run pytest ``` For all further detail, check out the [Contributing Page](CONTRIBUTING.md) ### Pre commit We have a configuration for [pre-commit](https://github.com/pre-commit/pre-commit), to add the hook run the following command: ```shell pre-commit install ``` ## Links - Project homepage: https://strawberry.rocks - Repository: https://github.com/strawberry-graphql/strawberry - Issue tracker: https://github.com/strawberry-graphql/strawberry/issues - In case of sensitive bugs like security vulnerabilities, please contact patrick.arminio@gmail.com directly instead of using the issue tracker. We value your effort to improve the security and privacy of this project! ## Licensing The code in this project is licensed under MIT license. See [LICENSE](./LICENSE) for more information. ![Recent Activity](https://images.repography.com/0/strawberry-graphql/strawberry/recent-activity/d751713988987e9331980363e24189ce.svg) strawberry-graphql-0.287.0/docs/000077500000000000000000000000001511033167500164625ustar00rootroot00000000000000strawberry-graphql-0.287.0/docs/README.md000066400000000000000000000064541511033167500177520ustar00rootroot00000000000000--- title: Strawberry docs --- # Strawberry docs - [Getting started](./index.md) ## General - [Schema basics](./general/schema-basics.md) - [Queries](./general/queries.md) - [Mutations](./general/mutations.md) - [Subscriptions](./general/subscriptions.md) - [Multipart Subscriptions](./general/multipart-subscriptions.md) - [Errors](./errors) - [Upgrading Strawberry](./general/upgrades.md) - [Breaking changes](./breaking-changes.md) - [FAQ](./faq.md) ## Types - [Schema](./types/schema.md) - [Operation Directives](./types/operation-directives.md) - [Schema Directives](./types/schema-directives.md) - [Schema configurations](./types/schema-configurations.md) - [Scalars](./types/scalars.md) - [Object types](./types/object-types.md) - [Interfaces](./types/interfaces.md) - [Input types](./types/input-types.md) - [Enums](./types/enums.md) - [Generics](./types/generics.md) - [Resolvers](./types/resolvers.md) - [Union types](./types/union.md) - [Lazy types](./types/lazy.md) - [Exceptions](./types/exceptions.md) - [Private/External Fields](./types/private.md) - [Defer and Stream](./types/defer-and-stream.md) ## Codegen - [Schema codegen](./codegen/schema-codegen.md) - [Query codegen](./codegen/query-codegen.md) ## Guides - [Accessing parent data](./guides/accessing-parent-data.md) - [Authentication](./guides/authentication.md) - [DataLoaders](./guides/dataloaders.md) - [Dealing with errors](./guides/errors.md) - [Federation](./guides/federation.md) - [Federation V1](./guides/federation-v1.md) - [Relay](./guides/relay.md) - [File upload](./guides/file-upload.md) - [Pagination](./guides/pagination/overview.md) - [Implementing Offset Pagination](./guides/pagination/offset-based.md) - [Implementing Cursor Pagination](./guides/pagination/cursor-based.md) - [Implementing the Connection specification](./guides/pagination/connections.md) - [Permissions](./guides/permissions.md) - [Built-in server](./guides/server.md) - [Tools](./guides/tools.md) - [Schema export](./guides/schema-export.md) - [Convert to dictionary](./guides/convert-to-dictionary.md) - [Query Batching](./guides/query-batching.md) ## Extensions - [Introduction](./extensions) - [Schema extensions](./guides/custom-extensions.md) - [Field extensions](./guides/field-extensions.md) ## Editor integration - [Mypy](./editors/mypy.md) - [Visual Studio Code](./editors/vscode.md) ## Concepts - [Async](./concepts/async.md) - [Type hints](./concepts/typings.md) ## Integrations - [AIOHTTP](./integrations/aiohttp.md) - [ASGI](./integrations/asgi.md) - [Django](./integrations/django.md) - [Channels](./integrations/channels.md) - [FastAPI](./integrations/fastapi.md) - [Flask](./integrations/flask.md) - [Quart](./integrations/quart.md) - [Sanic](./integrations/sanic.md) - [Chalice](./integrations/chalice.md) - [Starlette](./integrations/starlette.md) - [Litestar](./integrations/litestar.md) - [Creating an integration](./integrations/creating-an-integration.md) - [Pydantic **experimental**](./integrations/pydantic.md) ## Federation - [Introduction](./federation/introduction.md) - [Entities](./federation/entities.md) - [Entity interfaces](./federation/entity-interfaces.md) - [Custom directives](./federation/custom_directives.md) ## Operations - [Deployment](./operations/deployment.md) - [Testing](./operations/testing.md) - [Tracing](./operations/tracing.md) strawberry-graphql-0.287.0/docs/_test.md000066400000000000000000000035401511033167500201240ustar00rootroot00000000000000--- title: Test doc page toc: true --- # This is a test doc page Some examples of things you can do in docs. ## Code highlighting Code blocks now support: ### Highlighting words individually ```python highlight=strawberry,str import strawberry @strawberry.type class X: name: str ``` ### Highlighting lines ```python lines=1-4 import strawberry @strawberry.type class X: name: str ``` ### Add notes to code comments This is probably not implemented in the best way, but for now it works: ```python import strawberry # ^^^^^^^^^^ # This is a note about this line # this is a standard comment @strawberry.type class X: name: str ``` Strawberry is a cool library ### Split code blocks You can show two different code blocks next to each other (useful when comparing the GraphQL schema against the Python definition): ```python import strawberry @strawberry.type class Query: @strawberry.field def ping(self) -> str: return "pong" ``` ```graphql type Query { ping: String! } ``` or when showing the request and response to a query: ```graphql+response { ping } --- { "data": { "ping": "pong" } } ``` ## Call out blocks This is a tip. Useful information is contained here. This is a note. Something that you should know about. This is a warning. Something that you should be careful about. ## Blockquote > This is a quote ```mermaid sequenceDiagram Alice ->> Bob: Hello Bob, how are you? Bob-->>John: How about you John? Bob--x Alice: I am good thanks! Bob-x John: I am good thanks! Note right of John: Bob thinks a long
long time, so long
that the text does
not fit on a row. Bob-->Alice: Checking with John... Alice->John: Yes... John, how are you? ``` strawberry-graphql-0.287.0/docs/breaking-changes.md000066400000000000000000000022511511033167500221740ustar00rootroot00000000000000--- title: List of breaking changes and deprecations --- # List of breaking changes and deprecations - [Version 0.285.0 - 10 November 2025](./breaking-changes/0.285.0.md) - [Version 0.285.0 - 6 October 2025](./breaking-changes/0.283.0.md) - [Version 0.279.0 - 19 August 2025](./breaking-changes/0.279.0.md) - [Version 0.278.1 - 5 August 2025](./breaking-changes/0.278.1.md) - [Version 0.268.0 - 10 May 2025](./breaking-changes/0.268.0.md) - [Version 0.249.0 - 18 November 2024](./breaking-changes/0.249.0.md) - [Version 0.243.0 - 25 September 2024](./breaking-changes/0.243.0.md) - [Version 0.240.0 - 10 September 2024](./breaking-changes/0.240.0.md) - [Version 0.236.0 - 17 July 2024](./breaking-changes/0.236.0.md) - [Version 0.233.0 - 29 May 2024](./breaking-changes/0.233.0.md) - [Version 0.217.0 - 18 December 2023](./breaking-changes/0.217.0.md) - [Version 0.213.0 - 8 November 2023](./breaking-changes/0.213.0.md) - [Version 0.180.0 - 31 May 2023](./breaking-changes/0.180.0.md) - [Version 0.169.0 - 5 April 2023](./breaking-changes/0.169.0.md) - [Version 0.159.0 - 22 February 2023](./breaking-changes/0.159.0.md) - [Version 0.146.0 - 5 December 2022](./breaking-changes/0.146.0.md) strawberry-graphql-0.287.0/docs/breaking-changes/000077500000000000000000000000001511033167500216525ustar00rootroot00000000000000strawberry-graphql-0.287.0/docs/breaking-changes/0.146.0.md000066400000000000000000000037651511033167500230150ustar00rootroot00000000000000--- title: 0.146.0 Breaking Changes slug: breaking-changes/0.146.0 --- # v0.146.0 Breaking Changes - 5 December 2022 This release introduces a couple of breaking changes to the Sanic integration. ## `process_result` is now async and accepts the request as the first argument If you customized the `process_result` function, you will need to update your code to make it async and accept the request as the first argument. For example: ```python from strawberry.sanic.views import GraphQLView from strawberry.http import GraphQLHTTPResponse, process_result from strawberry.types import ExecutionResult from sanic.request import Request from graphql.error.graphql_error import format_error as format_graphql_error class MyGraphQLView(GraphQLView): async def process_result( self, request: Request, result: ExecutionResult ) -> GraphQLHTTPResponse: if result.errors: result.errors = [format_graphql_error(err) for err in result.errors] return process_result(data) ``` ## `get_context` now receives also the response as the second argument If you customized the `get_context` function, you will need to update your code to accept the response as the second argument. The response argument allows you to set cookies and other headers. For example: ```python from strawberry.sanic.views import GraphQLView from strawberry.sanic.context import StrawberrySanicContext from strawberry.http.temporal_response import TemporalResponse from sanic.request import Request class MyGraphQLView(GraphQLView): async def get_context( self, request: Request, response: TemporalResponse ) -> StrawberrySanicContext: return {"request": request, "response": response} ``` # Deprecations ## Context value is now a dictionary The context value is now a dictionary instead of a custom class. This means that you should access the context value using the `["key"]` syntax instead of the `.key` syntax. The `.key` syntax is still supported but will be removed in future releases. strawberry-graphql-0.287.0/docs/breaking-changes/0.159.0.md000066400000000000000000000022161511033167500230070ustar00rootroot00000000000000--- title: 0.159.0 Deprecations slug: breaking-changes/0.159.0 --- # v0.159.0 Introduces changes how extension hooks are defined This release changes how extension hooks are defined. The new style hooks are more flexible and allow to run code before and after the execution. The old style hooks are still supported but will be removed in future releases. # How to upgrade ## Before: ```python class MyExtension(Extension): def on_executing_start(self): ... def on_executing_end(self): ... ``` ## After ```python class MyExtension(Extension): def on_execute(self): # code before the execution starts yield # code after the execution ends ``` # Migration guide See the following table for a mapping between the old and new hooks. | Old hook | New hook | | ------------------- | ------------ | | on_request_start | on_operation | | on_request_end | on_operation | | on_validation_start | on_validate | | on_validation_end | on_validate | | on_parsing_start | on_parse | | on_parsing_end | on_parse | | on_executing_start | on_execute | | on_executing_end | on_execute | strawberry-graphql-0.287.0/docs/breaking-changes/0.169.0.md000066400000000000000000000013521511033167500230100ustar00rootroot00000000000000--- title: 0.169.0 Breaking changes slug: breaking-changes/0.169.0 --- # v0.169.0 Introduces a couple of breaking changes in the HTTP integrations ## Flask Both `get_root_value` and `get_context` now receive the request as a parameter. If you're customizing these methods you can change the signature to: ```python def get_root_value(self, request: Request) -> Any: ... def get_context(self, request: Request, response: Response) -> Any: ... ``` The same is true for the async version of the view. ## Sanic The `get_root_value` method now receives the request as a parameter and it is async. If you're customizing this method you can change the signature to: ```python async def get_root_value(self, request: Request) -> Any: ... ``` strawberry-graphql-0.287.0/docs/breaking-changes/0.180.0.md000066400000000000000000000017261511033167500230060ustar00rootroot00000000000000--- title: 0.180.0 Breaking changes slug: breaking-changes/0.180.0 --- # v0.180.0 introduces a breaking change for the Django Channels HTTP integration The context object is now a `dict`. This means that you should access the context value using the `["key"]` syntax instead of the `.key` syntax. For the HTTP integration, there is also no `ws` key anymore and `request` is a custom request object containing the full request instead of a `GraphQLHTTPConsumer` instance. If you need to access the `GraphQLHTTPConsumer` instance in a HTTP connection, you can access it via `info.context["request"].consumer`. For the WebSockets integration, the context keys did not change, e.g. the values for `info.context["ws"]`, `info.context["request"]` and `info.context["connection_params"]` are the same as before. If you still want to use the `.key` syntax, you can override `get_context()` to return a custom dataclass there. See the Channels integration documentation for an example. strawberry-graphql-0.287.0/docs/breaking-changes/0.213.0.md000066400000000000000000000012201511033167500227700ustar00rootroot00000000000000--- title: 0.213.0 Deprecation slug: breaking-changes/0.213.0 --- # v0.213.0 introduces a deprecation for `graphiql` parameter All HTTP integration now will use `graphql_ide` instead of `graphiql` parameter. If you're not using the `graphiql` parameter, you can safely ignore this deprecation. If you're using the `graphiql` parameter, you should change it to `graphql_ide` instead. Here's an example of the changes: ```diff -graphql_app = GraphQLRouter(schema, graphiql=True) +graphql_app = GraphQLRouter(schema, graphql_ide="graphiql") -graphql_app = GraphQLRouter(schema, graphiql=False) +graphql_app = GraphQLRouter(schema, graphql_ide=None) ``` strawberry-graphql-0.287.0/docs/breaking-changes/0.217.0.md000066400000000000000000000014551511033167500230060ustar00rootroot00000000000000--- title: 0.217 Breaking Changes slug: breaking-changes/0.217.0 --- # v0.217.0 changes how kwargs are passed to `has_permission` method Previously the `kwargs` argument keys for the `has_permission` method were using camel casing (depending on your schema configuration), now they will always follow the python name defined in your resolvers. ```python class IsAuthorized(BasePermission): message = "User is not authorized" def has_permission( self, source, info, **kwargs: typing.Any ) -> bool: # pragma: no cover # kwargs will have a key called "a_key" # instead of `aKey` return False @strawberry.type class Query: @strawberry.field(permission_classes=[IsAuthorized]) def name(self, a_key: str) -> str: # pragma: no cover return "Erik" ``` strawberry-graphql-0.287.0/docs/breaking-changes/0.233.0.md000066400000000000000000000013611511033167500230000ustar00rootroot00000000000000--- title: 0.233.0 Breaking Changes slug: breaking-changes/0.233.0 --- # v0.233.0 changes the `info` argument in `resolve_reference` in Federation In this release we have updated the `info` object passed to the `resolve_reference` function in Federation to be a `strawberry.Info` object instead of the one coming from GraphQL-core. If you need to access the original `info` object you can do so by accessing the `_raw_info` attribute. ```python import strawberry @strawberry.federation.type(keys=["upc"]) class Product: upc: str @classmethod def resolve_reference(cls, info: strawberry.Info, upc: str) -> "Product": # Access the original info object original_info = info._raw_info return Product(upc=upc) ``` strawberry-graphql-0.287.0/docs/breaking-changes/0.236.0.md000066400000000000000000000010361511033167500230020ustar00rootroot00000000000000--- title: 0.236.0 Breaking Changes slug: breaking-changes/0.236.0 --- # v0.236.0 changes some of the imports This release changes the location of some files in the codebase, this is to make the codebase more organized and easier to navigate. Technically most of these changes should not affect you, but if you were importing some of the files directly you will need to update the imports. We created a codemod to help you with that, feel free to try and submit bugs if we missed something. ```bash strawberry upgrade update-imports ``` strawberry-graphql-0.287.0/docs/breaking-changes/0.240.0.md000066400000000000000000000017051511033167500230000ustar00rootroot00000000000000--- title: 0.240.0 Breaking Changes slug: breaking-changes/0.240.0 --- # v0.240.0 updates `Schema.subscribe`'s signature In order to support schema extensions in subscriptions and errors that can be raised before the execution of the subscription, we had to update the signature of `Schema.subscribe`. Previously it was: ```python async def subscribe( self, query: str, variable_values: Optional[Dict[str, Any]] = None, context_value: Optional[Any] = None, root_value: Optional[Any] = None, operation_name: Optional[str] = None, ) -> Union[AsyncIterator[GraphQLExecutionResult], GraphQLExecutionResult]: ``` Now it is: ```python async def subscribe( self, query: Optional[str], variable_values: Optional[Dict[str, Any]] = None, context_value: Optional[Any] = None, root_value: Optional[Any] = None, operation_name: Optional[str] = None, ) -> Union[AsyncGenerator[ExecutionResult, None], PreExecutionError]: ``` strawberry-graphql-0.287.0/docs/breaking-changes/0.243.0.md000066400000000000000000000040761511033167500230070ustar00rootroot00000000000000--- title: 0.243.0 Breaking Changes slug: breaking-changes/0.243.0 --- # v0.243.0 Breaking Changes Release v0.243.0 comes with two breaking changes regarding multipart file uploads and Django CSRF protection. ## Multipart uploads disabled by default Previously, support for uploads via the [GraphQL multipart request specification](https://github.com/jaydenseric/graphql-multipart-request-spec) was enabled by default. This implicitly required Strawberry users to consider the [security implications outlined in the GraphQL Multipart Request Specification](https://github.com/jaydenseric/graphql-multipart-request-spec/blob/master/readme.md#security). Given that most Strawberry users were likely not aware of this, we're making multipart file upload support stictly opt-in via a new `multipart_uploads_enabled` view settings. To enable multipart upload support for your Strawberry view integration, please follow the updated integration guides and enable appropriate security measurements for your server. ## Django CSRF protection enabled Previously, the Strawberry Django view integration was internally exempted from Django's built-in CSRF protection (i.e, the `CsrfViewMiddleware` middleware). While this is how many GraphQL APIs operate, implicitly addded exemptions can lead to security vulnerabilities. Instead, we delegate the decision of adding an CSRF exemption to users now. Note that having the CSRF protection enabled on your Strawberry Django view potentially requires all your clients to send an CSRF token with every request. You can learn more about this in the official Django [Cross Site Request Forgery protection documentation](https://docs.djangoproject.com/en/dev/ref/csrf/). To restore the behaviour of the integration before this release, you can add the `csrf_exempt` decorator provided by Django yourself: ```python from django.urls import path from django.views.decorators.csrf import csrf_exempt from strawberry.django.views import GraphQLView from api.schema import schema urlpatterns = [ path("graphql/", csrf_exempt(GraphQLView.as_view(schema=schema))), ] ``` strawberry-graphql-0.287.0/docs/breaking-changes/0.249.0.md000066400000000000000000000006661511033167500230160ustar00rootroot00000000000000--- title: 0.249.0 Breaking Changes slug: breaking-changes/0.249.0 --- # v0.249.0 Breaking Changes After a year-long deprecation period, the `SentryTracingExtension` has been removed in favor of the official Sentry SDK integration. To migrate, remove the `SentryTracingExtension` from your Strawberry schema and then follow the [official Sentry SDK integration guide](https://docs.sentry.io/platforms/python/integrations/strawberry/). strawberry-graphql-0.287.0/docs/breaking-changes/0.251.0.md000066400000000000000000000013051511033167500227760ustar00rootroot00000000000000--- title: 0.251.0 Breaking Changes slug: breaking-changes/0.251.0 --- # v0.251.0 Breaking Changes We slightly changed the signature of the `encode_json` method used to customize the JSON encoder used by our HTTP views. Originally, the method was only meant to encode HTTP response data. Starting with this release, it's also used to encode WebSocket messages. Previously, the method signature was: ```python def encode_json(self, response_data: GraphQLHTTPResponse) -> str: ... ``` To upgrade your code, change the method signature to the following and make sure your method can handle the same inputs as the built-in `json.dumps` method: ```python def encode_json(self, data: object) -> str: ... ``` strawberry-graphql-0.287.0/docs/breaking-changes/0.268.0.md000066400000000000000000000032231511033167500230070ustar00rootroot00000000000000--- title: 0.268.0 Breaking Changes slug: breaking-changes/0.268.0 --- # v0.268.0 This release changes GlobalIDs to ID in the GraphQL schema, now instead of having `GlobalID` as type when using `relay.Node` you'll get `ID`. The runtime behaviour is still the same. If you need to use the previous behaviour you can use the following config: ```python schema = strawberry.Schema( query=Query, config=StrawberryConfig(relay_use_legacy_global_id=True) ) ``` This release renames the generated type from `GlobalID` to `ID` in the GraphQL schema. This means that when using `relay.Node`, like in this example: ```python @strawberry.type class Fruit(relay.Node): code: relay.NodeID[int] name: str ``` You'd create a GraphQL type that looks like this: ```graphql type Fruit implements Node { id: ID! name: String! } ``` while previously you'd get this: ```graphql type Fruit implements Node { id: GlobalID! name: String! } ``` The runtime behaviour is still the same, so if you want to use `GlobalID` in Python code, you can still do so, for example: ```python @strawberry.type class Mutation: @strawberry.mutation @staticmethod async def update_fruit_weight(id: relay.GlobalID, weight: float) -> Fruit: # while `id` is a GraphQL `ID` type, here is still an instance of `relay.GlobalID` fruit = await id.resolve_node(info, ensure_type=Fruit) fruit.weight = weight return fruit ``` If you want to revert this change, and keep `GlobalID` in the schema, you can use the following configuration: ```python schema = strawberry.Schema( query=Query, config=StrawberryConfig(relay_use_legacy_global_id=True) ) ``` strawberry-graphql-0.287.0/docs/breaking-changes/0.278.1.md000066400000000000000000000006051511033167500230120ustar00rootroot00000000000000--- title: 0.278.1 Breaking Changes slug: breaking-changes/0.278.1 --- # v0.278.1 Breaking Changes This release removes `strawberry.http.exceptions`. HTTP-related exceptions can now be imported directly from the `lia` package. For example, instead of: ```python from strawberry.http.exceptions import HTTPException ``` Now you need to do: ```python from lia import HTTPException ``` strawberry-graphql-0.287.0/docs/breaking-changes/0.279.0.md000066400000000000000000000076571511033167500230300ustar00rootroot00000000000000--- title: 0.279.0 Breaking Changes slug: breaking-changes/0.279.0 --- # v0.279.0 Breaking Changes This release changes the `strawberry.Maybe` type definition to provide a more consistent and intuitive API for handling optional fields. ## What Changed The `Maybe` type definition has been changed from: ```python Maybe: TypeAlias = Union[Some[Union[T, None]], None] ``` to: ```python Maybe: TypeAlias = Union[Some[T], None] ``` ## Impact on Your Code ### Type Annotations If you were using `Maybe[T]` and expecting to handle explicit `null` values, you now need to explicitly declare this with `Maybe[T | None]`: ```python # Before (0.278.0 and earlier) field: strawberry.Maybe[str] # Could handle Some(None) # After (0.279.0+) field: strawberry.Maybe[str] # Only handles Some("value") or None (absent) field: strawberry.Maybe[str | None] # Handles Some("value"), Some(None), or None ``` ### Runtime Behavior The runtime behavior changes to provide more consistent field checking: - `Maybe[str]` now represents "field present with non-null value" or "field absent" - `Maybe[str | None]` represents "field present with value", "field present but null", or "field absent" This means `Maybe[str]` can no longer receive explicit `null` values - they will cause a validation error. ### Consistent Field Checking This change provides a single, consistent way to check if a field was provided, regardless of whether the field allows null values: ```python @strawberry.input class UpdateUserInput: # Can be provided with a value or not provided at all name: strawberry.Maybe[str] # Can be provided with a value, provided as null, or not provided at all phone: strawberry.Maybe[str | None] @strawberry.mutation def update_user(input: UpdateUserInput) -> User: # Same checking pattern for both fields if input.name is not None: # Field was provided user.name = input.name.value # Type checker knows this is str if input.phone is not None: # Field was provided user.phone = input.phone.value # Type checker knows this is str | None ``` The key benefit is that `if field is not None` now consistently means "field was provided" for all Maybe fields. ## Migration ### Automatic Migration Strawberry provides a codemod to automatically update your code: ```bash strawberry upgrade maybe-optional ``` The codemod will automatically convert `Maybe[T]` to `Maybe[T | None]` to maintain the previous behavior. ### Manual Migration Review your `Maybe` usage and decide whether you need the union type: ```python # If you only need "present" vs "absent" (most common case) field: strawberry.Maybe[str] # If you need "present with value", "present but null", and "absent" field: strawberry.Maybe[str | None] ``` ## Why This Change? After extensive discussion, we decided that changing `Maybe` to `Some[T] | None` (instead of `Some[T | None] | None`) provides the best developer experience because: 1. **Consistent field checking**: You can always use `if field is not None:` to check if a field was provided, regardless of whether the field allows null values 2. **Preserves existing behavior**: You can still get the previous behavior by using `Maybe[T | None]` when you need to handle explicit nulls 3. **Better type safety**: `Maybe[str]` now properly indicates that the field cannot be null, while `Maybe[str | None]` explicitly allows nulls 4. **Cleaner API**: There's now "one true way" to check if a field was provided, making the API more intuitive The new approach provides both simplicity for common cases and flexibility for complex scenarios where null handling is needed. ## Need Help? - See the [Maybe documentation](../types/maybe.md) for comprehensive usage examples - Check the [migration guide](../types/maybe.md#migration-from-previous-versions) for detailed migration instructions - If you encounter issues, please [report them on GitHub](https://github.com/strawberry-graphql/strawberry/issues) strawberry-graphql-0.287.0/docs/breaking-changes/0.283.0.md000066400000000000000000000014061511033167500230050ustar00rootroot00000000000000--- title: 0.283.0 Breaking Changes slug: breaking-changes/0.283.0 --- # v0.283.0 Breaking Changes In this release, we renamed the `strawberry server` command to `strawberry dev` to better reflect its purpose as a development server. The old command will print a deprecation warning for now and will be removed in a future release. We also deprecated the `strawberry-graphql[debug-server]` extra in favor of `strawberry-graphql[cli]`. Please update your dependencies accordingly. ## Migrate Dependencies If you were using the `strawberry-graphql[debug-server]` extra in your project, please update it to `strawberry-graphql[cli]`. ## Migrate Scripts If you previously used the command: ```bash strawberry server ``` Please update it to: ```bash strawberry dev ``` strawberry-graphql-0.287.0/docs/breaking-changes/0.285.0.md000066400000000000000000000113051511033167500230060ustar00rootroot00000000000000--- title: 0.285.0 Breaking Changes slug: breaking-changes/0.285.0 --- # v0.285.0 Breaking Changes This release removes support for Apollo Federation v1 and updates the federation API to always use Federation v2. ## What Changed The `enable_federation_2` parameter has been removed and replaced with `federation_version`. Federation v2 is now always enabled, with version 2.11 as the default. ```python # Before (0.284.x and earlier) schema = strawberry.federation.Schema( query=Query, enable_federation_2=True # Opt-in to Federation 2 ) # After (0.285.0+) schema = strawberry.federation.Schema( query=Query, federation_version="2.11" # Defaults to "2.11", always Federation 2 ) ``` ## Impact on Your Code ### Federation v2 Users If you were already using `enable_federation_2=True`, you can remove that parameter: ```python # Before schema = strawberry.federation.Schema(query=Query, enable_federation_2=True) # After schema = strawberry.federation.Schema(query=Query) ``` Your schemas will continue to work without changes and will now default to Federation v2.11. ### Federation v1 Users If you were using Federation v1 (without `enable_federation_2`), you **must** migrate to Federation v2. Apollo Federation v1 is no longer supported by Strawberry. Federation v2 provides better schema composition and additional features. Most v1 schemas can be migrated with minimal changes: ```python # Before (Federation v1) schema = strawberry.federation.Schema(query=Query) # After (Federation v2) schema = strawberry.federation.Schema(query=Query) # Now uses v2.11 ``` Key differences in Federation v2: 1. **No `extend` keyword needed**: Types no longer need to be marked as extensions 2. **No `@external` required**: The `@key` directive alone is sufficient in most cases 3. **`@shareable` directive**: Use this to indicate fields that can be resolved by multiple subgraphs 4. **`@link` directive**: Automatically added to declare federation spec version See the [Apollo Federation v2 migration guide](https://www.apollographql.com/docs/federation/federation-2/moving-to-federation-2/) for more details. ## Specifying Federation Version You can now specify which Federation v2 version to use: ```python schema = strawberry.federation.Schema( query=Query, federation_version="2.5" # Use a specific version ) ``` Supported versions: 2.0, 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 2.9, 2.10, 2.11 This is useful if you need to ensure compatibility with a specific Apollo Router or Gateway version, or if you want to avoid using newer directives. ### Version Validation Strawberry validates that directives are compatible with your chosen federation version: ```python from strawberry.federation.schema_directives import Cost @strawberry.federation.type class Product: name: str = strawberry.federation.field( directives=[Cost(weight=10)] # Requires v2.9+ ) # This will raise an error because @cost requires v2.9+ schema = strawberry.federation.Schema( query=Query, federation_version="2.5" # Too old for @cost ) ``` ## Why This Change? 1. **Apollo Federation v1 is deprecated**: Apollo has moved to Federation v2 as the standard 2. **Simpler API**: Removes the boolean flag in favor of explicit version control 3. **Better features**: Federation v2 provides improved schema composition and new directives 4. **Version control**: Users can now specify exact federation versions for compatibility ## Migration Steps ### Step 1: Check Your Current Usage If you're using `enable_federation_2=False` or not setting it at all, you're using Federation v1 and need to migrate. ### Step 2: Remove Federation v1 Code ```python # Remove this parameter schema = strawberry.federation.Schema( query=Query, enable_federation_2=True # Remove this line ) ``` ### Step 3: Update Federation v1 Patterns If you were using Federation v1 patterns like `extend=True` and explicit `@external` fields, you can simplify: ```python # Before (Federation v1) @strawberry.federation.type(keys=["id"], extend=True) class Product: id: strawberry.ID = strawberry.federation.field(external=True) reviews: list[Review] # After (Federation v2) @strawberry.federation.type(keys=["id"]) class Product: id: strawberry.ID reviews: list[Review] ``` ### Step 4: Test Your Schema Run your schema composition and tests to ensure everything works correctly with Federation v2. ## Need Help? - See the [Federation guide](../guides/federation.md) for comprehensive usage examples - Review the [Apollo Federation v2 documentation](https://www.apollographql.com/docs/federation/federation-2/new-in-federation-2/) - If you encounter issues, please [report them on GitHub](https://github.com/strawberry-graphql/strawberry/issues) strawberry-graphql-0.287.0/docs/cli/000077500000000000000000000000001511033167500172315ustar00rootroot00000000000000strawberry-graphql-0.287.0/docs/cli/locate-definition.md000066400000000000000000000022011511033167500231430ustar00rootroot00000000000000--- title: Locate definition --- # Locate definition Strawberry provides a CLI command `strawberry locate-definition` that allows you to find the source location of a definition within the schema. You can provide either a model name or a model name and field name, e.g.: ``` strawberry locate-definition path.to.schema:schema ObjectName ``` ``` strawberry locate-definition path.to.schema:schema ObjectName.fieldName ``` If found, the result will be printed to the console in the form of `path/to/file.py:line:column`, for example: `src/models/user.py:45:12`. ## Using with VS Code's Relay extension You can use this command with the go to definition feature of VS Code's Relay extension (configured via the `relay.pathToLocateCommand` setting). You can create a script to do this, for example: ```sh # ./locate-definition.sh strawberry locate-definition path.to.schema:schema "$2" ``` Then, you can set the `relay.pathToLocateCommand` setting to the path of the script, e.g.: ```json "relay.pathToLocateCommand": "./locate-definition.sh" ``` You can then use the go to definition feature to navigate to the definition of a model or field. strawberry-graphql-0.287.0/docs/codegen/000077500000000000000000000000001511033167500200665ustar00rootroot00000000000000strawberry-graphql-0.287.0/docs/codegen/query-codegen.md000066400000000000000000000105251511033167500231620ustar00rootroot00000000000000--- title: Codegen experimental: true --- # Query codegen Strawberry supports code generation for GraphQL queries. Schema codegen will be supported in future releases. We are testing the query codegen in order to come up with a nice API. Let's assume we have the following GraphQL schema built with Strawberry: ```python from typing import List import strawberry @strawberry.type class Post: id: strawberry.ID title: str @strawberry.type class User: id: strawberry.ID name: str email: str @strawberry.field def post(self) -> Post: return Post(id=self.id, title=f"Post for {self.name}") @strawberry.type class Query: @strawberry.field def user(self, info) -> User: return User(id=strawberry.ID("1"), name="John", email="abc@bac.com") @strawberry.field def all_users(self) -> List[User]: return [ User(id=strawberry.ID("1"), name="John", email="abc@bac.com"), ] schema = strawberry.Schema(query=Query) ``` and we want to generate types based on the following query: ```graphql query MyQuery { user { post { title } } } ``` With the following command: ```shell strawberry codegen --schema schema --output-dir ./output -p python query.graphql ``` We'll get the following output inside `output/query.py`: ```python class MyQueryResultUserPost: title: str class MyQueryResultUser: post: MyQueryResultUserPost class MyQueryResult: user: MyQueryResultUser ``` ## Why is this useful? Query code generation is usually used to generate types for clients using your GraphQL APIs. Tools like [GraphQL Codegen](https://www.graphql-code-generator.com/) exist in order to create types and code for your clients. Strawberry's codegen feature aims to address the similar problem without needing to install a separate tool. ## Plugin system Strawberry's codegen supports plugins, in the example above for example, we are using the `python` plugin. To pass more plugins to the codegen tool, you can use the `-p` flag, for example: ```shell strawberry codegen --schema schema --output-dir ./output -p python -p typescript query.graphql ``` the plugin can be specified as a python path. ### Custom plugins The interface for plugins looks like this: ```python from strawberry.codegen import CodegenPlugin, CodegenFile, CodegenResult from strawberry.codegen.types import GraphQLType, GraphQLOperation class QueryCodegenPlugin: def __init__(self, query: Path) -> None: """Initialize the plugin. The singular argument is the path to the file that is being processed by this plugin. """ self.query = query def on_start(self) -> None: ... def on_end(self, result: CodegenResult) -> None: ... def generate_code( self, types: List[GraphQLType], operation: GraphQLOperation ) -> List[CodegenFile]: return [] ``` - `on_start` is called before the codegen starts. - `on_end` is called after the codegen ends and it receives the result of the codegen. You can use this to format code, or add licenses to files and so on. - `generated_code` is called when the codegen starts and it receives the types and the operation. You cans use this to generate code for each type and operation. ### Console plugin There is also a plugin that helps to orchestrate the codegen process and notify the user about what the current codegen process is doing. The interface for the ConsolePlugin looks like: ```python class ConsolePlugin: def __init__(self, output_dir: Path): """Initialize the plugin and tell it where the output should be written.""" ... def before_any_start(self) -> None: """This method is called before any plugins have been invoked or any queries have been processed.""" ... def after_all_finished(self) -> None: """This method is called after the full code generation is complete. It can be used to report on all the things that have happened during the codegen. """ ... def on_start(self, plugins: Iterable[QueryCodegenPlugin], query: Path) -> None: """This method is called before any of the individual plugins have been started.""" ... def on_end(self, result: CodegenResult) -> None: """This method typically persists the results from a single query to the output directory.""" ... ``` strawberry-graphql-0.287.0/docs/codegen/schema-codegen.md000066400000000000000000000010471511033167500232540ustar00rootroot00000000000000--- title: Schema codegen --- # Schema codegen Strawberry supports code generation from SDL files. Let's assume we have the following SDL file: ```graphql type Query { user: User } type User { id: ID! name: String! } ``` by running the following command: ```shell strawberry schema-codegen schema.graphql ``` we'll get the following output: ```python import strawberry @strawberry.type class Query: user: User | None @strawberry.type class User: id: strawberry.ID name: str schema = strawberry.Schema(query=Query) ``` strawberry-graphql-0.287.0/docs/concepts/000077500000000000000000000000001511033167500203005ustar00rootroot00000000000000strawberry-graphql-0.287.0/docs/concepts/async.md000066400000000000000000000012221511033167500217340ustar00rootroot00000000000000--- title: Async --- # Async Async is a concurrent programming design that has been supported in Python since version 3.4. To learn more about async in Python refer to [Real Python’s Async walkthrough](https://realpython.com/async-io-python/). Strawberry supports both async and non async resolvers, so you can mix and match them in your code. Here’s an example of an async resolver: ```python import asyncio import strawberry async def resolve_hello(root) -> str: await asyncio.sleep(1) return "Hello world" @strawberry.type class Query: hello: str = strawberry.field(resolver=resolve_hello) schema = strawberry.Schema(Query) ``` strawberry-graphql-0.287.0/docs/concepts/typings.md000066400000000000000000000133101511033167500223150ustar00rootroot00000000000000--- title: Type hints --- # Type hints Type hints are a modern feature of Python available since 3.5 whose existence is heavily influenced by the features of type-safe languages such as Rust. To learn more about type hints in Python, see [Real Python’s Type hinting walkthrough](https://realpython.com/lessons/type-hinting/). When using Strawberry to build graphQL APIs, as was shown in [Schema basics](https://strawberry.rocks/docs/general/schema-basics), type hints are required within classes decorated by `@strawberry.type` & `@strawberry.input` and functions decorated by `@strawberry.field` & `strawberry.mutation`. These type hints are sourced as the keywords `str`, `int`, `float`, and from packages imported directly from the Python standard libraries `typing`, that has been available since Python 3.5, `datetime`, and `decimal`. ## Mapping to graphQL types The complete mapping of the required type hints for the relevant graphQL types is as follows: | GraphQL | Python | | ------------- | ------------------------------ | | `ID` | `strawberry.ID` | | `String` | `str` | | `Integer` | `int` | | `Float` | `float` | | `Decimal` | `decimal.Decimal` | | `Array`, `[]` | `typing.List` or `list` | | `Union` | `typing.Union` or `\|` | | `Nullable` | `typing.Optional` or `None \|` | | `Date` | `datetime.date` | | `Datetime` | `datetime.datetime` | | `Time` | `datetime.time` | where `typing`, `datetime`, and `decimal` are all part of the Python standard library. There is also `typing.Dict` that possesses no mapping since it is the entire structure of the graphQL query itself that is a dictionary. There are a few different ways in which these Python type hints can be used to express the required Strawberry graphQL type annotation. - For versions of Python >= 3.10, it is possible to annotate an array of types with `list[Type]`. However, for all previous versions, `typing.List[Type]` must be used instead. - The annotation `|` is shorthand for `typing.Union[]`, allowing either of `typing.Union[TypeA, TypeB]` or `TypeA | TypeB` interchangably. - The annotation `typing.Optional[Type]` is shorthand for `typing.Union[None, Type]`, which is itself equivalent to `None | Type`. ## Example A complete example of this, extending upon [Schema basics](https://strawberry.rocks/docs/general/schema-basics), might be the following: ```python import datetime import decimal from typing import List, Optional import strawberry BOOKS_LOOKUP = { "Frank Herbert": [ { "title": "Dune", "date_published": "1965-08-01", "price": "5.99", "isbn": 9780801950773, } ], } @strawberry.type class Book: title: str author: "Author" date_published: datetime.date price: decimal.Decimal isbn: str def get_books_by_author(root: "Author") -> List["Book"]: stored_books = BOOKS_LOOKUP[root.name] return [ Book( title=book.get("title"), author=root, date_published=book.get("date_published"), price=book.get("price"), isbn=book.get("isbn"), ) for book in stored_books ] @strawberry.type class Author: name: str books: List[Book] = strawberry.field(resolver=get_books_by_author) @strawberry.type class Group: name: Optional[str] # groups of authors don't necessarily have names authors: List[Author] @strawberry.field def books(self) -> List[Book]: books = [] for author in self.authors: books += get_books_by_author(author) return books ``` - `self` within a resolver's definition, whether decorated as `@strawberry.field` or `@strawberry.mutation`, never needs a type hint because it can be inferred. - `Optional` is the way to tell Strawberry that a field is nullable. Without it, every field is assumed to be non-null. This is in contrast to graphene wherein every field is assumed nullable unless `required=True` is supplied. - Type hinting doesn't stop at being a requirement for Strawberry to function, it is also immensely helpful for collaborating developers. By specifying the type of `stored_books` in `get_books_by_author`, an IDE equipped with PyLance will be able to infer that `book` within the list comprehension is a dictionary and so will understand that `.get()` is a method function of the `dict` class. This helps the readability and maintainability of written code. ## Motivation Python, much like Javascript and Ruby, is a _dynamically typed_ language that allows for high-level programming where the fundamental types of variables, e.g. integers, arrays, hash-maps, _etc._, are understood by the machine at _runtime_ through Just-in-Time compilation. Yet, much like the low-level languages of C, Java, and Rust, the graphQL query language is _statically typed_ since the data types defined by the schema must be known prior to compiling the API code in order to define a definite schema to query against. In the low-level _statically typed_ languages mentioned above, every function must have the types of both their arguments and returns explicitly declared so that the compiler can interpret their behaviours correctly and ensure type safety and consistency. Strawberry takes inspiration from these languages by requiring that all of its types, fields, resolvers, and mutations declare the types of their arguments and returns. Through this, the schema is generated in a standard and efficient way that aligns with the style-direction of Python and programming as a whole. strawberry-graphql-0.287.0/docs/editors/000077500000000000000000000000001511033167500201335ustar00rootroot00000000000000strawberry-graphql-0.287.0/docs/editors/mypy.md000066400000000000000000000016061511033167500214560ustar00rootroot00000000000000--- title: Mypy --- # Mypy Strawberry comes with support for [Mypy](https://mypy.readthedocs.io/en/stable/), a popular static type checker for Python. This guide will explain how to configure Mypy to work with Strawberry. ## Install Mypy The first thing we need to do is to install [Mypy](https://mypy.readthedocs.io/en/stable/), this is the tool that will perform the type checking. Once the tool is installed, we need to configure it to enable type checking and use the Strawberry plugin. To do so we need to create a `mypy.ini` file in the root of our project and add the following settings: ```ini [mypy] plugins = strawberry.ext.mypy_plugin ``` You can also configure Mypy inside the `pyproject.toml` file, like so: ```toml [tool.mypy] plugins = ["strawberry.ext.mypy_plugin"] ``` Once you have configured the settings, you can run `mypy` and you should be getting type checking errors. strawberry-graphql-0.287.0/docs/editors/pylance.png000066400000000000000000002760321511033167500223060ustar00rootroot00000000000000PNG  IHDR Q%8 IDATxsy IW$ྈHqEQIDLm9'k}mGQbVqrtt}>''9)WʕT\)W{i;+f3̼j`)|=x T1c80 `0 `0 `~@:QƑqOp~`0 `0 `0 qUd;'v `0 `0 `0  Pƹj\I `0 `0 `0qq"9c0 `0 `0 ` x\1ROr`0 `0 `0 sxI0 `0 `0 `ƫ(q$z'0 `0 `0 ```+ƫ'uN0 `0 `0 `H5@0^E(Ws8'ԓ='0 `0 `0 `sx TN1 `0 `0 `0j` #xɍ>=0 `0 `0 `P?0 8yXˬY<]ߪU}x00 `0P V =f9`@ww?}1VAU]=IYշkZiI06 0 `0 TqlWm < 7@0N0N0 Ѻn'jB_0 `0 `7@0^┞b 1g`<EUPjUWުU]L0&0 `0PVe8`]qO1o\1 ںnxa0>0 `0 Tq*nH̏y}͟FW_Qqu]CCiq)eL5Lu\S}Cȶh_Tg1|yyS0 `0 TW3m xg0^X Mnwe9}aP&ql%P\ݳu_yo/Q^~6g鎋]z+zd[~e?ڒY' `0 `@&i0P qZ NFZm09q۱5'{x>v3 JKU'_?8d=W|X<:FzYg~}!1 `0 ` Lf3b`bj 2_`mB }i;݃uSf yG*(-UY}0~ě{V#E)d1 `0 `(xa}}@ 8# e0Wq͜MC0>@2>PKUwpƇ'y*U=el0 `0 ` ]O8\ 7`|pw/)ҥ/}JF1/3w*wNn~gĉ]gtwy;ne>0֠t:XZh D:u_.y ݹs礎|sϾ.^|tջ#S=+Q\CC[r;w?;}sί];}~{ 2B6u~~ݏtopz[Oǯμo/>?ݗCW^6}jOr_rʭZ_sELCO_z1Z;. ֻjY;V튞uʒ `2r?=lԷUf?˾}a1]AzuzvMwF#9M^}go۝.#Ӷhעv;]z+5{6k] GZ.|=]*A#مk3ZzKpHYQ'o;-󉷩u1u~~*zk[`|~9]^az ~ ut?q5%ӺBn, ϻɭ~e~iGN'4\}ݽ^30 `0 ` ϽWW,5^_g x2Zׯ;^~飄|r]pXX޳q'oi$G?+,{olG]]:d7]yh_L5 }mODW|e2{ ןy })\(߲XͅOp܊ -ҲL>J}^GwB} ud\w [_fb\_?'7o``\^+766'1$[k ~m}Ng߳g~?7n+%]b^y M[^{(A֒z~ywǏ˗z6mL Y_ƵnTzݎ[|7|\X?y=.  Cay]>g e9 [m0[;/uz ^BqzXG0_;ǿa %2ï_d˵xԿ&?lx׉]3^}G!}`\u ۻTe]i-|)~Y]ı0 `r1o,<8j`qvaxR8f= Z?s.p zOHu./S el|ǷCקetK_+'.vmmW庝oJYv>3^{kp*!5?_7{[q @eۛZ<$$2m0.uVZYnƸ #wX貏m,h}ԐRkф)~^ u`< u'rzx\%˄ӗVoJs0ϵo0מk: oS|kU >|0 `0 @?  ??k`c>0+3`fKh,3y蹋>矓Y o.AS:r#z40Zs}>N8/+Wka@|KB]\t``_]wy>oa 3%$v֓hq:]][n.g@ ҆Am ҃7jEK@\[~cee{5<{WlHX^M! _\'e2_xc+[y}cel `0 `a`2Ɖq@jxG޵iv?ϩKr<ޖ]o=<9h0- a׭;ݶ5vⲎ <~!0jcms1>9|ٲ\C0씐[s;n]o ܺ\,~{Δz闣H Z-8|H~^}(z^G<ܮ0+܈M7\{-!۞o/ېn ƓΗ7}- +u.85=6z8'ca ~7nƙq0 `0P##g`<~cq˜` +0v},xIs{˖ ~7s:_x[v j|^eՐ^{|h9:ϟ> o 6r/Z?ޖ]7I\e旽qyѯ$|O<>tiOt~]NNhN/Xvig CF Ɨ[m{%Շ~o:]ؖ[<㚚-V0.=)sjve]8ϗOfϴ޸o0 `@q x cx `z?cPv~>ᄒsa@mC~K?r+i/<+ Uߙ6 M|Y[gZG.wF\e̳iZ|)!'Srl2,1 r>]ףIrUC]BᰮC7ɷ;`[?#L?l'OL*g7o `0 ` ~7&' c`8}#}@ `{/\lǿ^Ucǣs/S o˾e>S %Kc&ZOw;v_ϣ֭˗'֗鹰c~4z1jin\֙n97\أVՃ+'_1e|مk_\'Z|yYW0_=ivk:܎]֣W2ɕZiX#0 `@e qxb0{3z0`͞$bۻrXlkk'Vh`oeS ANxMK^.?x>d{EGp[Y$ ׫?p˯O|ěu 7m:[֭}meyf؇}u3RCABϖmeZ?~vQe[\,Vpr`T?SgWlOv|iJ~ٖB^gj_Kn&sr<۞׍I/i9=]֡K. em=Lr2M}|#:1[K(/e[%.o %maZǒ3 `0 `<g0<۴oSUFov#5֬>='Ew?|K~>_'s?tv&^+/߽=Ogqx ۮבǶ; fp3r;S]μq]{&l`\sk>̿K06x6|Z O ,#-%~^? [_:jUӦ-:u_mx$[?u=?l\}sNnw|^n,{ȋ-eٻQat?ᶬ_w4a2֛9 ǿXpWjluBt_WK-W'r{ljUO-ߖR{{= x:5Oo.57D,!y-rlcΖ6ЉetyٶSd\ibr}+ S>0`0 `g`x%@ k<8> @qpa݇ׯ6mtSJ S1h[1QSMު^rKZ  `0 `(qlŖǠAc0 S'R^/%f֪U]K>=޻?7+Wg `0 ` ޘs3b`bz S·%Kܱ),ZVuulvtELw(z.Wg `0 ` ޘs3b`bz ӆoVAU]=p[յoϔ7/ `0 `m`s6G=̕8e:X4ڪv0M0 `0r ZQk6@0r*xiЬ?[Vuu[յoϔ7M `ހ IDAT0 `mo_qav Xs\388Wjz´48_0 `0 ;; xLJ DVAU]{Vu[3{0 `jՀj7 x{cz4@0N0ڪv07ƍq0 `0J4@0Jt668  hX0 J[շkZ)o0 `0 ۀʽ@ f1qq XVui]_i~'pE0 `0 ` /wwx;A('̭R:VZgj&@=0 `0 Ԫ뿿jo9(i``+VU]=q[`Zo$ `0 `h`mm.qI- B8`nZձoUz>Sހ1 `0 `(뿿{11@0^;cqXc ?\1 ӺnN~a0 ` 7@0^xpG0ovƃP4N?[Vuu[յoM{ `0 `Uվߜs0Pz11*88Wjzⶮ2H7 0 `*8n+-ی\ $'̃Z4@0q`*(coUߪ~k}c0 `0Pn+Rc c`vƚ㚱@~b744 '鮵-8̭ZjȪU]L;q/ `0 `뿿1CzH1x` 'ut[7w{w^NxqpuuU[0 jЪU]Ly30 `0 `Vjx55c 2_4MBk6VAU]=qXշkZi~'nE0 `0 `7@0>b 9./  .[Ax\ĩn|G۳z쾪 ǭ?[VuddUߪ~k}oc `0 `foos g`|5@e]0j`gL|wu 7?z`nZU7VZgZYo `0 ``oCsi`<¸`@[FCogt\̭ZŪU]L 0 `j̀j/ xzkz2`||GwtΌ'\n^?[VuՑU}Vxa0 `@5[5}\x p0.7`|$uuu{% iJ}UPkUWXշkZc`0 `@[ /_qM1PY*.>ue[ ǔUPkUWO\VZgZ㘾W `0 `@f{z6@0^at**w[7xGG8xQ XVuj]_iNb0 ` 'c`zƒ㒱@q TT0lf]h`Y0 J[շkZiqO~b0 `F6`7h1G /oxo~/]Cιf]-.UPkUWOvVZgZo61c `0 `q3@0ɸd{0Y,X*%փj3PE}(g%7as֪VZgʛ+0 `0 `6`r/80P;kg9k g | s֪8[յo47_0 `0c81  ^ 0`m? h Z0 J[շkZݛ0 `0PV}g9`Kcc Tqq XVum]_ie0n0 `0 TqV[ I.N'h`<EUPjUWުU]Ly0 `0 `Vjx55c sx`*'LL;/ `0 `(x@ {|8~  B8A`nZձoUz>S7zO1 `0 `V XV~s@ 8L\1 ۺn|#a7 `0 `D㸭Dl3ns1@0\0Njx sԪU} `0 `@ XK=1 Ԏkk 'U@mUWOu;w_ 0 `0P{;zx p0>3@0qh*(coUߪ~k}voc0 `@[會 /}qL1P ƹb<0`P[u}F¸1n0 `0Pq[nfb`'8aԢ VAU]{Vu[3 0 `0rc3׌53@0N0ڪ0v0N0 `0 `pw6@0a|0`g`<EUPjUWުU]L=0 `0Z5`Z;97@0^c2 sx`*'nL+qc0 `0 ` ݲ͸8Nrq<8E ]nn5 sԪoUz>Sހ1 `0 `(뿿{11@0^;cqXc ? 'wr{w^3`<ڹZ;Vu[3M0 `0 ``GCC x<DžqӦ)`tZz2oUz>b0 ` O TCC1`u5cQ왋ݎg}p>{ պnҜ+}0 `0  O N xu##㈁]0i;ׯC\}}CQCL-`nZ1oUz>)=0 `0 d7`7' ;; T%_z׮ji*[յoϴh_0 `hoq ı0@0^8bb'vs֪\[յoϴ'wzJO1 `0 ` X>>`pwm kWW/Rp̭ZwVu[37Ɨ0 `08[{6qb: W8r<2(XS&P|nR,Vs֪\[յoϴ'wzJO1 `0 ` X>>`pwmb;/|kjjUPkUWOtVZgZo4/ `0 `q4`8mX@u qxd1P|'utr<`\~ޱkhhpUPkUWO.VZgZ;=0 `0dC0P{;z6`^5#%h㥃aZՓU}X0 `0x` Wr2(&8 =V.v] S\ -]W VAU]ucUߪ~k}9W `0 `loM污73@0>az .l3M:1iZՓU}80 `0x` Wq1~(X2Or仆ϻ=˪:`nZՓU}$Oo-0 `0 ` 뿿1.Dž {Cz4`\7]QPRWUPkUWnUߪ~k}ø20 `0gso6] T??3`VUPkUWYշkZiNb0 `~\ } 8N%0̭Z:^VZgZo0+ `0 `q6`8m@e c0P:ioOoZՓU}$Oo-0 `0 ` } }@ 18d 1PڪXv0-͉W `0 `@Ԟ`: W8r<2( 0VAU]{Vu[3-ɝS `0 `@vc|?(x@u 'U@mUWOu;Vb0 `@ 1N<8uᩚ hUPjUWުU]Lyc0 `0 `Vjx55c sx`*'LL;/ `0 `(x@ {|8~  B8A`nZձoUz>S7zO1 `0 `V XV~s@ 8L\1 ۺn|#a7 `0 `D㸭Dl3ns1@0\0Njx sԪU} `0 `@ XK=1 Ԏkk 'U@mUWOu;w_ 0 `0P{;zx p0>3@0qh*(coUߪ~k}voc0 `@[會 /}qL1P ƹb<0`P[u}F¸1n0 `0Pq[nfb`'8aԢ VAU]{Vu[3 0 `0rc3׌53@0N0ڪ0v0N0 `0 `pw6@0a|0`g`<EUPjUWުU]L=0 `0Z5`Z;97@0^c2 sx`*'nL+qc0 `0 ` ݲ͸8Nrq<8EA('̭R:VZg00 `0 `6`r/80P;kg9k gzjL[ޅ]gBWWא&]PX_\}]]Ij6;}gy/)1pn\9n_\CCsɌs/ `0 `l *yvI `@ s0@E=ޝǪ% g%5ݣOq˥ae2m_4ewef/0 `a oX'&08Cq4P1xk8gǥ(߽knjL?Lp.؇_{7)-Xth=IjwasU?~Wq.8&cc0 ` X84b`|pi>(/sB[(oY>Yw؇|.*J -qވ-Q0 `jxm33\q_gb xsX uzKK[Q\c9|0?q}x0'~_h}Mޝ_ٮGu<ϻ.ZSٻtއWώ>׿阓:av\kw'Nqr|";.c+ImmcOq̆#w|w²R{9f_GϷ76O덞kPokb7\}p/o9Yry)v鹌 vpdp [XFPOWqh|N'Gnl9VOR=6>p{N8_H?8xoso;sBSs;|?G?'YgOD:nI~s}{۴Mws<+'le??>ӗ=Ŀ~<_ݚg2cKuOLޗVN^Vrݿ0 ̸Gj2Ͽhwv?_=/, 'θ _zƯ?Y]֙}-V].BGJ~vڌp~} /\8'ߟW\N?L嗙7>܎ֱ]~ځ?ߪuOdkOn'_&\zM2] 3.>~'`0 `|?! O ` |(~ߺk*PW{C$}h'}o?+\[nS?>Ov\.wvf/E\s;\Gԕe;:`MǺ\Gj_vI.{xr_wv{m>\˜[?eۓqY/o9~p~=\<9_=_qAJm۠_z(wES/&l\ -~եۂ <uGr<{߼q p*Wkkn}>1?Ϟ0ORĹ:#/'/[p²Rcy/؏˄#ƌIk30 `0 `<gv40(]5ݿ{y l06\,wȯSn.i] nuA(?]pJh ey su9"ZӐ\U-KX[k2υE}(ן7ɕ溍O Z7]0kd`\jӻcG˭ݵܒ>/X~s܆_/4ohlǀmmS-|U ,/Vir^N z/W~k<+]ty^/]e'ma0ί c;|-]V(׫yo9\i'he ~x+z ?$?ȲrUz17q n|gRa0._~ס_a?۳kݟO~]S>,`0 `c`<}3}@ 8  'OCw Ay9z}]U0e jU na0.AޕNp].TRgii뾳i%MM%VXg-~e([{q2=\ev6^n%O{up`<׾^SO֗;~g.uŜO돃_o)>۾CuHЮy}]1zƎ^ `0 `(qlŖq Z>j vxCC۹\q[~_MOQ8j3%+tft;G6Ro9-X5(U M=NBfyUu;[n.anP;9ƣ>o.~b*'r{|o仳u`\Bup]~YYO^.$O+jp`<׾5uܲ^ ?l߷;w}wמ?=-$m8u>{o 6a0p(Sm7_i:_6\??A0 `0P^77g`|5@e]0>߇sH-X6 ޒF·|f8+WA{֕+?~tμ]Ltq M3mCbze/ `0 `1@0`,0P\'>'@q 5[|<5߹K 4p/m0IJ/_MUrm? Jra8,w혹m|vTo.&߃A|Da}<Wh0.ֱ]б?-;vۋkY+\O 몞7iƒ0 `0P:-n-l 5`\LZ yWQw桟pWg:Zڢ`XsYt䠴~Olv=W|X>;?j,'}v赧7=,:e`5Gk,85a|WwL~! 3|_i/wsmFh[^Úg3LҎI/vg|U~5>&c`0 `3% 'P YK .!gMƌs7~s @> ZnOۤ۬SyMdX_?p]>g9rEB^?6N`!W|.E{z~!m+ d{CsunᒃЃNƄ><`3:>8|O2OٿjnSɝczA/0 `0 `8 R2@0Rbتt sihreʇ}8'՛~0 7Mur<>_K}ćGeë;udWb c5\ۓkȕL_ۣ_z:~iD4K\~j6|r0.ik9]^Ӄc[g6qϮ~-_[ۧk\*M[w;vvOZꝟ8ÜoWsKخ'O`']y:Կ'&-Ե9=$GZ `0 `@y x(a`|L<J>_}n}o =I'OFϝc?ow8Jgˣ[K-^:/r0չK|s!!jA_HuQm|'\-߹-O=ɵ?hsזnv麼|l\H/V0.W44L#K@}N78"hNJWkڌ~YYǕu0Onƒnb5'an˾C怜kplo8 sû?_u,Xt8ep9dƅq0 `0P}ƫoL9NS 5@0α 7@0`c= .=?S {ݢmnhhvr{g_%! Cg hu0粭gGK+jdn _YϣۨS=zg-'ˆI-].9@=7YBcY;rَƻ-ҷvqYl] 3ϧ/}KP>2wk(SB;7zYFtsa0'ZS=cǦ:w5?|3v0ME_ 0 `0 8u)N'l d`<]>wނ5J\k dé\9}~:W7.aaP=޻*Z^mNX^)$ 뮛ܓv im3>ɶ(?m2yw|kҥu;rty}>"]!n\垼,+_ݮwn]Hº >{!:#`n9 WMg,t'vrL Ltue;:747^HW_?s78eNtKw}.^.0c܄e1[ܡcC[4nzaЯ*=oU7~vrŻicX^C#m3x4Θc0 `88`Z cZm_: SJ@aC?]G>S֪ƪUL-ߟ?/?_hbO{H?^N8ov0 `0 ` ?FG 3@0^XF0P+$׃M2pKOZ_Vu3vw(zl֛S벞Θ?ȣ{IN.2;紌~Rߠc0 `@i 3@0n{{ x:ZWVu[3`0 `@5 u5fp-qHo``<ձVAU]=AXշkZi4}/0 `0 /]oqKo1`k`ܶqqU@mUWOu;͂al0 `0 ` cL?VXP LDA('̭R:VZgxb0 `0PzcK?תqת}# 'U@mUWOu; `0 `@ c\֨r \Qsb`<E4h̭R:VZgʛ&0 `0 `6`r/80P;kg9k g``+VU]=aZ` ~/ `0 `@ m`<`x sԪU}ڽ {z `0 `jowsJo`=1=@e 'U@mUWOu;V Ƹa0 `@% m%eqqypRƃP4N?[Vuu[յoϔ7` `0 `mo_qav Xs\388Wjz´48_0 `0 ;; xLJ DVAU]{Vu[3{0 `jՀj7 x{cz4@0N0ڪv07ƍq0 `0J4@0Jt668  hX0 J[շkZ)o0 `0 ۀʽ@ f1qq XVui]_i~'pE0 `0 ` /wwx; ]nn5ɓ纕+},X08b߬RjFϞ557Dyg]i[g980 `0 o7 /}qL1Pbw{w^鱠wuy`ȕ/OӴ} j L[ܥǭi{uz?iu2~6䲭S'{ƍq0 `0 $gb?@| g,8. @q&KxN0xd; ַl=5z `0 `@ 2.&\8u .o=O+\G|in _ r|2#ܛj<O70 `0 `v oة];=cwQVbww`|nϲ GR|0~!ޟ:0 `0 `R ol')xu//㋁ .9= rݞݏW|C7u544ԧBرW3OǽO< \[ۄݖ-m}ܫ('۵kjjIX^iyA-:7v,/vU|:h {wm<>kpo0>4`B n?j0p[{/WA,̧7Ľw]F(4奆\5~[)~gBNF̫Q-Wmsmts羼dtyX^GwJ}m/P\斎5u=a_ `0 `(=-^O%@< c8 @ΉS}0.1b]$߼7.-+~0߿) ѭ>lor]Ͷ㷟˕.}>x ˼jt֬K==qoZ3 ϝʿ.!\`}·R둾\-.a[OX~y׌;[^`\!A|w8?\e?HBo]_O8?Θ0&0 `0 ȽGe`Ƌ@ .꜖k a ~7&' a vxw`|ͪnꔹnɮgV5|޿~ߺ6*K,#頙?_:[qY6`\L5y\ͮToDU:_8ՀXnͮgѢ)+/,Z<{lۨ}p2C}~+K6 u鯤ۻ??y'Z^gF^Iq`0 `0 `1'}cx]0.W/=$~[𴶌s{v\xcCSJ0X O 廾Grv~_( e=:]0~'|-I.~n܌SCչRG ߶!7~Gگ^V5N/X߳4a)c;nw6Cnlc[#zN_<~Oڲwgߛ˷p^3Gguf< `0 ` {I!e`<^x` >bc'1e]qG?[KWki.ojjuׯn'/\x02Yw;uj\c@qٗ c\YζGM\ ; O ?+ultw}in}O0 `03@0^ycqƘa 7 O g`lH5pQ GZV^oQ[K }pխkyf'>te~N~4ۿyW;>skWseZ==0 `0T7zzBO0P+{8? @%mmM [[ƹ=;.y_\h3Rqu kVyW~{y%K\. zǢy4߿)s)四RIWs紾~?|q nklv:[^Ǝ ?m955{_u55k%eSWl8=uMOyXxcc/ݭ;.zlVJ;y[z `0 `}]@ oL8N @q|zN7kƢ Y}ny-U3|+Wg:I'NDW23ge~?.z\%>vϞ|'~~ _  ݍVrwmW~ۭ[wm%^+5 ^P۝cG^I/5qy=!in<_zѝ* ]Sz./'mur%<[U7=-ue;]k4mֿMB^/V0zc>p~SW{4'uƁq0 `0 f@Fr}O3VWk v0$6t5$][Mxڙ sx.%=tfֺrW6DcUb:_ś=絾\-rU)'mwh[&|N9nB֕p䟏Gb+^IƟ+s-N70 `0 ` o*{@e b0P> IІS/E׸L?^-x?/[ýSfZzŊ=)l>L8=rN<9hN%ۘ 2O|/Z'v*q{r-]a˖ / MN)*+f(.= m̿Xڀwg과iY >wúΖ6ЉQ~l6+[߸>߱'w"0 `0俿{Hi`<¸`@lqwti5cW4qnUn޼ծƥ0ົ{[͞}]n޼5N޵Noa]}Mwvpmn. t׋9M[Liݮ!o{绊,OpAHB{ %%E# a aUtAP13;3czkv7gf+z;r:/y{{n5iNɼIsm[3k/(d(]۶t6Nq^9Vg_+(}|8xܥYne2 b b b c hK:#*1P oV1#JY&y+y+;ߒ>4 , 1@ 1@ 1@ @` f3IB $ks:'$Ư)fA1ϋ+]1XyWy[ҧ[L;ra1@ 1@ 1@ 42kd2o@ 1(.Hn͝QWbJ?tηOˆeC 1@ 1@ 1Ш 1Nl7*/b81@;@ ~ '1Nb\a /:t0松Ao b b b$Ƴ)qJbˁ(H+RHͻcQWRyWy[ҧ[Ɓe2!b b bht =ehC0@b,b b b Fq⹑̼$ƉU WhwJ7|Ktk{ b b bY1[!1qL%mjp .kaW:tնm\?v\F_yͧP;1@ 1@ 1@ @=c`W=灲Ā} 0|!fq^xm cRb:+"=i}3S潶ub b b b81C O $Ɖ͢bryc8qjuz+GXafpM81O 1@ 1@ 1@ $qb')v):HE(#Fq$IHqlj.G 1@ 1@ 1Hwyiwq8cY=b8q$I+ ԣ!1@ 1@ 1@ 1@b<Έ3 rb}b9q$)ʭmέkoPR1@ 1@ 1@ @6 1G(HLXOX&@10@b8q\10Ʃˁ@ 1@ 1@ 1Pm 'ƪ1O^^c^1@bx]O0O4ժXE]1>kZr&3zDQ|q1Z[1@ 1@ 1@ EqⲈLe 1Ne#A5"oZsJ)(\6m5m5}0ӯo͗6뉧~o׮Сkj߾+,jc'.\>"]O #WyF}D@s+u6լ"b b b&H_&q/O1m bmvā/͙?Y62a^4b|РrSЎ[&LXd;t6"'-1|[kB?pMwm4I[蟘!b b b( HE" qb*kL1>bQ0Py,)~M6mH71 λdK/+Kf'D%۞f^:p?ߊ'n"1 oI00h1@ 1@ 1@ ~0@b<_xy̕μ78O|Ν3';y{QҎcldp^=y|)ʓ+ƷLf,\߶573w*㏭:j"JM}'2k:K|#̥y*~D9זA~*1}v]O]BH!}yc6?TF|˒ ߺ垲4Nו 1Nb<3b b b Za8V+1b 1NsL o:sߕ`mV/tMеo6m5dksqGLL7L= ߱ws`&g 5v J}9̔ͰIC32̌ ޷b[W˯K|9Jy7zWdW".8UF/_su-ͽ^1WHC^7e%KYx84eX b b b1@bGRf6 H'QpB?I3bxˬj3'~ju|ڤ M9k֢^rGJ#mX3fz.1>pl{3gز ڃr߃ײO76+bT"?LˈqT#G_LoLu;3С{jWVmo!ǎ\iKs~'10-"b b b7 'f JTl(1޾]sed;>6{ Xem6Vha%Aji=Z?_`xp~7lxν, ^pSix-ˆas / b b b b b bV4 G2˒ ȇnkmgn^|~͵׶q6w\>b|¬A.1gw7v?sql땶cqϘ?,igA9ĒgKO!1 #b b b b b b((i]  G/F/ݷo{sO>b[^4aĶ&~l?o>{X38O?ܦ=3vg6\7.ߴx^1@ 1@ 1@ 1@ 1@ 1@ @1 |-H"Q8b/ZkxIZ?ӈĸ>}p%A3X}8 1Pť `[wNߓg1@ 1@ 1@ 1@ 1@ 1@ 1E k!C񽷾U"zϜt͜1_Pzx[1 jN7 bGAvsJ}ORfCL 1@ 1@ 1@ 1@ 1@ 1P pxtoZsΒޝ:%Sw$;z /mJ]f|?y?xmxO,b}.fCn$ݤZ9Ɣ b b b b ʈB IDATb b@V+܊y3wZb|êGZ]:6}V~zFb<1{S-/#۵d.{C-1>'s#Ɯ>OfĭeeQb#D 1@ 1@ 1@ 1@ 1@ 1P ?Uq}-2:}@qfִ[67-<Ґ8f'{fW>ov?^FV?pߟ}fǭg͘19ͫ[ U"Ʒm1kUV] O-`cJ 1@ 1@ 1@ 1@ 1@ 1 T\W=Q8b!3V };/mۑT8qqy-xuرcgJ۾.'Ə벴tIb#D 1@ 1@ 1@ 1@ 1@ 1P ?ߪp$Aеٱ9:\H{̝ôiӦaIqYZD޻8*b "?俛S7;C4tEVGI˾kI1 61@ 1@ 1@ 1@ 1@ 1@ O%-\aqø@> 8tحp3rb:m6"#Ƒ~]ѫL+ƑD^1c4}=}mLd 1@ 1@ 1@ 1 1^Pgz-~$5wcg7&n&NADg~n?xf]-xuO?5{-ʘiԱ5k_;gITF'1έԹVbxڥᨃG?jB~LQVMwlmpYlewx#;ey~W8|w,Z֠{cW'Ͳe"e&kc[.\4r?lboU)O,J nً;̌-&J bo/^n"G:ޣRqiou~0VFie nd{!cl{/l[V&O5pձEk/(} MƌNemt j(Xt(g!ZC98ۦM; ]0Hu,'o1rĄz{--Kk"zB,͕F'qߛkX]!R>z>,]VmR ?Uk^,zA'p8W1\a~i;s̘bsO]p@敗_+m[aM4iI|-Ae&}!n!u36,:Fydэ7o!ƣ7nfy>^[ ™NO>- 'xK+3n #a <4}|ߠ*!A_d[O}i]m2 /L8u',fcNr@Mb<ͮfjø'y5&#>ھ=#㳒=Ó qҧ="?IjI"_dK.+_Zk&o>wC%YW\o!?:j祷,cD&D:ިcI :w_g=Mڤ8xAmu^myHcwgYG 1fY|~bW:L=YWeMI>[-bWr1ϒGD)HۨxGm]yٚ_Bco1Yɂ >Aa$mR߲C^ b\Xi%/oضR]1^t*wqtߌ Y fmߚB$ƥ=E?$N=,߬ʮ(+իVͼ'oE![25ύJ'ś/3͂oZgyYOC!6)fHVJR˶!Go̼v&7tl=sᲫZ#ճ*/ q;YBw[f!0sKV$YX՘~k+ˆaCǕ 4 $Ǚ}Xр-ǘ%HwTQ<|D+<]w2HViv=0KVPJˀ2 jql,TXio#UWkVjou3`5UڷSzK[>矾`~g||W&ś)ge7tDޞ=;V|YOC!6)*H/1w/LԖW^*n\Ֆ1,f!}:^ĸފDҥ+K/bmEytݧ?l;Ƞ E T~g6n=hڶYIb7f!ojN:aۂf_c`5 ֛DNPQZC;ZW#}+15EF&MYz.-/yqߊ {P yIOfgt"l^kbZ#1~MMy1||ie'AyHw ّXCqo}qiC(?QZ=qŒHՒL^=p }ky{s2^ie>us%|{! *`\c{WcTzp-8?}*GO=<8Ey*'/jPAa߱@HE^ 9; 8:Oau3ǹZ8oY%0_8mpes饗37n<_'ʮhk4΁gC!yOEq+{ŋW_}@noiZZV¢FayFV=EA}n> Y0Ɗ#Zɦ|YRl [)vlY6 u>CU/|V{{C8i6Ρ6un2CA1.=A,L8yĎ3£Vo+t(vrFڐʕ͑' d *L[W$Oi!3Aܿc`pQr/m7n~2KwĄ2zG}A[ YuN#9Wʶ$}.۸a[ !q{Wqoi/跡]@ۆI{6l ڕ%γH?Lҷ" ӧ( 8E_}e-/Oid]ZQK(m?5b7&B'Oe}˳o\=nb[~oVǞ7rҏ+wy- uRDŽI?#g+*{7.jz<7`E}%yw]_^]9mJo"CWT^Idλ&dDoYI&I[F#CbĸwU8 ] !(:@ ..۷* =AwMAM`NcBY0{Gj`@]fV8DOwZO ×C+iYNc/ݡ-ѥQέ\;n3Vo D% N|jLв2G2i-(e0YET^KK볤$-qAea.ݸصn ᆓgNM{,CƖ=& Rvy9#>Y8H+֗. Jv7?vDкa RƾQ%ƥ(TVW| \ YE%e ɎwzOv>a&^IhIQar$d9Ο֛6f/ȕ^WFؘJi VP{ g78e)2`>/|GVvI/ CtMLM+.}Go`>YQ$w4 KZO11qv+[ȁqяυ΂oi]]};IX%]7}yPvD׾id8Im0{g"MOi$Bf-¾Aޒ ѝV.Y7ȃ "7@;lʝs^zK+1S겞/ZNݷdBߥoпX0䆕pR,6_p]_^E^qfBo"GR72]Oub&?+:*\6)M$ J1+kKL m+u&/ #fKCمAR~1 ۸`5 -3\$@(8ҕU" }SeȖ-;dGQ}t8N b5. =%6 #﵋ZioI.Hr{ 봰NrE]>@V0ÊɓsuHXi5;)_N2(cE {U1ANKo\_BAZ|Z#ȃV 0=HXV>¯ t8߽˷T.-lޣO Kǣ}FzMk0%(OL*WdO"A.ް['Zװ׸Ï  !VnFm917m<(e W6`HKr I&8etO>辣.;ޒCZŎ{8ᦕY#S&/π~ &lBCvTeq_F:@=zs0[Lo-qqGx,y #طzƦS7m#\&r]H&qAIDF7L~Ȅӭ쨟zBݛ$_ +=ܴ-~#)ްm{ĞA_auL qԶ< ߶[R֕2n^z+_ISE{ȊY[?;w&oy˭+?ȮOj$՛O+w=MA!mRYIBȺ.qi ‚N~NF?Yd΋  , $^Y!mс fbK!N_NbջA` <~wCL8HֽkH3E'Vi;h76F\:xn s/l7er3ݟ!ēoZ4(? ʱ g [ ?qߋÈq7N]V~?y^d-OaHŖ"&.M~c^W#n<նS֮ (?߽`GypwdE uNWyǠa{pȷ3(q4,A~t a!uWOk =c> Gԭm۞ 1Pm~m=̑| :nBßߑYI$yJE>0H+K AA< ӥ]9@6Kxo({W~;ʳmFXd},v_z2eyܺ"mvB~#AG|w<~oei(3izªoB&Ɓ9w 8kjbzk迉/-l qE|;8iݸ<.A?JQ[wwڲ^ݺ d)YMc#̊Kvk{;ū6}˳"oA :$-q]m%q후E8o!*~桷ybUګJ;wiV#],a݅-i\=%śLr4h՛#}i g=qeYIQHb8Q0Jļ*?X`EчNxpϻ'uqGRD/6+b+ŬHy'?*ay'RAzFX Ju<Đn/tv̓)$^ ԈЏ2:島EQM]fAx ;b LL7 Z5SMK:RПw$ eᛗ\W߶oc>}m-&Vd2XQ߫wN(g ֑(Lq'i[ϬGIr-o\>;R~~/}˒Gs'ѕi1\=e7i |}|fیx41WoZF i"ō ~]֭ۨWvU;~ނ~ sZĸ^as$6Gp[j A ߻Yu0x*ՠdl), EЉ %z31J--DƼz%5a|u\RP}V|[a[^ [/H!_2.m}}[Cc;T2σg(NdUÈ$&ƻu-Sk:z7*]`tjM-MR 61KuX1-5^މ4^0 };!>z6⒃Ql$V#~v!J= {4WjqӉ"{X]I(eV)m5Ǎ-L +>ɰncS$4-1,iM?K |Qk[Eq74̾_ Vq&rك4ts^oEH^pAܕ3o$yCoIaC"1aU!zqnzg7VHke'ЌQ!nĭnfy&131ަMӦ͵F%IOct5=Ko;=H>gb\^<#1 6:wȺe-yS4hp&߂dYj61. sae&,=%@x(jFbZWVR g:Lf 1'ze'1oO,Md@WFJWqenBR/ܴ|Ͼϯ`v_X 2^\ ƹ»6E!}u߹(NكoAe}z]0w}E!ƃ򒦞fI=oKJ|Ew77?tdF5\b>{.vp78FgvLk"<-M6K~HZϳF퇸z[OLboyC|I.hʍ7ނr7 _<6q!$}H#-k>z!n=u73yE&A;*kjqř2!deh :ח&Ѩy?4Zݭ 46+l^=(3w"+{Zb[e\/muu\?4UMZ[ƁAE,wg] [ZҪF ƃ^Ib'㺞{^ \bYVc/j4t^g6M3X~g "}z 1Ktd߂ ʕwE"@;N H@{+mM.M>bZY$- f`GJMK*d aZQl 2'#.ۯ6-=ղջ؈"{P-̺̂lkPںdO0iix|OzyPKGKo)}T7Ӈkk6".޲w=Kkmۃɼr<7Ȓg?$LIA卷ЛN?}~8x>iO&+E%4zsyӨS7|< ?%;|j= ϧD`S>36uW'4md6ʷPVrdw7'H6C Q:`3cF ,|b&_+$ges oQ9ܼ7s4j?- ,$ b|E7[bu#RVI.IwyA-~|[yv7\t@x~~d0 d ߵ [>R@;8=9on\Q7߄y&[H:yiwZ/ys]]dgN4 uçWBc嵔?JǮ^nq:&`s7I/nԓ /lFQiPqz"Ʊ+Asf_-գ|⋶{J+2u7D twtd>uƆ"}@$" %߰} -v^A-$g;Sem4q#0U Y=Ampp{R"u ;Ϟ}ԖK(:zdn]ps=yCePu]Iڶ޶S) %\i3}ȲWv]fqz"Ηnu51j"r N~ʯ6ɢ&i_M|Anxy1J7ͿQ7}MOF^Цhڷ<󛥍Ȋ8JpCR$K8u?$(OiAŵoiWy-m^nzo.|i^mrd7VOk{?YOC2og'ƻےflHRzӦd=ի@3bTss7\yh|{0qyѯo{`.?[Nm۵ ?|(̅KbNF)=1dxӭ[s_޽ڿ13huu4w}\x_w_0=?G~e:wVN O?vwNAĴ?XE&2 Oo瞷a!#V` g[(ĸ:e@!y6\,H#dpR.V >r.plt2P]vi{lԫ{޻Y@$h_meAw@#H xf+p汬bwwf6S&.MvCBGmWmMҞEn{뱤[oyɆG>"OVF9ag5f:nظZoH3n+ %YQ 6 $3lKލWaP':U;uX}6_F[{ϔv`R)}FwW>vи"/ ̓7tߋWs 2Cu^M: sLTifFeĩn?Nj?u裏?+͊4T^8x7Ic,=E (};ֆIZxjo*6 첂~GkjW^6BA;贳OdAז iѺ&jHrb=";vTtޒ$M}Q6qq';2h1n1o$M>qS3GI$}q .^%4mN7wyӴ]Vqtf~*jJN]MLj^=xzv߰צt͙|?gy#_y1s  S~Ԩ6=€Q.B;M|/}m81H4@ؕpMzK/箺xX8|{ݎڪ+i^q3>a c@tà/1Հ2p"2.Τ3[ńJ֍aGqUzmnt!MBzNAe)7҅}.a-\]`%߃\o~ A=& iZi׷[ ăv$9޷P.$i!a$WoY>x0[; ~o|2Kw 9􏉘iiZbzNe? [G1ie#nZZkwнk*aڷZ1(lɃOvO?[t־!!n`\zE[ܕ9}sHzKoTKD}OIJsoQR]W`uilsZi9^[M}-m?DSYgN5#Ä}GZ;-^JߗsO|kxp^zmBD|h޺{|?v51GRZ=.q>x袹bYp uԵGp?=ŠJG~X|X1# ϿX淚?zl- Vٍz֠ĸne&&+:V*N\ ?# >* bU 33%02+됆ox~ fSO=c9;./*tb3iN3nFO3`lw/"_\cNa&oOnjK?h\[3aV3qv_3x|o_)]97~7 .b45e玧,1G7pA =eR] k>E]}l|`}/Mǎw %m +,˕:{ayQ]lN#{g ?aۆena5/:yS3o0ѕ.q㦙+{9u͊7ZgsE|f-V[b,`_2r8gfQAJUYˊEpe"C&a{ S̉K{W;'A~a%ߵ@$OI$Ȩ'bjw솁4vċ0R`cG":L!oW2v*'M?TAo#Ջ+vެ*t bp-51(~Fc-Þ&IҪH"_ = nIY7O^In+ͬowva*xՐ%nb|¤v%vv~ݽoV7Xz£8Y.OioӦY~oV6b:Y Gk^r92؆E< xQ\0,<3{47,,e G`ƊlyXDeߊ,9ևMct?ot} qf3ŅgH7|NJr!];މᩦqu`ƅ X~0Z[A?.VT hb\rq*s=˅զ_1^3*zg߶/X^w,A}ŀ^ɬ2}+nY߳̈c@Oc Aza qxeP&zrle4uC;ܒӧԊllBXqm6Vha%Aj׷ ?vz.1>}в ]?xl[][KxM>+^`JvD_yv { w2?bxqm(?WNpIp˔u.|`jٽE-n kb\gl>`L/,~cGmB u `[Y=z3zE8\߼崙=g,uqpwC>\ 6, 1@ 1@ 1@ 1@ 1@ 1@ 4Y8 b]fJ8ܶm5=1D*g>2 _\YW쵤 eyO?o>C )1@ 1@ 1@ 1@ 1@ 1@ ŀS>1l]->yDcV؊qz&|ΒޫVbfCO˯kwvyw)Cq ˆ b b b b b  OU>_6Kw=Eb%q^LW֏}ǃoCN;kdM#M٦} k9u=sϖi$cʼs@ 1@ 1@ 1@ 1@ 1@ @q1 xރ-)pަVc# ĉ -1Wd 2nmyΚ۴#tY+m~i>شO f1@ 1@ 1@ 1@ 1@ @b57{%UjAȓON|zK> e7ngM##^t=Vwԅ8qӨ *"1@ 1@ 1@ 1@ 1@ 1P\ /1~u,)r^Ӧ͵$Gd}xP|y1XJ?Z?dl~7w!?OfJXU{=m5jXFoXzf/?wZ%Ko5|6>-Ñ/hÉaPƁeò!b b b b b F€SjBoȊf_1^I?Qmތ9СM۶,c[w_4i1cڶHƔya b b b b b b@X#1^.pH糥5ql}bx8q`ٰlb b b b b b0 jw$I{g{+*q6hԠ1/31@ 1@ 1@ 1@ 1@ 1@ axA⼈Ҽq2R| b b b b b b -Hous8qióA$b b b b b bq$Gp+bx ߳"b b b C1O~ }=n_G\ x$fŀSy,#M1^Љ\1^[bZ*g׬_V|?z[ޏ>أw}O}WAYö bwe/u^s}z>1@ u5k6ӧ0w9`F22c% H=@1@bD'1^DY2M?L0?Z5U)|W?S^i=ͬY ANEvek7w֭?˼`^mȟKq%/"AG{D'1NwL8#qpwv'7հoMgcz$FqJ[bxŐ";.]kVo… BL.}JW˯,qZY-r4ʓɓf22wjb…F矍WCJ2͝(R](+)7[{3{ټy1QAVk,~ c,=*1.eVKiiܴ?tsL*s3?`D>nڴà~.chMێ,]ΘOzFts"̢Zd#e ޯ[)r[|Aw8!yvpܸiݘѓ#=,]ȇUƾ+,\o0ob5|LVz ?j$sK~cHc;*]Ip1ÇN7{7y$Ct;r,յ뀊uC7;4h|Wc:d9x ?\j eEQg1@ /jJb+!UuK篾%7?'0*rDȻi v+3& n\7m7_BG%f7l_Jd~k#?A8dO‰.zRJ[wzHXO:M7ƇP>? U"czZI88Tc'mAIL5b?"2y/LT.]z/^|10w~1'N6SqҌFMc#`Eo{b^|WWC EfʻyQ@i<ĉK"pUk{`PIn~jQ~Wn۶HT-[lxtɻ(nCelXcSfޜkE@V.' <,LJqfy1#EdMӶ9buA @1@bDqT|'1^ %ܲ$ơ*Q 6^~Q;{5E(J]Y)؃[Ow"GTsbXxx0i=ͻ1 wy:N$%ƱţkW]ŕQO 1n\ݹImv:wɊSNCI?l'lq=.ĪřnȎ#1h,qS58^J2Ώ IsQ?37V^\f&]? €+ȕwQ.qWQ 1~=j!ǔ K7Mڵ'\I[>.1>tTK,dtuҷ(ʯU㮎>O1LnA/OMر1buOr b]1^Љ$ƹb<>@i{W㠹|muT9Ϙ$HN g>rw A~_5S p8Kqc{H[o]6XuVx+3/V7C~Cp[ùIcz /T>U^z٩Qq<Ρu˗߱Du9ww $7t)G3S7ݸINg-wl]Pא{ >>tAO>q=Vϰ[v!mu?C=iͫ^ʯ7xӴ,ov~Jw-|X-\f.\h{曗 Zg/zލĉ{Jqn6)KhNǓľŕ:1v];ݥs? 3j@ˈmJm r:Ain\Uj_/_uI8:(QɈ N?޻ۖ'X+}oEn߽.HAe+ b=J;gq)]l Z@Yr+/8殻5sɆ#iYV}Voܰ̀Np2ֺĶkn21w<] {cf획^V(#G2.t49}6,Yt[mfN_mVӬ_{l JSޏb.e֮:jnYwҀ?o3xļ9JNx6FknnY{W)MUL_1dݪ͆u'Ka[`|>i!cF+p,iӇݻNEgp7rX#]6e%AdWwltn1s߂tn>tT7-ڔENzSWe7QZ 9xpxܩ:yY|v?Yв5«O[Z.:6dZ|}z&[de&1mUIܱ Wb2#)vEf7%䦣Q [U+׬:b/ՠkr߱CR}F=;oGmu$a]7uBn;žI VX_^{ +VK>{]E?2]\b\:n="Kqb>"xq02"LJٙ3&ʌgPA [^in^9P >꼻B]g:ם!P h>ȁ tQw*'@΅ 0w=&e>'!a@ɏTQWgYWI&m [ta8k:_.4Q)bzڻWV:?q'MicWALj wA%u~=p/iaZ8'&;I8IuGޭ]NFu㘬#8& nCyR6q#2'!q&kH>D.?tA}SKd1kȉLs .gG_~S78eu1ćW'yά|:`̫[cR4z݁mܑ'θ2cvb[[ɫ1@ 1H.)g$ݰc2\y/To׭?lݰJ]dݝ)'\]G{?wI+쌃t=F!oe+}u[_Ym?~7H?tٽ{_+zd8VjCƽ{[wVmp[-i}ONHín9B%,H;n]݃Wk`ҙ hc7gG~P%1@ Ā$Ixw^g|䄼z~?)3p sPxe@5l0 q,t0?aZlw-ru)5ٴ}.BW&k^pd/ I$&ZH_l9-ܒppw%}$G`x6k&:tXf{Luz8H]Wۅ7.1LH^ goR|6D7yy'm VbK\2qLp$}[FiGz)X/qBGxw | sMSiTp|HwvpE|#6J8eU2qįWV,w68ֶV7.1 NB`҄ԁ8njbW.>51x[dnfwX"a{\ڎ__7o(g>w^%."pָjo~cN|uo:[hW?M>aqTPX03Aau&&F]R;eIlq,qP"G3 EY3ʏi Q#ʏ$++Ȗu;E>b= fXc9e=09BA};'p6^  Av82C\{i5ap|Fԓ\3s@@oQiz[y :؝Aˆ{!gaGHI1@ 0 dNqoVc^AAa^\b?oN %Yv 瑏3t!N7l% y1\YXӒ IDAT,e+LM.Wq(qʏ%&ߝYQ52.wgj% 1DY=Vt h2JrO/zdMzib'UNVCՄ{&:k&2֚"~VݾvAt/˛Zb{V[T#^d!$d",Wm5Y+> 1"o:"G՛w9*_Ӧεxѻm{q [.xlwy.p6. ޝ%ƱZ. 簋l#O(]Xe.a&y/.}7~v gKXL&^|1ZVTiTb'rl?D<93w!}$KԃMjGªU[-;Ŧ?pkM*[N!z$o.zLf(b_ 7(w\|#~*t䦭kMkI 2sϤe":xJYS7!ƑOdw3hû+%. Su>l+zӻf@W\| xKS1@ Q1@bDqPpGbxTh*G|v -)#}'vJxzZl-g>jsW-IOBX,Y GH)K4a}f Sx+$xЙҘ"qJ"w/d6t\lrYV޾|l?8Ahb'BW"#1G.v\BYcDZX]˃81)O_Oڅ߾}WIgyd#.;w/~kE'iVQz*qS$B Q^$g&w+zU;qC7?J[oX~IgBwwN/-{뭷[?Çɂw 5FŶ&\=+%P%&-+ HH51|6eR՘%GR@c qѫv[caʾU&IuZ)\b~dE6VmA갸x\I\X92VЁDnAcpOOƘ0n}~>Ja=m=uo比  ` 爻isn;씀: z; 7-1VozȎc0Y;9`bH2i˸z+b ~]ۨυJkۚ]{^=Ԝ̫b+z _+51.aAxX\]oV 7ɠ@XoE_F!j ]LBYjCⲋWWB VnaҾDt%1t#% hpUvﺺ*;=H_RIoHnb\jH0Wĸߠ,^=Vjc6.K7M0jVAzª*+ŃyHW~tYY㢛0'7.1~}ޥ ai}m9|ViN2>Kb<0mU|zӻ/ 2?͏2w[HY3 ǃ,o834O>KiLaާy9Gd cwcͬ /qxW^r³ɚKXEP념E ~e^m>${ loۥO:u6s(ai t{:'}zg2 xӾxKX?ߒ%ك>},2H# nIho$VU֫o}[΁qȖgީdm߹ %)S7ysD㰵s^=B;v#Meŋ[t?%'J{@'Tzqgt7F~¸_]\abD2 *eBY⯔]@J}PR.TǸw߼͞Lcz0a3X;~yf꥽p"YxY9Ec\暁c=lƽvnj P7-i.1,k ׸򤜋 27i[4`yMw-eii{ QR- d X1@aٺ{:"~ *-H+xYق9u|w7w>8Z~ƭL=ou#8xKث6))ڬwl` ˍ~.a(^f9/)j9n*8<&m󆽇E8l ֦):ϛPFl'Z&m$e=>󖅇m"k{ "oly&BP5Sl+0>~,_{l}j^z(sqgvk!֧iSs'^qk˟3Ќ|o*yzmzd;a|jJYIR|_\3 2@"a@)=M=7B{w~:A|oNeSEǏzcw AJy]vq-##_yU6ᵋ\E BqSTH8Cy7f/r]18B1 NX;gΜD MMVϐ1's5!ٰQ[Еr(o_ۏm <3H%oŋh /+ ن)v?f|O>$hYŽ!s Cԁ|' S3ƋO?봏H onp̅x"Mp[DyGRەJפ|@഍A@x|[\{嶿K"^Nc1~\~w#'<aCRښ3"<5¹)d{W7".:xL4)4gvSRǏG?֬٨IeE!.}CZ?5x1/Jx#<.8B(sjg6<’)nC7Ta/Iﹱ~kh۬ qdF#~匜&\DCx7}ż6 a;n| g)v^rd j_f>' Ilx>}jܠ % zK]}M=¸;QӜ}\ڇgV_6.k=m _/v@5C :sa,Rqةjٖf+wk[xi{G{=]M*(s;̙7h\RiU:*c^v g]FLr1x^// 3onI{^xy9oػܜsG;o=TSM.tƚayܻ27_|QWR"d 2?('Y5r!N-|[v87q/H`Γ_:~,7{f=o9g!~SR6RabGR"|%{=O-¸)JJ>^aķz ["F[pmū\)`o:Ds pR3zD,7xe`mCۿJl+իz(7~cud!͎2ƃGp{>}sb6)^KFR/m3* N!HzYy}*&:vb)c[`Ϫko<$zО1[i.?# ¡Wb F؈`qJYI%ŏ3=|/e%5qSEy!RV0r ),e} 5.6b|^>/ҶٖB~#@/Z ` bKRюMʃ|H?k?Τ~{SxemMڶeg"wa,-6ܧDG;o2m/HI|gŊѦ}a=}4^דծ'Y˃ک:g?bQ~!j#<}a?_y>z,짲  ؃g,Ƃy ~sfW:ᩛ95Ic1~D 1$Ҩ>Hp~"X$ VͶp=| 6HPW IDATBm3ާXㅀ g #gg ]Oͼ!zH:p<;G߱cocytp8d 2 dޒ>`BmGM:x6sEqz _HS;I;xR2H;0| ~ϑ20meǘCm{CTTlxۤ}>["(t~-tO+8ސ!H7nܚrpqd Pr$2٪fs(|ޜf5x\9vd/>C?9G=G e}v__|9`1=490\+8 [[~7&v XQ^=?y|(-^P>|öıՆ%^lpݹd 3 d鞦 >F*Pu 6Q#&UNcHsN=c?sJv0ak5;0ցfܛ}Q!K _T/-RaCu'd1@a<#G7ݻԞ(ڵ[@o8dO 6-\N8nk T``*hasgT{`<5}jʟ078zp?Lj0ur}Ak>֩3jJXu_K1 2@@ax-*zLdIϽ)SOZs d 2@ d 2@ d D3@aw-zw*z/eeZtܯ)Õ d 2@ d $ |/D2z Pm32@@ PO0>`L zw]1TM'ܑS0xp 2@ d 2@ d 2@ 1@a0{JkݺPY9bΟUҔ8| y<2@ d 2@ d 2@ d r('Y(#hs8gd 2@ d 2@ d 2@ѧD'KTjxcU?Gu5m"<8sF d 2@ d 2@ d 0'N({nq |ḥ{s8d 2@ d 2@ d 2@gx ő*_QQ,TZmڞTX--)Y֬sFo}[O?UNz|;wS7ͨmwQ}=zc?7y]w]t k8˯k7g`Gŋ\p. d 2@ d $ ]tSu 6jQ|*{H/KUoy (irrrD{']gڗOIq,li|uF̒у/mcU/~ٓoRWcf8v<ӯ%#j7wtϤʈӯD_wdi³̏V8?=,R\ϧpEy nx1.`c+Gp;R=s>㣏>R+?0~Ǻ+kwލz|؃޾}nv>CMzm_zowE6jjEd/ܵw d 2@ d 2}JttOoI;] 3UݻT.]v (jcjO1N~@wk!\߶A,@pypܩwԬ%!mJcݾ=~;0'5濼3Õ*cv?*諎#ݻ*mݮk[Ϻځb ]lD[kA$+ҫ_sYG7޸>AG9pSx+77VG2TvmD_z)33ȱ;2@ d 2@ d PrpZ-zC"4ZV#]0Na7"ލ5MO-筗Ef =OyٹjمEyp} WYǺu >p@vmDOfOmC6Ꭸrl HbLqdJ߾0u0'8SO=Fn#z#d=4{sGO-W#\0Na_#GL}Ŕ9uFrA/?a}:c d4{N{9bx5yRK=vbQ}ي0@?u>hi'BDž޷>㙄W|8[&>+g Pka.Nokc sJ d 2@ d :Ɠ,^E 2c"yG##q <,x:e zEb |m@})B\j9ZeQcɟP䡝=Uy h_ੌ),0~.O楇W[߮Q.RS:ڙ+±mP8oFXy18F +T۷\aQ߮?ׇmC('l㴝[ű7bOl#{q 8se+ok"_~Ck }C s5-v?Cu5~Ċ`ڱc~_T~;p96٨Ş򥇕ʾ{j͚M  / {RG(+s*mڊumogur5p-x/jkyy1ӡyZg!a,9ٛ裏=0F2_u+{ϚY`cv/40vx17nsف-0{e)iq w}O)ƍ7ѣ?ԓ2@ d 2@ d =0da|ʤZ􆷘MP6m:5 ƣRX=SO\],%fp׌鍺xV <\o{kj[-u/QbO0n|\=ԳZ0۲eg@ uKt]i=ɸ!6sqW^vY,fGe V!:E:8,j[{rm-?Uo,AvP&cm mI a 2mT/XېRE-|-uO{RU>^gk6i댲~\1f_yEiSRL? ?P1xx˖i-בH d 2@ d bx!Y0PYڙ]ԔIt~m'e%0Na<*X>&fy6qDlJ]S-a!8bhwꕧi臈V~!ۥmӧz#r1n.1cƑ/nKпiý.8mJBsx^wƥM{0wx{o0aܜUx@̉7]La}i]q©v";2}j&hZ,m_8U9Nyo[,s0+ߡaqzb.K۲>؉W*Bqxg*DA6KKʵp a,m:es6ݴYSH=v m-_zM/jx%#T]ͳgyʲ1E1\kεyEg\3>|S؇p*,F^NXrӞ=u? 'NUӦV^$z={΃3᭭u_{nk/;89-xrKm+7 5ϸzpþ}UAA}YgDR6F*qi߇~ԙǼN#lgܓvw;"&xl#lݺKw7=e2@ d 2@ d t.('Y=I^]V͟\A(H'Qz(~ Y8վ- nxaQ_M-v$h5S+"AGuo0#ia cK:B7YBl!$K;i+/*H$5_zdMaǶ+6vtA^vUf7S^r\amLrpD1~!'V2fq.œ)#ܹȟ5fp0n n^6ex .|>:'6_{@w9=e7nPDƼvȇSϨ۷qBKHR^W<^x%ǻW7EkWڒyζ>Cw9Ƌ2F 8nxK}?a s/}Fj#:w^^k_Km"=wf<<sd 2@ d 2@:/SH7E=z;^' IDAT-ޭ[ q\dOVZڗO|dcx" mkSic"J[l!h8Hؿyd?˸l}0u~ 7 %,H۳g#<|Lo|?a\“K[Rsde]¼c/pH?[ʙ)B<Ǽsz\1\چH"8hZ!ai0Rll9˯:`+Ԙõ)&BKl!Q(PRe?gx#eC@)}4la!xJ?#{K`)dk۬y;z"ُMs0?HYشF{EXu )o/עswyWKlzS?a<±ϸpg `29O<sd 2@ d 2@:ONiJRMi-cx,L^FNт-0.!DYKB#^Mso[73DCSaa|'\m;%,; g!E 5a<8,{#eujM!'|{D017˹G8Dxao:}j^k}dQ|x&d^a_t*CA 3XS#lJgkSAӦ)i0 qxaKo {n=h7޸aC\ G[Œ#O" x\QQ0o^믿ָ.Xc ^^x 致)y׮=؂8mb{lq I~4¸l3[> ~y׋ ~¸mwl502+u8wx}ʹ#5皓2@ d 2@| PMcuꊵ*##x)YjW8LVǗZ7,D@D4=׭>爁wթ |Qn|ƋN늓591os6)/r/iS+0~z%!=q3d쉾6}4DR߹agN~)~z&Z8ڑ:+6⃔5.9aW;&m3=%$Ű#"gL_Gl)O0]_kkK#cm)}m [PED]9@}m]F߷myGr=6u2asl~=m"~v c1W(+sk~x D˺[ٕ{%lQQ zI}.)«K~8Ky3:eu.#am<<|r͹d 2@ d 2@@0D,ӔڥM CF(1N`Hnׂ-BC3Ei-!Maf\C{j㺻ur3gT"voO=muM9?aXnP+CT1{TÃB$;uկ)xz36̖&DZy~2^aw'F{چ(.Dpq}gt{?9±P} {ʓ'j[96G}=M-=u?Wi·\uJ}5Vas9JBwtZZ:%mv&M칍ad_!2ãu!{Cηc|_x G?q䡮3rW?^{^;ߏ;`[B2@ d 2@ ds1@a~/(y[~A\LGy9-yqcleK!nû:k"^abY^A\ù|WK/6mzEmOD^xIۂY/عxBwos1-m\S>îGjz lBHm{6Z[bvo~¦v/Z+ځ Ů!y<Ŏ قqHoBԁ^B{ +f}9/{:0\rd 2@ d 2@0BxUZ0ӈxfl"C."kmu>pN\B`YI6bfϾxmC+w]Df+Yƪ k/p; y}6k]'FxMeUlGLQ_`]/x̱Mn|'=;5÷Înͪճ1P!)cgxi{H><׽y>l [zn<(j^mġKdcװ}pQ&QO?lԆM줟H!ZW? bΥ.]`kɵnG\ahVPQ"ﭷq񥎤C *l േֶ e꾇92@ d 2@ d ]0Bx^>=1P'v11vxGjcI b7DHCF8acbF{m= 6cY당6Fa;P.>{u|x"xĢsXx<= b7wLltDHMP>>=£i7uA'=aR_20Fg>ΚY軭L,!<=ܠ%FDGܣ3x9F 2@ d 2@ c8L0Na=9QSʨŝT_+_r"ǚGOFf![- d 2@ d 2@)SVOV2d/>p&onW}OXp d 2@ d 2@@0Nqzc< ȃ=9ɝ[T>ݱW=s3)k뻩<߸9V- d 2@ d 2@ `@hi1/cx&8 d 2@ d 2@ d 2@#U;zy /%2@ d 2@ d 2@ d x1@axhD| ]~2@ d 2@ d 2@ d P0Na{'yK_Bd 2@ d 2@ d 2@ `@)o:^)"=1+ d 2@ d 2@ d 2@:'ST(NǷ8q~ u/!;ם 2@ d 2@ d 2@H)'esMkcyO9o&@־mt馺výNV/vy z񝍋kkD)UJ=̽&ͬSUjzaU6'緵%mId[ƈ4/"H9Ah2ZGs szn/HwTR>Vi nlFb'_/=?cy[Z4ֹp}kt,87ztKmNg d 2@ 2 Ek" q2jL?d2U0>.H5mG*Iݫ;k'6kK,nl_-o,e;(synL{&R=m ov(_|{BQ\#ƻ̯}[_qvl\v] յZkU_K,P?AA;W-3۟5Z}ppݣ7ۗ6=mBvz`I_=6F+׫Jju]%+k/ަZ}usm!K`%#5ՐQ_jBPY=]?)ߪuU?8BF:G.]ijB+;&ha}0"6»\C p3/mQ[ N86m8B߳wc3^>#1W'5Ӭǯ.9^"cʿ_<Ǐ>ճ31w?~\?2@ d tn7rй9sӑ :u,0^fxQ<0ĥ,Dq {k6{w9[աE)5DolܮW,q ªe%Z&kZ7&om\綯Զ!7̮нu yd#{mI?ylMyk5ך 2@ d#0 u %^Xt&&TSɄݡi,DrbΨe`V_?c!K.W5TzJrw7k7Ec&[{tMRomQcm;zXcF9GLU0p:^Tc7UlWy00[y׬﫞=F{vnt|OՊTC]ha|[pR0~AݿZȲ7k,!2»[WxZ -Vwnef/#mm=U-02f*"zpÏ/lΜZ3*skuU^}Uc+z쉟8%`?v?C޹N]{9[Vq(.s?y]uαeqʔru_w7~}1b:ty^Uuu|_B5zl?8Ơo<ؿUZ_޿QȚjpvƛg䩯_:Iニ?ܳNgVWժ\HY3貿{VF}ͬw~zGv'ADul:&d 2@ d R%2@"e8'3E0>ur{m ĺ,JYL>Rզm qtff<~ί:0 tG\#6m{Suۃ lyhΫ:v;W{bsSaq.CI~V@/Ytf`iN"Fc͝ u<%s']23 6ǗƟX2]Ңmzv K0ԃ}N:xW H?|  >t?2\w2@ d Tg8MuF?2^(zd'HyaW~Z@0vc_ ^a"~CnxcF|xw75PyXw^Zc # ]!qMi'8ͯ9W}L8xBD>ՑZ>*Gx>o\~v}Eb01ӴHY\{eWc)?pԔa!읭jIYoyG;/ܾJwusE:5LƵ'4/~4/?!y8g<7U!q/oD=5_s h(?|8W%{tG•ʰY8'CF+ӝkqzf;smOεjMav{tӶUmn0arm^^O0Ý#,{]QW!}Gж/zyQۀoɹx?y lJvX09#GW9Zǜݯ{'VZX쪚w7]3۶ڊg m߭W⼒2@ d ex   eX*EgT;vC ڳN}xڏV[{P+ 6>fWaq ..ah髙"֟1ˇ:Ouaivݣ7=!ԭl *ncaq¸(8j^o)(t\xy\(yO^wI{'N3.א]nrQI0r\?{}]y"{=vĞWT7ˤ⹈6p+!|5gkƇdt(n-5̔0D,u$d 2@ 3 qŸ3 t (wuu"gx f3!rx9 bƃ@k9u!g~!=B{x(xpYss])wػ{O_?rY]ODt ZJUa|zWۼwr}l?M[ݶD$uKfڣG/De[_jj72-+ʜ>+LooouG`QWY_?n_\xmLkqQ7Eq[^ݻy΋)z%JW=ƽk#'d 2@7X}2,(dv^3xFFj3^?xۍ,iGOR[5%8D, z+itGxK% [ڐz/KG~~凾EDPk75>zNՖ=K* j\?=^AQ=)cKGe+U{|!Nrюi+,a{߷k9nqEFunʰ8$?+6틭]WKs1K͗ 6.upOjWxnV?k)vs}JNb|s d 2@R-ľ!d  P'G6Q:2rx^4-zg iqA?\mG^0לu r\Tä濧\aևۀ@凴n]t-X i$oü/EkĈZDjo_3}oo[kM/j[wTo 븺oS?vҢ!Ľ'S}u=6.e^l-cwMܖ=WMwھOjb/P@lΐw[ ?'N,u4\S{ S@Gv~Tn0eᄰ8}Pe^ڛN6Panmv vHwu*9|ww` 1.,))׿W\Y,-m,vfY'M? d 2@H6[y/2k(X3E{d*]H9ab8q̝4>}\. #߰]#'̯UʇPwcÄ >ަkfMжxfe@ݺd+뜣Ogs~rs T3}/ ūo? d 2@[.%d Y P'{b풽Tg .Q;fYnoL|._6{2Z 蟭rGOC8;5@PH ܩZFs%CF+oy.ջ3Ѭ &Ce En qݤ:B S{&+){V^=Eyo/^mx}ͼi<1ns8 vEt!nSоx9Vu]w ׹3ڿo-ApHoq%.a{jAC[߷Y]DjTw6\i{6g:-C6ccL֞0a`\-`=~@?}G:yIig\Dm0׺*,>sB]qc2@67te֬EY:yEeڮ6ɸvqըVS3W-y`$7579_VEQoF6H}z(sMex3ǫ`tw //9\%cm&x&d 2@ d Q ojm2@q(Yh \ֵ;X ;iAsotlaNV-@MGO ..[ ۜ?8Ζ4l-Dy؊0[}-맟Sòw -utU m c\Bo]yyu]J?޼T j"Kw{`6KmW>ه<n<7,C'22" ϶y=3f'^eeͺ.l!>˚_;6 KYl0jD} 5W}ɿuĞ7ݺ~O l?]~soW^^~r)0O!܏~}x̐?rxqmϻ? d 2@H&o}@@0Nb푩ta %qL.B.] 7lV؇qgfd=S]aEGTQ(߹ w[4zΥ]y ϱƻuUwB;Ssu+\\Rȸtf:zmK؄8Na m{0, 8⚀#kլzf9wcja)6mmeY8y,ۺ.3o~K2`azQwކXv,ZĬًk.} %wŴEzv)j)7IU^_ [e3 d 2@ `dm d P'O䉶S:1x:Mv$cI@I-݁Ƿ9ڏv|~!Lrd~XT 6+fl5Avmk G{}vS(oIIp9d 2@ d q $7u֚s͹l P'ẏyu[.vǎ嘉BK0(Sծj?t%˺鐡հŪ_VͲf<331 b1 /_lۯK6X.?`(ιQ>3 0 0 \8ڞsZo6 oBёSqn>UL_פ__|'wˏ`~8ؿ?_s/ 0 0 0/7~f6|6z7 <΀`qO^q/`|?@΀` EG~0O5TT5w{g``X@۪}o1zN Ɵ={v[goB!@[C,` jSuB~nzU: ̍````fo3α;ۀ`|8̇'7_g6O?{{>x̰ۛ橠6U]= 0 0 0V}0߀`9c4-_?Plo n>ߟ-Ԧ,U?U7o=g}f```^p \ui <.{/?6o ƿ@]IDAT_?:RAmn"U?U7o=g}f```^p \ui <.5_Oo?qlo _~ ?x[7/^0tDJՋTTZ```{^0p]5Oy2t㗯W{om5?y+`I՗TTs\````uq_9 :o͖_UoP2׿$ԦVRSuq=s``` Ɲ`zg5[gK0~({^0! jSu'}W}縞9 0 0 0΁s@0~uޚ-3 5y*Mխ9]c^\```@5\;gϿ30`οNTPk߸`m6f 0 0 0?MyG3 o&3a` qT@[TTu9 0 0 0J㼯^._kof``\0HԩuLׯnx^1 0 0 08?c`\qg1M(:y*(Mխ٧]? 0 0 0V=0p9}Yπ`\0@*Nխv~u 0 0 0jq UFsFV5 oBёSAin>U?U7ouf``` ߿_c c@0άf>q/:U.u}p/```wzcc> ƛPt$TP[OOM[s7{```U ߿wvaxs iK0߾>χ~/|;O/o'v矿՗w@L?Trwշy17sc``@̽s}6 {>a gɃm]aC~`?;Z0SAmnIOM[s7{```U ߿wvaxs i@0_6U.`}W}7s37``` ٯc!q>񱲁'WnS=` jSukv}W}1 0 0 0 m c`uf6kb㍁T@[t: ~ 0 0 0 Ə;c`l1rM(:y*(Mխ٧]= 0 0 0V}0߀`9c4 b1 Su_aFbn 0 0 0083un1 ddE& @<SSuV7````8@U9:ym 3 b1 Su낙_aw/b```qw m@0>|?@΀` EG~0O5TT5w{g``X@۪}o1zq_7Run]8sH```f4 vF-lq7h@0ބ#H?Tݚ}~nzU 0 0 0gH;{9Xǀ`|Y;͚}q_7Run]08._ 0 0 0 7 ?;􎁱 Ǟ|7H橠4Ufwշnz 0 0 0H[@=XӀ`\0@*Nխ w~u 0 0 01sŀ`-N ƛPt$TP[OOM[݀````logW=3kY3π`\0@*Nխ f~u\```{ǝ10q9& b<SSuM@````Uoݾ]so@0޿1s }1HԩuNׯy#17sc``р`:fnsʼnpx ` JSuk}W}0 0 0 0 m c`uf6k }1HԩuLׯ~1 0 0 0p܀`x;6 {>a g@0ބ#AL?Tݚ}~nzUߚ 3 0 0 j mվ۷k c=f`Nq/:U.u9o$fn 0 0 03s;[qN8NV4 oBёSAin>U?U7ouf``` ߿_c c@0άf>q/:U.u}p/```wzcc> ƛPt$TP[OOM[s7{```U ߿wvaxs i@0.xc Pօ;]:̍````Fqngt똹b@0'~ɊM(:y*(Mխ٧]n 0 0 0 0psu יڬg@0.xc P3]ú_ 0 0 0qNۀ`|8̇x1` JSuk}W}k&z 0 0 0Un߮9 7 c9 o TݺpqX缑1 0 0 h@0n3[ 9o8Yр` EG~0O5TT ```6~v~s1:v^5 o Tݺ`qX]K```8n@0~wc=03 oBё SAin>U?U7o^```V5~j5{̱30`㍁T@[t:7s37``` ƹѭcv8'[ '+7H橠4Ufwշ3 0 0 0ޯz11 _gkf``㍁T@[t: ~ 0 0 0 Ə;c`l1rM(:y*(Mխ٧]= 0 0 0V}0߀`9c4 b1 Su_aFbn 0 0 0083un1 ddE& @<SSuV7````8@U9:ym 3 b1 Su낙_aw/b```qw m@0>|?@΀` EG~0O5TT5w{g``X@۪}o1zq_7Run]8sH```f4 vF-lq7h@0ބ#H?Tݚ}~nzU 0 0 0gH;{9Xǀ`|Y;͚}q_7Run]08._ 0 0 0 7 ?;􎁱 Ǟ|7H橠4Ufwշnz 0 0 0H[@=XӀ`\0@*Nխ w~u 0 0 01sŀ`-N ƛPt$TP[OOM[݀````logW=3kY3π`\0@*Nխ f~u\```{ǝ10q9& b<SSuM@````Uoݾ]so@0޿1s }1HԩuNׯy#17sc``р`:fnsʼnpx ` JSuk}W}0 0 0 0 m c`uf6k }1HԩuLׯ~1 0 0 0p܀`x;6 {>a g@0ބ#AL?Tݚ}~nzUߚ 3 0 0 j mվ۷k c=f`Nq/:U.u9o$fn 0 0 03s;[qN8NV4 oBёSAin>U?U7ouf``` ߿_c c@0άf>q/:U.u}p/```wzcc> ƛPt$TP[OOM[s7{```U ߿wvaxs i@0.xc Pօ;]:̍````Fqngt똹b@0'~ɊM(:y*(Mխ٧]n 0 0 0 0psu יڬg@0.xc P3]ú_ 0 0 0qNۀ`|8̇x1` JSuk}W}k&z 0 0 0Un߮9 7 c9 o TݺpqX缑1 0 0 h@0n3[ 9o8Yр` EG~0O5TT ```6~v~s1:v^5 o Tݺ`qX]K```8n@0~wc=03 oBё SAin>U?U7o^```V5~j5{̱30`㍁T@[t:7s37``` ƹѭcv8'[ '+7H橠4Ufwշ3 0 0 0ޯz11 _gkf``㍁T@[t: ~ 0 0 0 Ə;c`l1rM(:y*(Mխ٧]= 0 0 0V}0߀`9c4 b1 Su_aFbn 0 0 0083un1 ddE& @<SSuV7````8@U9:ym 3 b1 Su낙_aw/b```qw m@0>|?@΀` EG~0O5TT5w{g``X@۪}o1zq_7Run]8sH```f4 vF-lq7h@0ބ#H?Tݚ}~nzU 0 0 0gH;{9Xǀ`|Y;͚}q_7Run]08._ 0 0 0 7 ?;􎁱 Ǟ|7H橠4Ufwշnz 0 0 0H[@=XӀ`\0@*Nխ w~u 0 0 01sŀ`-N ƛPt$TP[OOM[݀````logW=3kY3π`\0@*Nխ f~u\```{ǝ10q9& b<SSuM@````Uoݾ]so@0޿1s }1HԩuNׯy#17sc``р`:fnsʼnpx ` JSuk}W}0 0 0 0 m c`uf6k }1HԩuLׯ~1 0 0 0p܀`x;6 {>a g~{MIENDB`strawberry-graphql-0.287.0/docs/editors/vscode.md000066400000000000000000000021701511033167500217400ustar00rootroot00000000000000--- title: Visual studio code --- # Visual studio code Strawberry comes with support for both MyPy and Pylance, Microsoft's own language server for Python. This guide will explain how to configure Visual Studio Code and Pylance to work with Strawberry. ## Install Pylance The first thing we need to do is to install [Pylance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance), this is the extension that enables type checking and intellisense for Visual Studio Code. Once the extension is installed, we need to configure it to enable type checking. To do so we need to change or add the following two settings: ```json { "python.languageServer": "Pylance", "python.analysis.typeCheckingMode": "basic" } ``` The first settings tells the editor to use Pylance as the language server. The second setting tells the editor to enable type checking by using the basic type checking mode. At the moment strict mode is not supported. Once you have configured the settings, you can restart VS Code and you should be getting type checking errors in vscode. strawberry-graphql-0.287.0/docs/errors.md000066400000000000000000000024231511033167500203210ustar00rootroot00000000000000--- title: Errors --- # Errors in strawberry Strawberry has built-in errors for when something goes wrong with the creation and usage of the schema. It also provides a custom exception handler for improving how errors are printed and to make it easier to find the exception source, for example the following code: ```python import strawberry @strawberry.type class Query: @strawberry.field def hello_world(self): return "Hello there!" schema = strawberry.Schema(query=Query) ``` will show the following exception on the command line: ```text error: Missing annotation for field `hello_world` @ demo.py:7 6 | @strawberry.field ❱ 7 | def hello_world(self): ^^^^^^^^^^^ resolver missing annotation 8 | return "Hello there!" To fix this error you can add an annotation, like so `def hello_world(...) -> str:` Read more about this error on https://errors.strawberry.rocks/missing-return-annotation ``` These errors are only enabled when `rich` and `libcst` are installed. You can install Strawberry with errors enabled by running: ```shell pip install "strawberry-graphql[cli]" ``` If you want to disable the errors you can do so by setting the `STRAWBERRY_DISABLE_RICH_ERRORS` environment variable to `1`. strawberry-graphql-0.287.0/docs/errors/000077500000000000000000000000001511033167500177765ustar00rootroot00000000000000strawberry-graphql-0.287.0/docs/errors/_template.md000066400000000000000000000003351511033167500222730ustar00rootroot00000000000000--- title: Some Error --- # Some Error Error ## Description This error is thrown when ... for example the following code will throw this error: ```python import strawberry schema = strawberry.Schema(query=Query) ``` strawberry-graphql-0.287.0/docs/errors/conflicting-arguments.md000066400000000000000000000011011511033167500246130ustar00rootroot00000000000000--- title: Conflicting Arguments Error --- # Conflicting Arguments Error ## Description This error is thrown when you define a resolver with multiple arguments that conflict with each other, like "self", "root", or any arguments annotated with strawberry.Parent. For example the following code will throw this error: ```python import strawberry @strawberry.type class Query: @strawberry.field def hello( self, root, parent: strawberry.Parent[str] ) -> str: # <-- self, root, and parent all identify the same input return f"hello world" ``` strawberry-graphql-0.287.0/docs/errors/duplicated-type-name.md000066400000000000000000000021461511033167500243360ustar00rootroot00000000000000--- title: Duplicated Type Name Error --- # Duplicated Type Name Error ## Description This error is thrown when you try to register two types with the same name in the schema. For example, the following code will throw this error: ```python import strawberry @strawberry.type class User: name: str @strawberry.type(name="User") class UserB: name: str @strawberry.type class Query: user: User user_b: UserB schema = strawberry.Schema(query=Query) ``` ## How to fix this error To fix this error you need to make sure that all the types in your schema have unique names. For example in our example above we can fix this error by changing the `name` argument of the `UserB` type: ```python import strawberry @strawberry.type class User: name: str # Note: Strawberry will automatically use the name of the class # if it is not provided, in this case we are passing the name # to show how it works and how to fix the error @strawberry.type(name="UserB") class UserB: name: str @strawberry.type class Query: user: User user_b: UserB schema = strawberry.Schema(query=Query) ``` strawberry-graphql-0.287.0/docs/errors/invalid-argument-type.md000066400000000000000000000017601511033167500245510ustar00rootroot00000000000000--- title: Invalid Argument Type Error --- # Invalid Argument Type Error ## Description This error is thrown when an argument is of the wrong type, it usually happens when passing **unions** or **interfaces** as an argument, for example the following code will throw this error: ```python import strawberry from typing import Union, Annotated @strawberry.type class TypeA: id: strawberry.ID ExampleUnion = Annotated[Union[TypeA], strawberry.union("ExampleUnion")] @strawberry.type class Query: @strawberry.field def example(self, data: Example) -> str: return "this is an example" schema = strawberry.Schema(query=Query) ``` ## Using union types as arguments The latest [GraphQL specification](https://spec.graphql.org/October2021/) doesn't allow using unions as arguments. There's currently an [RFC for adding a `oneOf` directive](https://github.com/graphql/graphql-spec/pull/825) that might work for your use case, but it's not yet implemented in the spec and Strawberry strawberry-graphql-0.287.0/docs/errors/invalid-superclass-interface.md000066400000000000000000000011261511033167500260660ustar00rootroot00000000000000--- title: Invalid Superclass Interface Error --- # Invalid Superclass Interface Error ## Description This error is thrown when you define a class that has the `strawberry.input` decorator but also inherits from one or more classes with the `strawberry.interface` decorator. The underlying reason for this is that in GraphQL, input types cannot implement interfaces. For example, the following code will throw this error: ```python import strawberry @strawberry.interface class SomeInterface: some_field: str @strawberry.input class SomeInput(SomeInterface): another_field: int ``` strawberry-graphql-0.287.0/docs/errors/invalid-type-for-union-merge.md000066400000000000000000000026511511033167500257400ustar00rootroot00000000000000--- title: Invalid Type for Union Merge Error --- # Invalid Type for Union Merge Error ## Description This error is thrown when trying to extend an union with a type that's not allowed in unions, for example the following code will throw this error: ```python import strawberry from typing import Union, Annotated @strawberry.type class Example: name: str ExampleUnion = Annotated[Union[Example], strawberry.union("ExampleUnion")] @strawberry.type class Query: field: ExampleUnion | int schema = strawberry.Schema(query=Query) ``` This happens because GraphQL doesn't support scalars as union members. ## How to fix this error At the moment Strawberry doesn't have a proper way to merge unions and types, but you can still create a union type that combines multiple types manually. Since GraphQL doesn't allow scalars as union members, a workaround is to create a wrapper type that contains the scalar value and use that instead. For example the following code will create a union type between `Example` and `IntWrapper` which is a wrapper on top of the `int` scalar: ```python import strawberry from typing import Union, Annotated @strawberry.type class Example: name: str @strawberry.type class IntWrapper: value: int ExampleUnion = Annotated[Union[Example, IntWrapper], strawberry.union("ExampleUnion")] @strawberry.type class Query: field: ExampleUnion schema = strawberry.Schema(query=Query) ``` strawberry-graphql-0.287.0/docs/errors/invalid-union-type.md000066400000000000000000000024131511033167500240530ustar00rootroot00000000000000--- title: Invalid Type for Union Error --- # Invalid Type for Union Error ## Description This error is thrown when trying to create an union with one or more type that's are allowed in unions, for example the following code will throw this error: ```python import strawberry from typing import Union, Annotated @strawberry.type class Example: name: str ExampleUnion = Annotated[Union[Example, int], strawberry.union("ExampleUnion")] @strawberry.type class Query: field: ExampleUnion schema = strawberry.Schema(query=Query) ``` This happens because GraphQL doesn't support scalars as union members. ## How to fix this error Since GraphQL doesn't allow scalars as union members, a workaround is to create a wrapper type that contains the scalar value and use that instead. For example the following code will create a union type between `Example` and `IntWrapper` which is a wrapper on top of the `int` scalar: ```python import strawberry from typing import Union, Annotated @strawberry.type class Example: name: str @strawberry.type class IntWrapper: value: int ExampleUnion = Annotated[Union[Example, IntWrapper], strawberry.union("ExampleUnion")] @strawberry.type class Query: field: ExampleUnion schema = strawberry.Schema(query=Query) ``` strawberry-graphql-0.287.0/docs/errors/missing-arguments-annotations.md000066400000000000000000000016431511033167500263330ustar00rootroot00000000000000--- title: Missing arguments annotation Error --- # Missing arguments annotation Error ## Description This error is thrown when an argument is missing an annotation, for example the following code will throw this error: ```python import strawberry @strawberry.type class Query: @strawberry.field def hello(self, name) -> str: # <-- note name here is missing an annotation return f"hello {name}" schema = strawberry.Schema(query=Query) ``` This happens because Strawberry needs to know the type of every argument to be able to generate the correct GraphQL type. ## How to fix this error You can fix this error by adding an annotation to the argument, for example, the following code will fix this error: ```python import strawberry @strawberry.type class Query: @strawberry.field def hello(self, name: str) -> str: return f"hello {name}" schema = strawberry.Schema(query=Query) ``` strawberry-graphql-0.287.0/docs/errors/missing-field-annotation.md000066400000000000000000000016051511033167500252240ustar00rootroot00000000000000--- title: Missing field annotation Error --- # Missing field annotation Error ## Description This error is thrown when a field on a class is missing an annotation, for example the following code will throw this error: ```python import strawberry @strawberry.type class Query: name: str age = strawberry.field( name="ageInYears" ) # note that here we don't have a type for this field schema = strawberry.Schema(query=Query) ``` This happens because Strawberry needs to know the type of every field for a type to be able to generate the correct GraphQL type. ## How to fix this error You can fix this error by adding an annotation to the field, for example, the following code will fix this error: ```python import strawberry @strawberry.type class Query: name: str age: int = strawberry.field(name="ageInYears") schema = strawberry.Schema(query=Query) ``` strawberry-graphql-0.287.0/docs/errors/missing-return-annotation.md000066400000000000000000000016201511033167500254550ustar00rootroot00000000000000--- title: Missing return annotation Error --- # Missing return annotation Error ## Description This error is thrown when a resolver and it's corresponding field don't have a return annotation, for example the following code will throw this error: ```python import strawberry @strawberry.type class Query: @strawberry.field def example(self): return "this is an example" schema = strawberry.Schema(query=Query) ``` This happens because Strawberry needs to know the return type of the resolver to be able to generate the correct GraphQL type. ## How to fix this error You can fix this error by adding a return annotation to the resolver, for example, the following code will fix this error: ```python import strawberry @strawberry.type class Query: @strawberry.field def example(self) -> str: return "this is an example" schema = strawberry.Schema(query=Query) ``` strawberry-graphql-0.287.0/docs/errors/node-id-annotation.md000066400000000000000000000020431511033167500240060ustar00rootroot00000000000000--- title: Node ID annotation error --- # Node `ID` annotation errors ## Description This error is thrown when a `relay.Node` implemented type can't resolve its `id` field, due to it being missing or multiple annotated fields being found. The following code will throw this error: ```python import strawberry from strawberry import relay @strawberry.type class Fruit(relay.Node): code: str name: str ``` This happens because `relay.Node` don't know which field should be used to resolve to generate its `GlobalID` field. The following would also throw this errors because multiple candidates were found: ```python import strawberry from strawberry import relay @strawberry.type class Fruit(relay.Node): code: relay.NodeID[str] name: relay.NodeID[str] ``` ## How to fix this error When inheriting from `relay.Node`, you should annotate exactly one `NodeID` field in the type, like: ```python import strawberry from strawberry import relay @strawberry.type class Fruit(relay.Node): code: relay.NodeID[str] name: str ``` strawberry-graphql-0.287.0/docs/errors/object-is-not-an-enum.md000066400000000000000000000015441511033167500243370ustar00rootroot00000000000000--- title: Object is not an Enum Error --- # Object is not an Enum Error ## Description This error is thrown when applying `@strawberry.enum` to a non-enum object, for example the following code will throw this error: ```python import strawberry # note the lack of @strawberry.enum here: class NotAnEnum: A = "A" @strawberry.type class Query: field: NotAnEnum schema = strawberry.Schema(query=Query) ``` This happens because Strawberry expects all enums to be subclasses of `Enum`. ## How to fix this error You can fix this error by making sure the class you're applying `@strawberry.enum` to is a subclass of `Enum`. For example, the following code will fix this error: ```python import strawberry @strawberry.enum class NotAnEnum: A = "A" @strawberry.type class Query: field: NotAnEnum schema = strawberry.Schema(query=Query) ``` strawberry-graphql-0.287.0/docs/errors/object-is-not-class.md000066400000000000000000000015361511033167500241050ustar00rootroot00000000000000--- title: Object is not an Class Error --- # Object is not an Class Error ## Description This error is thrown when applying `@strawberry.type/interface/input` to a non-class object, for example the following code will throw this error: ```python import strawberry @strawberry.type def a_function(): ... @strawberry.type class Query: field: a_function schema = strawberry.Schema(query=Query) ``` This happens because Strawberry expects all enums to be subclasses of `Enum`. ## How to fix this error You can fix this error by making sure the class you're applying `@strawberry.type/interface/input` to is a class. For example, the following code will fix this error: ```python import strawberry @strawberry.type class AFunction: field: int @strawberry.type class Query: field: AFunction schema = strawberry.Schema(query=Query) ``` strawberry-graphql-0.287.0/docs/errors/permission-fail-silently-requires-optional.md000066400000000000000000000030171511033167500307430ustar00rootroot00000000000000--- title: Using silent permissions on optional fields --- # Cannot use fail_silently on non-optional or non-list field ## Description This error is thrown when a permission extension is configured to use silent permissions on a field that is required and not a list: ```python import strawberry from strawberry.permission import PermissionExtension @strawberry.type class Query: @strawberry.field( extensions=[PermissionExtension([IsAuthorized()], fail_silently=True)] ) def name( self, ) -> str: # This is a required field, the schema type will be NonNull (String!) return "ABC" schema = strawberry.Schema(query=Query) ``` This happens because fail_silently is suppsed to hide the field from a user without an error in case of no permissions. However, non-nullable fields always raise an error when they are set to null. The only exception to that is a list, because an empty list can be returned. ## How to fix this error You can fix this error by making this field an optional field. For example, the following code will fix this error in the above example: ```python import strawberry from strawberry.permission import PermissionExtension @strawberry.type class Query: @strawberry.field( extensions=[PermissionExtension([IsAuthorized()], fail_silently=True)] ) def name(self) -> str | None: # This is now a nullable field return "ABC" schema = strawberry.Schema(query=Query) ``` Alternatively, not using `fail_silently` might be a good design choice as well. strawberry-graphql-0.287.0/docs/errors/private-strawberry-field.md000066400000000000000000000026351511033167500252630ustar00rootroot00000000000000--- title: Private Strawberry Field Error --- # Private Strawberry Field Error ## Description This error is thrown when using both `strawberry.Private[Type]` and `strawberry.field` on the same field, for example the following code will throw this error: ```python import strawberry @strawberry.type class Query: name: str age: strawberry.Private[int] = strawberry.field(name="ageInYears") schema = strawberry.Schema(query=Query) ``` This happens because a `strawberry.Private` field is not going to be exposed in the GraphQL schema, so using `strawberry.field` on that field won't be useful, since it is meant to be used to change information about a field that is exposed in the GraphQL schema. This makes sense, but now we don't have a way to do something like: strawberry.Private[list[str]] = strawberry.field(default_factory=list) (workaround is to use dataclasses.field, explained below) ## How to fix this error You can fix this error by either removing the `strawberry.Private` annotation or by removing the `strawberry.field` usage. If you need to specify a default value using `default_factory` you can use `dataclasses.field` instead of `strawberry.field`. For example: ```python import strawberry import dataclasses @strawberry.type class Query: name: str tags: strawberry.Private[str] = dataclasses.field(default_factory=list) schema = strawberry.Schema(query=Query) ``` strawberry-graphql-0.287.0/docs/errors/relay-wrong-annotation.md000066400000000000000000000031151511033167500247360ustar00rootroot00000000000000--- title: Relay wrong annotation Error --- # Relay wrong annotation error ## Description This error is thrown when a field on a relay connection has a wrong type annotation. For example, the following code will throw this error: ```python from typing import List import strawberry from strawberry import relay @strawberry.type class MyType(relay.Node): ... @strawberry.type class Query: # The annotation is not a subclass of relay.Connection my_type_conn: List[MyType] = relay.connection() # Missing the Connection class annotation @relay.connection def my_type_conn_with_resolver(self) -> List[MyType]: ... # The connection class is not a subclass of relay.Connection @relay.connection(List[MyType]) def my_type_conn_with_resolver2(self) -> List[MyType]: ... ``` ## How to fix this error You can fix this error by properly annotating your attribute or resolver with `relay.Connection` type subclass. For example: ```python from typing import List import strawberry from strawberry import relay @strawberry.type class MyType(relay.Node): ... def get_my_type_list() -> List[MyType]: ... @strawberry.type class Query: my_type_conn: relay.Connection[MyType] = relay.connection( resolver=get_my_type_list, ) # Missing the Connection class annotation @relay.connection(relay.Connection[MyType]) def my_type_conn_with_resolver(self) -> List[MyType]: ... # The connection class is not a subclass of relay.Connection @relay.connection(relay.Connection[MyType]) def my_type_conn_with_resolver2(self) -> List[MyType]: ... ``` strawberry-graphql-0.287.0/docs/errors/relay-wrong-resolver-annotation.md000066400000000000000000000033031511033167500265740ustar00rootroot00000000000000--- title: Relay wrong resolver annotation Error --- # Relay wrong resolver annotation error ## Description This error is thrown when a field on a relay connection was defined with a resolver that returns something that is not compatible with pagination. For example, the following code would throw this error: ```python from typing import Any import strawberry from strawberry import relay @strawberry.type class MyType(relay.Node): ... @strawberry.type class Query: @relay.connection(relay.Connection[MyType]) def some_connection_returning_mytype(self) -> MyType: ... @relay.connection(relay.Connection[MyType]) def some_connection_returning_any(self) -> Any: ... ``` This happens because the connection resolver needs to return something that can be paginated, usually an iterable/generator of the connection type itself. ## How to fix this error You can fix this error by annotating the resolver with one of the following supported types: - `List[]` - `Iterator[]` - `Iterable[]` - `AsyncIterator[]` - `AsyncIterable[]` - `Generator[, Any, Any]` - `AsyncGenerator[, Any]` For example: ```python from typing import Any import strawberry from strawberry import relay @strawberry.type class MyType(relay.Node): ... @strawberry.type class Query: @relay.connection(relay.Connection[MyType]) def some_connection(self) -> Iterable[MyType]: ... ``` Note that if you are returning a type different than the connection type, you will need to subclass the connection type and override its `resolve_node` method to convert it to the correct type, as explained in the [relay guide](../guides/relay). strawberry-graphql-0.287.0/docs/errors/scalar-already-registered.md000066400000000000000000000024141511033167500253400ustar00rootroot00000000000000--- title: Scalar already registered Error --- # Scalar already registered Error ## Description This error is thrown when trying to use a scalar that is already registered. This usually happens when using the same name for different scalars, for example the following code will throw this error: ```python import strawberry MyCustomScalar = strawberry.scalar( str, name="MyCustomScalar", ) MyCustomScalar2 = strawberry.scalar( int, name="MyCustomScalar", ) @strawberry.type class Query: scalar_1: MyCustomScalar scalar_2: MyCustomScalar2 strawberry.Schema(Query) ``` This happens because different types in Strawberry (and GraphQL) cannot have the same name. This error might happen also when trying to defined a scalar with the same name as a type. ## How to fix this error You can fix this error by either reusing the existing scalar, or by changing the name of one of them, for example in this code we renamed the second scalar: ```python import strawberry MyCustomScalar = strawberry.scalar( str, name="MyCustomScalar", ) MyCustomScalar2 = strawberry.scalar( int, name="MyCustomScalar2", ) @strawberry.type class Query: scalar_1: MyCustomScalar scalar_2: MyCustomScalar2 strawberry.Schema(Query) ``` strawberry-graphql-0.287.0/docs/errors/unresolved-field-type.md000066400000000000000000000021101511033167500245400ustar00rootroot00000000000000--- title: Unresolved Field Type Error --- # Unresolved Field Type Error ## Description This error is thrown when Strawberry is unable to resolve a field type. This happens when the type of a field is not accessible in the current scope. For example the following code will throw this error: ```python import strawberry @strawberry.type class Query: user: "User" schema = strawberry.Schema(query=Query) ``` Note that we are using the forward reference syntax to define the type of the field. This is because the `User` type is not yet defined when the `Query` type is defined. This would also happen when using `from __future__ import annotations`. To fix this error you need to import the type that you are using in the field, for example: ```python import strawberry from .user import User @strawberry.type class Query: user: "User" schema = strawberry.Schema(query=Query) ``` Unfortunately, this won't work in cases where there's a circular dependency between types. In this case, you can use `strawberry.LazyType`. strawberry-graphql-0.287.0/docs/errors/unsupported-type.md000066400000000000000000000003541511033167500236710ustar00rootroot00000000000000--- title: Unsupported Type Error --- # Unsupported Type Error ## Description This error is thrown when trying to convert arguments with a type that Strawberry doesn't know about. It shouldn't happen with normal usage of Strawberry. strawberry-graphql-0.287.0/docs/extensions.md000066400000000000000000000011511511033167500212010ustar00rootroot00000000000000--- title: Extensions --- # Extensions Extensions allow you, as an application developer, to customise the GraphQL execution flow based on your needs. Strawberry provides multiple built in extensions that allow you to extend the capability of your GraphQL server. If you can't find what you need among the [built-in extensions](#built-in-extensions), you can also build your own custom extension based on a standard interface. Check out the [schema extensions](./guides/custom-extensions.md) and [field extensions](./guides/field-extensions.md) guides to find out more. ## Built-in extensions strawberry-graphql-0.287.0/docs/extensions/000077500000000000000000000000001511033167500206615ustar00rootroot00000000000000strawberry-graphql-0.287.0/docs/extensions/_template.md000066400000000000000000000015601511033167500231570ustar00rootroot00000000000000--- title: Extension Name summary: A summary of the extension. tags: comma,separated,list,of,tags --- # `ExtensionName` This extension does ... ## Usage example: ```python import strawberry from strawberry.extensions import ExtensionName @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "Hello, world!" schema = strawberry.Schema( Query, extensions=[ ExtensionName(), ], ) ``` ## API reference: ```python class ExtensionName(an_argument=None): ... ``` #### `an_argument: Optional[str] = None` Description of the argument. ## More examples:
Using `an_argument` ```python import strawberry from strawberry.extensions import ValidationCache schema = strawberry.Schema( Query, extensions=[ ExtensionName(an_argument="something"), ], ) ```
strawberry-graphql-0.287.0/docs/extensions/add-validation-rules.md000066400000000000000000000051401511033167500252130ustar00rootroot00000000000000--- title: Add Validation Rules summary: Add GraphQL validation rules. tags: validation,security --- # `AddValidationRules` This extension allows you add custom validation rules. See [graphql.validation.rules.custom](https://github.com/graphql-python/graphql-core/tree/main/src/graphql/validation/rules/custom) for some custom rules that can be added from GraphQl-core. ## Usage example: ```python import strawberry from strawberry.extensions import AddValidationRules from graphql import ValidationRule @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "Hello, world!" class MyCustomRule(ValidationRule): ... schema = strawberry.Schema( Query, extensions=[ AddValidationRules(MyCustomRule), ], ) ``` ## API reference: ```python class AddValidationRules(validation_rules): ... ``` #### `validation_rules: List[Type[ASTValidationRule]]` List of GraphQL validation rules. ## More examples:
Adding a custom rule ```python import strawberry from strawberry.extensions import AddValidationRules from graphql import ValidationRule @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "Hello, world!" class CustomRule(ValidationRule): def enter_field(self, node, *args) -> None: if node.name.value == "example": self.report_error(GraphQLError("Can't query field 'example'")) schema = strawberry.Schema( Query, extensions=[ AddValidationRules([CustomRule]), ], ) result = schema.execute_sync("{ example }") assert str(result.errors[0]) == "Can't query field 'example'" ```
Adding the `NoDeprecatedCustomRule` from GraphQL-core ```python import strawberry from strawberry.extensions import AddValidationRules from graphql.validation import NoDeprecatedCustomRule @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "Hello, world!" schema = strawberry.Schema( Query, extensions=[ AddValidationRules([NoDeprecatedCustomRule]), ], ) ```
Adding the `NoSchemaIntrospectionCustomRule` from GraphQL-core ```python import strawberry from strawberry.extensions import AddValidationRules from graphql.validation import NoSchemaIntrospectionCustomRule @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "Hello, world!" schema = strawberry.Schema( Query, extensions=[ AddValidationRules([NoSchemaIntrospectionCustomRule]), ], ) ```
strawberry-graphql-0.287.0/docs/extensions/apollo-tracing.md000066400000000000000000000020701511033167500241150ustar00rootroot00000000000000--- title: Apollo Tracing summary: Add Apollo tracing to your GraphQL server. tags: tracing --- # `ApolloTracingExtension` This extension adds [tracing information](https://github.com/apollographql/apollo-tracing) to your response for [Apollo Engine](https://www.apollographql.com/platform/). ## Usage example: ```python import strawberry from strawberry.extensions.tracing import ApolloTracingExtension @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "Hello, world!" schema = strawberry.Schema( Query, extensions=[ ApolloTracingExtension, ], ) ``` If you are not running in an Async context then you'll need to use the sync version: ```python import strawberry from strawberry.extensions.tracing import ApolloTracingExtensionSync @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "Hello, world!" schema = strawberry.Schema( Query, extensions=[ ApolloTracingExtensionSync, ], ) ``` ## API reference: _No arguments_ strawberry-graphql-0.287.0/docs/extensions/datadog.md000066400000000000000000000034041511033167500226070ustar00rootroot00000000000000--- title: Datadog summary: Add Datadog tracing to your GraphQL server. tags: tracing --- # `DatadogExtension` This extension adds support for tracing with Datadog. Make sure you have `ddtrace` installed before using this extension. ```shell pip install ddtrace ``` ## Usage example: ```python import strawberry from strawberry.extensions.tracing import DatadogTracingExtension @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "Hello, world!" schema = strawberry.Schema( Query, extensions=[ DatadogTracingExtension, ], ) ``` If you are not running in an Async context then you'll need to use the sync version: ```python import strawberry from strawberry.extensions.tracing import DatadogTracingExtensionSync @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "Hello, world!" schema = strawberry.Schema( Query, extensions=[ DatadogTracingExtensionSync, ], ) ``` ## API reference: _No arguments_ ## Extending the extension ### Overriding the `create_span` method You can customize any of the spans or add tags to them by overriding the `create_span` method. Example: ```python from ddtrace import Span from strawberry.extensions import LifecycleStep from strawberry.extensions.tracing import DatadogTracingExtension class DataDogExtension(DatadogTracingExtension): def create_span( self, lifecycle_step: LifecycleStep, name: str, **kwargs, ) -> Span: span = super().create_span(lifecycle_step, name, **kwargs) if lifecycle_step == LifecycleStep.OPERATION: span.set_tag("graphql.query", self.execution_context.query) return span ``` strawberry-graphql-0.287.0/docs/extensions/disable-introspection.md000066400000000000000000000024471511033167500255130ustar00rootroot00000000000000--- title: Disable Introspection summary: Disable all introspection queries. tags: security,validation --- # `DisableIntrospection` The `DisableIntrospection` extension disables all introspection queries for the schema. This can be useful to prevent clients from discovering unreleased or internal features of the API. ## Usage example: ```python import strawberry from strawberry.extensions import DisableIntrospection @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "Hello, world!" schema = strawberry.Schema( Query, extensions=[ DisableIntrospection(), ], ) ``` ## API reference: _No arguments_ ## Example query: Running any query including the introspection field `__schema` will result in an error. Consider the following query, for example: ```graphql query { __schema { __typename } } ``` Running it against the schema with the `DisableIntrospection` extension enabled will result in an error response indicating that introspection has been disabled: ```json { "data": null, "errors": [ { "message": "GraphQL introspection has been disabled, but the requested query contained the field '__schema'.", "locations": [ { "line": 2, "column": 3 } ] } ] } ``` strawberry-graphql-0.287.0/docs/extensions/disable-validation.md000066400000000000000000000016261511033167500247430ustar00rootroot00000000000000--- title: Disable Validation summary: Disable all query validation. tags: performance,validation --- # `DisableValidation` This extensions disables all query validation. This can be useful to improve performance in some specific cases, for example when dealing with internal APIs where queries can be trusted. Only do this if you know what you are doing! Disabling validation breaks the safety of having typed schema. If you are trying to improve performance you might want to consider using the [ValidationCache](./validation-cache) instead. ## Usage example: ```python import strawberry from strawberry.extensions import DisableValidation @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "Hello, world!" schema = strawberry.Schema( Query, extensions=[ DisableValidation(), ], ) ``` ## API reference: _No arguments_ strawberry-graphql-0.287.0/docs/extensions/input-mutation.md000066400000000000000000000031261511033167500242020ustar00rootroot00000000000000--- title: Input Mutation Extension summary: Automatically create Input types for mutations tags: QoL --- # `InputMutationExtension` The pattern of defining a mutation that receives a single [input type](../types/input-types.md) argument called `input` is a common practice in GraphQL. It helps to keep the mutation signatures clean and makes it easier to manage complex mutations with multiple arguments. The `InputMutationExtension` is a Strawberry field extension that allows you to define a mutation with multiple arguments without having to manually create an input type for it. Instead, it generates an input type based on the arguments of the mutation resolver. ## Usage example: ```python import strawberry from strawberry.field_extensions import InputMutationExtension @strawberry.type class User: username: str @strawberry.type class Query: hello: str @strawberry.type class Mutation: @strawberry.mutation(extensions=[InputMutationExtension()]) def register_user( self, username: str, password: str, ) -> User: user = User(username=username) # maybe persist the user in a database return user schema = strawberry.Schema(query=Query, mutation=Mutation) ``` The Strawberry schema above and the usage of the `InputMutationExtension` will result in the following GraphQL schema: ```graphql type User { username: String! } input RegisterUserInput { username: String! password: String! } type Mutation { registerUser(input: RegisterUserInput!): User! } type Query { hello: String! } ``` ## API reference: _No arguments_ strawberry-graphql-0.287.0/docs/extensions/mask-errors.md000066400000000000000000000044461511033167500234600ustar00rootroot00000000000000--- title: Mask Errors summary: Hide error messages from the client. tags: security --- # `MaskErrors` This extension hides error messages from the client to prevent exposing sensitive details. By default it masks all errors raised in any field resolver. ## Usage example: ```python import strawberry from strawberry.extensions import MaskErrors @strawberry.type class Query: @strawberry.field def hidden_error(self) -> str: raise KeyError("This error will not be visible") schema = strawberry.Schema( Query, extensions=[ MaskErrors(), ], ) ``` ## API reference: ```python class MaskErrors( should_mask_error=default_should_mask_error, error_message="Unexpected error." ): ... ``` #### `should_mask_error: Callable[[GraphQLError], bool] = default_should_mask_error` Predicate function to check if a GraphQLError should be masked or not. Use the `original_error` attribute to access the original error that was raised in the resolver. The `default_should_mask_error` function always returns `True`. #### `error_message: str = "Unexpected error."` The error message to display to the client when there is an error. ## More examples:
Hide some exceptions ```python import strawberry from strawberry.extensions import MaskErrors from graphql.error import GraphQLError class VisibleError(Exception): pass @strawberry.type class Query: @strawberry.field def visible_error(self) -> str: raise VisibleError("This error will be visible") def should_mask_error(error: GraphQLError) -> bool: original_error = error.original_error if original_error and isinstance(original_error, VisibleError): return False return True schema = strawberry.Schema( Query, extensions=[ MaskErrors(should_mask_error=should_mask_error), ], ) ```
Change error message ```python import strawberry from strawberry.extensions import MaskErrors @strawberry.type class Query: @strawberry.field def hidden_error(self) -> str: raise KeyError("This error will not be visible") schema = strawberry.Schema( Query, extensions=[ MaskErrors(error_message="Oh no! An error occured. Very sorry about that."), ], ) ```
strawberry-graphql-0.287.0/docs/extensions/max-aliases-limiter.md000066400000000000000000000013671511033167500250610ustar00rootroot00000000000000--- title: Max Aliases Limiter summary: Add a validator to limit the maximum number of aliases in a GraphQL document. tags: security --- # `MaxAliasesLimiter` This extension adds a validator to limit the maximum number of aliases in a GraphQL document. ## Usage example: ```python import strawberry from strawberry.extensions import MaxAliasesLimiter @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "Hello, world!" schema = strawberry.Schema( Query, extensions=[ MaxAliasesLimiter(max_alias_count=15), ], ) ``` ## API reference: ```python class MaxAliasesLimiter(max_alias_count): ... ``` #### `max_alias_count: int` The maximum allowed number of aliases in a GraphQL document. strawberry-graphql-0.287.0/docs/extensions/max-tokens-limiter.md000066400000000000000000000020071511033167500247330ustar00rootroot00000000000000--- title: Max Tokens Limiter summary: Add a validator to limit the maximum number of tokens in a GraphQL document. tags: security --- # `MaxTokensLimiter` This extension adds a validator to limit the maximum number of tokens in a GraphQL document sent to the server. ## Usage example: ```python import strawberry from strawberry.extensions import MaxTokensLimiter @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "Hello, world!" schema = strawberry.Schema( Query, extensions=[ MaxTokensLimiter(max_token_count=1000), ], ) ``` With the above configuration, if a client sends a query with more than 1000 tokens, the server will respond with an error message. ## API reference: ```python class MaxTokensLimiter(max_token_count): ... ``` #### `max_token_count: int` The maximum allowed number of tokens in a GraphQL document. The following things are counted as tokens: - various brackets: "{", "}", "(", ")" - colon : - words Not counted: - quotes strawberry-graphql-0.287.0/docs/extensions/opentelemetry.md000066400000000000000000000053451511033167500241060ustar00rootroot00000000000000--- title: Open Telemetry summary: Add Open Telemetry tracing to your GraphQL server. tags: tracing --- # `OpenTelemetryExtension` This extension adds tracing information that is compatible with [Open Telemetry](https://opentelemetry.io/). This extension requires additional requirements: ```shell pip install 'strawberry-graphql[opentelemetry]' ``` ## Usage example: ```python import strawberry from strawberry.extensions.tracing import OpenTelemetryExtension @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "Hello, world!" schema = strawberry.Schema( Query, extensions=[ OpenTelemetryExtension, ], ) ``` If you are not running in an Async context then you'll need to use the sync version: ```python import strawberry from strawberry.extensions.tracing import OpenTelemetryExtensionSync @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "Hello, world!" schema = strawberry.Schema( Query, extensions=[ OpenTelemetryExtensionSync, ], ) ``` ## API reference: ```python class OpenTelemetryExtension(arg_filter=None): ... ``` #### `arg_filter: Optional[ArgFilter]` A function to filter certain field arguments from being included in the tracing data. ```python ArgFilter = Callable[[Dict[str, Any], GraphQLResolveInfo], Dict[str, Any]] ``` ## More examples:
Using `arg_filter` ```python import strawberry from strawberry.extensions.tracing import OpenTelemetryExtensionSync @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "Hello, world!" def arg_filter(kwargs, info): filtered_kwargs = {} for name, value in kwargs.items(): # Never include any arguments called "password" if name == "password": continue filtered_kwargs[name] = value return filtered_kwargs schema = strawberry.Schema( Query, extensions=[ OpenTelemetryExtensionSync( arg_filter=arg_filter, ), ], ) ```
Using `tracer_provider` ```python import strawberry from opentelemetry.trace import TracerProvider from strawberry.extensions.tracing import OpenTelemetryExtension @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "Hello, world!" class MyTracerProvider(TracerProvider): def get_tracer(self, name, version=None, schema_url=None): return super().get_tracer(name, version, schema_url) schema = strawberry.Schema( Query, extensions=[ OpenTelemetryExtension( tracer_provider=MyTracerProvider(), ), ], ) ```
strawberry-graphql-0.287.0/docs/extensions/parser-cache.md000066400000000000000000000023371511033167500235450ustar00rootroot00000000000000--- title: Parser Cache summary: Add in memory caching to the parsing step of query execution. tags: performance,caching,parsing --- # `ParserCache` This extension adds LRU caching to the parsing step of query execution to improve performance by caching the parsed result in memory. ## Usage example: ```python import strawberry from strawberry.extensions import ParserCache @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "Hello, world!" schema = strawberry.Schema( Query, extensions=[ ParserCache(), ], ) ``` ## API reference: ```python class ParserCache(maxsize=None): ... ``` #### `maxsize: Optional[int] = None` Set the maxsize of the cache. If `maxsize` is set to `None` then the cache will grow without bound. More info: https://docs.python.org/3/library/functools.html#functools.lru_cache ## More examples:
Using maxsize ```python import strawberry from strawberry.extensions import ParserCache @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "Hello, world!" schema = strawberry.Schema( Query, extensions=[ ParserCache(maxsize=100), ], ) ```
strawberry-graphql-0.287.0/docs/extensions/pyinstrument.md000066400000000000000000000013441511033167500237660ustar00rootroot00000000000000--- title: PyInstrument summary: Easily Instrument your Schema tags: instrumentation,profiling --- # `PyInstrument` This extension allows you to instrument your schema and inspect the call stack. ## Usage example: ```python import strawberry from strawberry.extensions import pyinstrument @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "Hello, world!" schema = strawberry.Schema( Query, extensions=[ pyinstrument.PyInstrument(report_path="pyinstrument.html"), ], ) ``` ## API reference: ```python class PyInstrument(report_Path=Path("pyinstrument.html")): ... ``` #### `report_path: Path = Path("pyinstrument.html")` Path to write the HTML PyInstrument report strawberry-graphql-0.287.0/docs/extensions/query-depth-limiter.md000066400000000000000000000112231511033167500251140ustar00rootroot00000000000000--- title: Query Depth Limiter summary: Add a validator to limit the query depth of GraphQL operations. tags: security --- # `QueryDepthLimiter` This extension adds a validator to limit the query depth of GraphQL operations. ## Usage example: ```python import strawberry from strawberry.extensions import QueryDepthLimiter @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "Hello, world!" schema = strawberry.Schema( Query, extensions=[ QueryDepthLimiter(max_depth=10), ], ) ``` ## API reference: ```python class QueryDepthLimiter(max_depth, callback=None, should_ignore=None): ... ``` #### `max_depth: int` The maximum allowed depth for any operation in a GraphQL document. #### `callback: Optional[Callable[[Dict[str, int]], None]` Called each time validation runs. Receives a dictionary which is a map of the depths for each operation. #### `should_ignore: Optional[Callable[[IgnoreContext], bool]]` Called at each field to determine whether the field should be ignored or not. Must be implemented by the user and returns `True` if the field should be ignored and `False` otherwise. The `IgnoreContext` class has the following attributes: - `field_name` of type `str`: the name of the field to be compared against - `field_args` of type `strawberry.extensions.query_depth_limiter.FieldArgumentsType`: the arguments of the field to be compared against - `query` of type `graphql.language.Node`: the query string - `context` of type `graphql.validation.ValidationContext`: the context passed to the query This argument is injected, regardless of name, by the `QueryDepthLimiter` class and should not be passed by the user. Instead, the user should write business logic to determine whether a field should be ignored or not by the attributes of the `IgnoreContext` class. ## Example with field_name: ```python import strawberry from strawberry.extensions import QueryDepthLimiter, IgnoreContext @strawberry.type class Book: title: str author: "User" @strawberry.type class User: favourite_books: list[Book] published_books: list[Book] @strawberry.type class Query: @strawberry.field def book(self) -> Book: return Book( title="The Hitchhiker's Guide to the Strawberry Fields", author=User(favourite_books=[], published_books=[]), ) @strawberry.field def user(self) -> User: return User(favourite_books=[], published_books=[]) def should_ignore(ignore: IgnoreContext): return ignore.field_name == "user" schema = strawberry.Schema( Query, extensions=[ QueryDepthLimiter(max_depth=2, should_ignore=should_ignore), ], ) # This query fails schema.execute( """ query TooDeep { book { author { publishedBooks { title } } } } """ ) # This query succeeds because the `user` field is ignored schema.execute( """ query NotTooDeep { user { favouriteBooks { author { publishedBooks { title } } } } } """ ) ``` ## Example with field_args: ```python import strawberry from strawberry.extensions import QueryDepthLimiter, IgnoreContext @strawberry.type class Book: title: str author: "User" @strawberry.type class User: name: str | None favourite_books: list[Book] published_books: list[Book] @strawberry.type class Query: @strawberry.field def book(self) -> Book: return Book( title="The Hitchhiker's Guide to the Strawberry Fields", author=User(favourite_books=[], published_books=[]), ) @strawberry.field def user(self, name: str | None = None) -> User: return User(name=name, favourite_books=[], published_books=[]) def should_ignore(ignore: IgnoreContext): return ignore.field_args.get("name") == "matt" schema = strawberry.Schema( Query, extensions=[ QueryDepthLimiter(max_depth=2, should_ignore=should_ignore), ], ) # This query fails schema.execute( """ query TooDeep { book { author { publishedBooks { title } } } } """ ) # This query succeeds because the `user` field is ignored schema.execute( """ query NotTooDeep { user(name:"matt") { favouriteBooks { author { publishedBooks { title } } } } } """ ) ``` strawberry-graphql-0.287.0/docs/extensions/sentry-tracing.md000066400000000000000000000010751511033167500241570ustar00rootroot00000000000000--- title: Sentry Tracing summary: Add Sentry tracing to your GraphQL server. tags: tracing --- # `SentryTracingExtension` As of Sentry 1.32.0, Strawberry is officially supported by the Sentry SDK. Therefore, Strawberry's `SentryTracingExtension` has been deprecated in version 0.210.0 and finally removed with Strawberry 0.249.0 in favor of the official Sentry SDK integration. For more details, please refer to the [documentation for the official Sentry Strawberry integration](https://docs.sentry.io/platforms/python/integrations/strawberry/). strawberry-graphql-0.287.0/docs/extensions/validation-cache.md000066400000000000000000000024101511033167500243730ustar00rootroot00000000000000--- title: Validation Cache summary: Add in memory caching to the validation step of query execution. tags: performance,caching,validation --- # `ValidationCache` This extension adds LRU caching to the validation step of query execution to improve performance by caching the validation errors in memory. ## Usage example: ```python import strawberry from strawberry.extensions import ValidationCache @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "Hello, world!" schema = strawberry.Schema( Query, extensions=[ ValidationCache(), ], ) ``` ## API reference: ```python class ValidationCache(maxsize=None): ... ``` #### `maxsize: Optional[int] = None` Set the maxsize of the cache. If `maxsize` is set to `None` then the cache will grow without bound. More info: https://docs.python.org/3/library/functools.html#functools.lru_cache ## More examples:
Using maxsize ```python import strawberry from strawberry.extensions import ValidationCache @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "Hello, world!" schema = strawberry.Schema( Query, extensions=[ ValidationCache(maxsize=100), ], ) ```
strawberry-graphql-0.287.0/docs/faq.md000066400000000000000000000040351511033167500175550ustar00rootroot00000000000000--- title: FAQ faq: true --- # Frequently Asked Questions ## How can I hide a field from GraphQL? Strawberry provides a `Private` type that can be used to hide fields from GraphQL, for example, the following code: ```python import strawberry @strawberry.type class User: name: str age: int password: strawberry.Private[str] @strawberry.type class Query: @strawberry.field def user(self) -> User: return User(name="Patrick", age=100, password="This is fake") schema = strawberry.Schema(query=Query) ``` will result in the following schema: ```graphql type Query { user: User! } type User { name: String! age: Int! } ``` ## How can I deal with circular imports? In cases where you have circular imports, you can use `strawberry.lazy` to resolve the circular imports, for example: ```python # posts.py from typing import TYPE_CHECKING, Annotated import strawberry if TYPE_CHECKING: from .users import User @strawberry.type class Post: title: str author: Annotated["User", strawberry.lazy(".users")] ``` For more information, see the [Lazy types](./types/lazy.md) documentation. ## Can I reuse Object Types with Input Objects? Unfortunately not because, as the [GraphQL spec](https://spec.graphql.org/June2018/#sec-Input-Objects) specifies, there is a difference between Objects Types and Inputs types: > The GraphQL Object type (ObjectTypeDefinition) defined above is inappropriate > for re‐use here, because Object types can contain fields that define arguments > or contain references to interfaces and unions, neither of which is > appropriate for use as an input argument. For this reason, input objects have > a separate type in the system. And this is also true for Input types' fields: you can only use Strawberry Input types or scalar. See our [Input Types](./types/input-types.md) docs. ## Can I use asyncio with Strawberry and Django? Yes, Strawberry provides an async view that can be used with Django, you can Check [Async Django](./integrations/django.md#async-django) for more information. strawberry-graphql-0.287.0/docs/federation/000077500000000000000000000000001511033167500206025ustar00rootroot00000000000000strawberry-graphql-0.287.0/docs/federation/custom_directives.md000066400000000000000000000016331511033167500246620ustar00rootroot00000000000000--- title: Exposing directives on the supergraph (Apollo Federation) --- # Exposing directives on the supergraph (Apollo Federation) By default (most) [schema directives are hidden from the supergraph schema](https://www.apollographql.com/docs/federation/federated-types/federated-directives/#composedirective). If you need to expose a directive to the supergraph, you can use the `compose` parameter on the `@strawberry.federation.schema_directives` decorator, here's an example: ```python import strawberry @strawberry.federation.schema_directive( locations=[Location.OBJECT], name="cacheControl", compose=True ) class CacheControl: max_age: int ``` This will create a `cacheControl` directive and it will also use [`@composeDirective`](https://www.apollographql.com/docs/federation/federated-types/federated-directives/#composedirective) on the schema to make sure it is included in the supergraph schema. strawberry-graphql-0.287.0/docs/federation/entities.md000066400000000000000000000047631511033167500227620ustar00rootroot00000000000000--- title: Entities (Apollo Federation) --- # Entities (Apollo Federation) In a federated graph, an [entity](https://www.apollographql.com/docs/federation/entities) is an object type that can resolve its fields across multiple subgraphs. Entities have a unique identifier, called a key, that is used to fetch them from a subgraph. In Strawberry entities are defined by annotating a type with the `@strawberry.federation.type` decorator, like in example below: ```python import strawberry @strawberry.federation.type(keys=["id"]) class Book: id: strawberry.ID title: str ``` You can also use the `Key` directive manually, like so: ```python import strawberry from strawberry.federation.schema_directives import Key @strawberry.type(directives=[Key(fields="id")]) class Book: id: strawberry.ID title: str ``` # Resolving references When a GraphQL operation references an entity across multiple services, the Apollo Router will fetch the entity from the subgraph that defines it. To do this, the subgraph needs to be able to resolve the entity by its key. This is done by defining a class method called `resolve_reference` on the entity type. For example, if we have a `Book` entity type, we can define a `resolve_reference` method like this: ```python import strawberry @strawberry.federation.type(keys=["id"]) class Book: id: strawberry.ID title: str @classmethod def resolve_reference(cls, id: strawberry.ID) -> "Book": # here we could fetch the book from the database # or even from an API return Book(id=id, title="My Book") ``` Strawberry provides a default implementation of `resolve_reference` that instantiates the object type using the data coming from the key. This means that you can omit the `resolve_reference` method if you don't need to fetch any additional data for your object type, like in the example below: ```python import strawberry @strawberry.federation.type(keys=["id"]) class Book: id: strawberry.ID reviews_count: int = strawberry.field(resolver=lambda: 3) ``` In the example above we are creating an entity called `Book` that has a `title` field and a `reviews_count` field. This entity will contribute to the `Book` entity in the supergraph and it will provide the `reviews_count` field. When the Apollo Router fetches the `Book` entity from this subgraph, it will call the `resolve_reference` method with the `id` of the book and, as mentioned above, Strawberry will instantiate the `Book` type using the data coming from the key. strawberry-graphql-0.287.0/docs/federation/entity-interfaces.md000066400000000000000000000044201511033167500245610ustar00rootroot00000000000000--- title: Entity interfaces (Apollo Federation) --- # Extending interfaces [Entity interfaces](https://www.apollographql.com/docs/federation/federated-types/interfaces) are similar to [entities](./entities.md), but usually can't contribute new fields to the supergraph (see below for how to use `@interfaceObject` to extend interfaces). Strawberry allows to define entity interfaces using the `@strawberry.federation.interface` decorator, here's an example: ```python import strawberry @strawberry.federation.interface(keys=["id"]) class Media: id: strawberry.ID ``` This will generate the following GraphQL type: ```graphql type Media @key(fields: "id") @interface { id: ID! } ``` # Extending Entity interfaces (Apollo Federation) In federation you can use `@interfaceObject` to extend interfaces from other services. This is useful when you want to add fields to an interface that is implemented by types in other services. Entity interfaces that extend other interfaces are defined by annotating an interface with the `@strawberry.federation.interface_object` decorator, like in example below: ```python import strawberry @strawberry.federation.interface_object(keys=["id"]) class Media: id: strawberry.ID title: str ``` This will generate the following GraphQL type: ```graphql type Media @key(fields: "id") @interfaceObject { id: ID! title: String! } ``` `@strawberry.federation.interface_object` is necessary because if we were to extend the `Media` interface using `@strawberry.federation.interface`, we'd need to also define all the types implementing the interface, which will make the schema hard to maintain (every updated to the interface and types implementing it would need to be reflected in all subgraphs declaring it). # Resolving references Entity interfaces are also used to resolve references to entities. The same rules as [entities](./entities.md) apply here. Here's a basic example: ```python import strawberry @strawberry.federation.interface_object(keys=["id"]) class Media: id: strawberry.ID title: str # TODO: check this @classmethod def resolve_reference(cls, id: strawberry.ID) -> "Media": # here we could fetch the media from the database # or even from an API return Media(id=id, title="My Media") ``` strawberry-graphql-0.287.0/docs/federation/introduction.md000066400000000000000000000017461511033167500236550ustar00rootroot00000000000000--- title: Apollo Federation --- # Apollo Federation Strawberry supports [Apollo Federation](https://www.apollographql.com/docs/federation/) out of the box, that means that you can create services using Strawberry and federate them via Apollo Gateway or Apollo Router. Strawberry is a schema first library, to use Apollo Federation you need to add directives to your schema, types and fields. Strawberry has built support for directives, but it also provide shortcuts for Apollo Federation. All shortcuts live under the `strawberry.federation` module. For example if you want to create an [Entity](https://www.apollographql.com/docs/federation/entities) you can do: ```python @strawberry.federation.type(keys=["id"]) class Book: id: strawberry.ID title: str ``` And strawberry will automatically add the right directives to the type and schema. # Getting started If you want to get started with Apollo Federation, you can use our [Apollo Federation guide](../guides/federation.md). strawberry-graphql-0.287.0/docs/general/000077500000000000000000000000001511033167500200775ustar00rootroot00000000000000strawberry-graphql-0.287.0/docs/general/multipart-subscriptions.md000066400000000000000000000012731511033167500253520ustar00rootroot00000000000000--- title: Multipart subscriptions --- # Multipart subscriptions Strawberry supports subscription over multipart responses. This is an [alternative protocol](https://www.apollographql.com/docs/router/executing-operations/subscription-multipart-protocol/) created by [Apollo](https://www.apollographql.com/) to support subscriptions over HTTP, and it is supported by default by Apollo Client. # Support We support multipart subscriptions out of the box in the following HTTP libraries: - Django (only in the Async view) - ASGI - Litestar - FastAPI - AioHTTP - Quart # Usage Multipart subscriptions are automatically enabled when using Subscription, so no additional configuration is required. strawberry-graphql-0.287.0/docs/general/mutations.md000066400000000000000000000137341511033167500224540ustar00rootroot00000000000000--- title: Mutations --- # Mutations As opposed to queries, mutations in GraphQL represent operations that modify server-side data and/or cause side effects on the server. For example, you can have a mutation that creates a new instance in your application or a mutation that sends an email. Like in queries, they accept parameters and can return anything a regular field can, including new types and existing object types. This can be useful for fetching the new state of an object after an update. Let's improve our books project from the [Getting started tutorial](../index.md) and implement a mutation that is supposed to add a book: ```python import strawberry # Reader, you can safely ignore Query in this example, it is required by # strawberry.Schema so it is included here for completeness @strawberry.type class Query: @strawberry.field def hello() -> str: return "world" @strawberry.type class Mutation: @strawberry.mutation def add_book(self, title: str, author: str) -> Book: print(f"Adding {title} by {author}") return Book(title=title, author=author) schema = strawberry.Schema(query=Query, mutation=Mutation) ``` Like queries, mutations are defined in a class that is then passed to the Schema function. Here we create an `addBook` mutation that accepts a title and an author and returns a `Book` type. We would send the following GraphQL document to our server to execute the mutation: ```graphql mutation { addBook(title: "The Little Prince", author: "Antoine de Saint-Exupéry") { title } } ``` The `addBook` mutation is a simplified example. In a real-world application mutations will often need to handle errors and communicate those errors back to the client. For example we might want to return an error if the book already exists. You can checkout our documentation on [dealing with errors](/docs/guides/errors#expected-errors) to learn how to return a union of types from a mutation. ## Mutations without returned data It is also possible to write a mutation that doesn't return anything. This is mapped to a `Void` GraphQL scalar, and always returns `null` ```python @strawberry.type class Mutation: @strawberry.mutation def restart() -> None: print(f"Restarting the server") ``` ```graphql type Mutation { restart: Void } ``` Mutations with void-result go against [this community-created guide on GQL best practices](https://graphql-rules.com/rules/mutation-payload). ## The input mutation extension It is usually useful to use a pattern of defining a mutation that receives a single [input type](../types/input-types) argument called `input`. Strawberry provides the [`InputMutationExtension`](../extensions/input-mutation.md), a [field extension](../guides/field-extensions.md) that automatically creates an input type for you, whose attributes are the same as the arguments in the resolver. For example, suppose we want the mutation defined in the section above to be an input mutation. We can add the `InputMutationExtension` to the field like this: ```python from strawberry.field_extensions import InputMutationExtension @strawberry.type class Mutation: @strawberry.mutation(extensions=[InputMutationExtension()]) def update_fruit_weight( self, info: strawberry.Info, id: strawberry.ID, weight: Annotated[ float, strawberry.argument(description="The fruit's new weight in grams"), ], ) -> Fruit: fruit = ... # retrieve the fruit with the given ID fruit.weight = weight ... # maybe save the fruit in the database return fruit ``` That would generate a schema like this: ```graphql input UpdateFruitWeightInput { id: ID! """ The fruit's new weight in grams """ weight: Float! } type Mutation { updateFruitWeight(input: UpdateFruitWeightInput!): Fruit! } ``` ## Nested mutations To avoid a graph becoming too large and to improve discoverability, it can be helpful to group mutations in a namespace, as described by [Apollo's guide on Namespacing by separation of concerns](https://www.apollographql.com/docs/technotes/TN0012-namespacing-by-separation-of-concern/). ```graphql type Mutation { fruit: FruitMutations! } type FruitMutations { add(input: AddFruitInput): Fruit! updateWeight(input: UpdateFruitWeightInput!): Fruit! } ``` Since all GraphQL operations are fields, we can define a `FruitMutation` type and add mutation fields to it like we could add mutation fields to the root `Mutation` type. ```python import strawberry @strawberry.type class FruitMutations: @strawberry.mutation def add(self, info, input: AddFruitInput) -> Fruit: ... @strawberry.mutation def update_weight(self, info, input: UpdateFruitWeightInput) -> Fruit: ... @strawberry.type class Mutation: @strawberry.field def fruit(self) -> FruitMutations: return FruitMutations() ``` Fields on the root `Mutation` type are resolved serially. Namespace types introduce the potential for mutations to be resolved asynchronously and in parallel because the mutation fields that mutate data are no longer at the root level. To guarantee serial execution when namespace types are used, clients should use aliases to select the root mutation field for each mutation. In the following example, once `addFruit` execution is complete, `updateFruitWeight` begins. ```graphql mutation ( $addFruitInput: AddFruitInput! $updateFruitWeightInput: UpdateFruitWeightInput! ) { addFruit: fruit { add(input: $addFruitInput) { id } } updateFruitWeight: fruit { updateWeight(input: $updateFruitWeightInput) { id } } } ``` For more details, see [Apollo's guide on Namespaces for serial mutations](https://www.apollographql.com/docs/technotes/TN0012-namespacing-by-separation-of-concern/#namespaces-for-serial-mutations) and [Rapid API's Interactive Guide to GraphQL Queries: Aliases and Variables](https://rapidapi.com/guides/graphql-aliases-variables). strawberry-graphql-0.287.0/docs/general/queries.md000066400000000000000000000046751511033167500221120ustar00rootroot00000000000000--- title: Queries --- # Queries In GraphQL you use queries to fetch data from a server. In Strawberry you can define the data your server provides by defining query types. By default all the fields the API exposes are nested under a root Query type. This is how you define a root query type in Strawberry: ```python @strawberry.type class Query: name: str schema = strawberry.Schema(query=Query) ``` This creates a schema where the root type Query has one single field called name. As you notice we don't provide a way to fetch data. In order to do so we need to provide a `resolver`, a function that knows how to fetch data for a specific field. For example in this case we could have a function that always returns the same name: ```python def get_name() -> str: return "Strawberry" @strawberry.type class Query: name: str = strawberry.field(resolver=get_name) schema = strawberry.Schema(query=Query) ``` So now, when requesting the name field, the `get_name` function will be called. Alternatively a field can be declared using a decorator: ```python @strawberry.type class Query: @strawberry.field def name(self) -> str: return "Strawberry" ``` The decorator syntax supports specifying a `graphql_type` for cases when the return type of the function does not match the GraphQL type: ```python class User: id: str name: str def __init__(self, id: str, name: str): self.id = id self.name = name @strawberry.type(name="User") class UserType: id: strawberry.ID name: str @strawberry.type class Query: @strawberry.field(graphql_type=UserType) def user(self) -> User return User(id="ringo", name="Ringo") ``` ## Arguments GraphQL fields can accept arguments, usually to filter out or retrieve specific objects: ```python FRUITS = [ "Strawberry", "Apple", "Orange", ] @strawberry.type class Query: @strawberry.field def fruit(self, startswith: str) -> str | None: for fruit in FRUITS: if fruit.startswith(startswith): return fruit return None ``` ### Argument Descriptions Use `Annotated` to give a field argument a description: ```python from typing import Annotated import strawberry @strawberry.type class Query: @strawberry.field def fruit( self, startswith: Annotated[ str, strawberry.argument(description="Prefix to filter fruits by.") ], ) -> str | None: ... ``` strawberry-graphql-0.287.0/docs/general/schema-basics.md000066400000000000000000000246571511033167500231410ustar00rootroot00000000000000--- title: Schema basics --- # Schema basics GraphQL servers use a **schema** to describe the shape of the data. The schema defines a hierarchy of **types** with fields that are populated from data stores. The schema also specifies exactly which queries and mutations are available for clients to execute. This guide describes the basic building blocks of a schema and how to use Strawberry to create one. ## Schema definition language (SDL) There are two approaches for creating the schema for a GraphQL server. One is called “schema-first” and the other is called “code-first”. Strawberry _only_ supports code-first schemas. Before diving into code-first, let’s first explain what the Schema definition language is. Schema first works using the Schema Definition Language of GraphQL, which is included in the GraphQL spec. Here’s an example of schema defined using the SDL: ```graphql type Book { title: String! author: Author! } type Author { name: String! books: [Book!]! } ``` The schema defines all the types and relationships between them. With this we enable client developers to see exactly what data is available and request a specific subset of that data. The `!` sign specifies that a field is non-nullable. Notice that the schema doesn’t specify how to get the data. That comes later when defining the resolvers. ## Code first approach As mentioned Strawberry uses a code first approach. The previous schema would look like this in Strawberry ```python import typing import strawberry @strawberry.type class Book: title: str author: "Author" @strawberry.type class Author: name: str books: typing.List[Book] ``` As you can see the code maps almost one to one with the schema, thanks to python’s type hints feature. Notice that here we are also not specifying how to fetch data, that will be explained in the resolvers section. ## Supported types GraphQL supports a few different types: - Scalar types - Object types - The Query type - The Mutation type - Input types ## Scalar types Scalar types are similar to Python primitive types. Here’s the list of the default scalar types in GraphQL: - Int, a signed 32-bit integer, maps to python’s int - Float, a signed double-precision floating-point value, maps to python’s float - String, maps to python’s str - Boolean, true or false, maps to python’s bool - ID, a unique identifier that usually used to refetch an object or as the key for a cache. Serialized as string and available as `strawberry.ID(“value”)` - `UUID`, a [UUID](https://docs.python.org/3/library/uuid.html#uuid.UUID) value serialized as a string Strawberry also includes support for date, time and datetime objects, they are not officially included with the GraphQL spec, but they are usually needed in most servers. They are serialized as ISO-8601. These primitives work for the majority of use cases, but you can also specify your [own scalar types](/docs/types/scalars#custom-scalars). ## Object types Most of the types you define in a GraphQL schema are object types. An object type contains a collection of fields, each of which can be either a scalar type or another object type. Object types can refer to each other, as we had in our schema earlier: ```python import typing import strawberry @strawberry.type class Book: title: str author: "Author" @strawberry.type class Author: name: str books: typing.List[Book] ``` ## Providing data to fields In the above schema, a `Book` has an `author` field and an `Author` has a `books` field, yet we do not know how our data can be mapped to fulfil the structure of the promised schema. To achieve this, we introduce the concept of the [_resolver_](../types/resolvers.md) that provides some data to a field through a function. Continuing with this example of books and authors, resolvers can be defined to provide values to the fields: ```python def get_author_for_book(root) -> "Author": return Author(name="Michael Crichton") @strawberry.type class Book: title: str author: "Author" = strawberry.field(resolver=get_author_for_book) def get_books_for_author(root) -> typing.List[Book]: return [Book(title="Jurassic Park")] @strawberry.type class Author: name: str books: typing.List[Book] = strawberry.field(resolver=get_books_for_author) def get_authors(root) -> typing.List[Author]: return [Author(name="Michael Crichton")] @strawberry.type class Query: authors: typing.List[Author] = strawberry.field(resolver=get_authors) books: typing.List[Book] = strawberry.field(resolver=get_books_for_author) ``` These functions provide the `strawberry.field` with the ability to render data to the GraphQL query upon request and are the backbone of all GraphQL APIs. This example is trivial since the resolved data is entirely static. However, when building more complex APIs, these resolvers can be written to map data from databases, e.g. making SQL queries using SQLAlchemy, and other APIs, e.g. making HTTP requests using aiohttp. For more information and detail on the different ways to write resolvers, see the [resolvers section](../types/resolvers.md). ## The Query type The `Query` type defines exactly which GraphQL queries (i.e., read operations) clients can execute against your data. It resembles an object type, but its name is always `Query`. Each field of the `Query` type defines the name and return type of a different supported query. The `Query` type for our example schema might resemble the following: ```python @strawberry.type class Query: books: typing.List[Book] authors: typing.List[Author] ``` This Query type defines two available queries: books and authors. Each query returns a list of the corresponding type. With a REST-based API, books and authors would probably be returned by different endpoints (e.g., /api/books and /api/authors). The flexibility of GraphQL enables clients to query both resources with a single request. ### Structuring a query When your clients build queries to execute against your data graph, those queries match the shape of the object types you define in your schema. Based on our example schema so far, a client could execute the following query, which requests both a list of all book titles and a list of all author names: ```graphql query { books { title } authors { name } } ``` Our server would then respond to the query with results that match the query's structure, like so: ```json { "data": { "books": [{ "title": "Jurassic Park" }], "authors": [{ "name": "Michael Crichton" }] } } ``` Although it might be useful in some cases to fetch these two separate lists, a client would probably prefer to fetch a single list of books, where each book's author is included in the result. Because our schema's Book type has an author field of type Author, a client could structure their query like so: ```graphql query { books { title author { name } } } ``` And once again, our server would respond with results that match the query's structure: ```json { "data": { "books": [ { "title": "Jurassic Park", "author": { "name": "Michael Crichton" } } ] } } ``` ## The Mutation type The `Mutation` type is similar in structure and purpose to the Query type. Whereas the Query type defines your data's supported read operations, the `Mutation` type defines supported write operations. Each field of the `Mutation` type defines the signature and return type of a different mutation. The `Mutation` type for our example schema might resemble the following: ```python @strawberry.type class Mutation: @strawberry.mutation def add_book(self, title: str, author: str) -> Book: ... ``` This Mutation type defines a single available mutation, `addBook`. The mutation accepts two arguments (title and author) and returns a newly created Book object. As you'd expect, this Book object conforms to the structure that we defined in our schema. Strawberry converts fields names from snake case to camel case by default. This can be changed by specifying a [custom `StrawberryConfig` on the schema](../types/schema-configurations.md) ### Structuring a mutation Like queries, mutations match the structure of your schema's type definitions. The following mutation creates a new Book and requests certain fields of the created object as a return value: ```graphql mutation { addBook(title: "Fox in Socks", author: "Dr. Seuss") { title author { name } } } ``` As with queries, our server would respond to this mutation with a result that matches the mutation's structure, like so: ```json { "data": { "addBook": { "title": "Fox in Socks", "author": { "name": "Dr. Seuss" } } } } ``` ## Input types Input types are special object types that allow you to pass objects as arguments to queries and mutations (as opposed to passing only scalar types). Input types help keep operation signatures clean. Consider our previous mutation to add a book: ```python @strawberry.type class Mutation: @strawberry.mutation def add_book(self, title: str, author: str) -> Book: ... ``` Instead of accepting two arguments, this mutation could accept a single input type that includes all of these fields. This comes in extra handy if we decide to accept an additional argument in the future, such as a publication date. An input type's definition is similar to an object type's, but it uses the input keyword: ```python @strawberry.input class AddBookInput: title: str author: str @strawberry.type class Mutation: @strawberry.mutation def add_book(self, book: AddBookInput) -> Book: ... ``` Not only does this facilitate passing the AddBookInput type around within our schema, it also provides a basis for annotating fields with descriptions that are automatically exposed by GraphQL-enabled tools: ```python @strawberry.input class AddBookInput: title: str = strawberry.field(description="The title of the book") author: str = strawberry.field(description="The name of the author") ``` Input types can sometimes be useful when multiple operations require the exact same set of information, but you should reuse them sparingly. Operations might eventually diverge in their sets of required arguments. ## More If you want to learn more about schema design make sure you follow the [documentation provided by Apollo](https://www.apollographql.com/docs/apollo-server/schema/schema/#growing-with-a-schema). strawberry-graphql-0.287.0/docs/general/subscriptions.md000066400000000000000000000316671511033167500233450ustar00rootroot00000000000000--- title: Subscriptions --- # Subscriptions In GraphQL you can use subscriptions to stream data from a server. To enable this with Strawberry your server must support ASGI and websockets or use the AIOHTTP integration. This is how you define a subscription-capable resolver: ```python import asyncio from typing import AsyncGenerator import strawberry @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "world" @strawberry.type class Subscription: @strawberry.subscription async def count(self, target: int = 100) -> AsyncGenerator[int, None]: for i in range(target): yield i await asyncio.sleep(0.5) schema = strawberry.Schema(query=Query, subscription=Subscription) ``` Like queries and mutations, subscriptions are defined in a class and passed to the Schema function. Here we create a rudimentary counting function which counts from 0 to the target sleeping between each loop iteration. The return type of `count` is `AsyncGenerator` where the first generic argument is the actual type of the response, in most cases the second argument should be left as `None` (more about Generator typing [here](https://docs.python.org/3/library/typing.html#typing.AsyncGenerator)). We would send the following GraphQL document to our server to subscribe to this data stream: ```graphql subscription { count(target: 5) } ``` In this example, the data looks like this as it passes over the websocket: ![A view of the data that's been passed via websocket](../images/subscriptions-count-websocket.png) This is a very short example of what is possible. Like with queries and mutations the subscription can return any GraphQL type, not only scalars as demonstrated here. ## Authenticating Subscriptions Without going into detail on [why](https://github.com/websockets/ws/issues/467), custom headers cannot be set on websocket requests that originate in browsers. Therefore, when making any GraphQL requests that rely on a websocket connection, header-based authentication is impossible. Other popular GraphQL solutions, like Apollo for example, implement functionality to pass information from the client to the server at the point of websocket connection initialisation. In this way, information that is relevant to the websocket connection initialisation and to the lifetime of the connection overall can be passed to the server before any data is streamed back by the server. As such, it is not limited to only authentication credentials! Strawberry's implementation follows that of Apollo's, which as documentation for [client](https://www.apollographql.com/docs/react/data/subscriptions/#5-authenticate-over-websocket-optional) and [server](https://www.apollographql.com/docs/apollo-server/data/subscriptions/#operation-context) implementations, by reading the contents of the initial websocket connection message into the `info.context` object. With Apollo-client as an example of how to send this initial connection information, one defines a `ws-link` as: ```javascript import { GraphQLWsLink } from "@apollo/client/link/subscriptions"; import { createClient } from "graphql-ws"; const wsLink = new GraphQLWsLink( createClient({ url: "ws://localhost:4000/subscriptions", connectionParams: { authToken: "Bearer I_AM_A_VALID_AUTH_TOKEN", }, }), ); ``` and then, upon the establishment of the Susbcription request and underlying websocket connection, Strawberry injects this `connectionParams` object as follows: ```python import asyncio from typing import AsyncGenerator import strawberry from .auth import authenticate_token @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "world" @strawberry.type class Subscription: @strawberry.subscription async def count( self, info: strawberry.Info, target: int = 100 ) -> AsyncGenerator[int, None]: connection_params: dict = info.context.get("connection_params") token: str = connection_params.get( "authToken" ) # equal to "Bearer I_AM_A_VALID_AUTH_TOKEN" if not authenticate_token(token): raise Exception("Forbidden!") for i in range(target): yield i await asyncio.sleep(0.5) schema = strawberry.Schema(query=Query, subscription=Subscription) ``` Strawberry expects the `connection_params` object to be any type, so the client is free to send any valid JSON object as the initial message of the websocket connection, which is abstracted as `connectionParams` in Apollo-client, and it will be successfully injected into the `info.context` object. It is then up to you to handle it correctly! ## Advanced Subscription Patterns Typically a GraphQL subscription is streaming something more interesting back. With that in mind your subscription function can return one of: - `AsyncIterator`, or - `AsyncGenerator` Both of these types are documented in [PEP-525][pep-525]. Anything yielded from these types of resolvers will be shipped across the websocket. Care needs to be taken to ensure the returned values conform to the GraphQL schema. The benefit of an AsyncGenerator, over an iterator, is that the complex business logic can be broken out into a separate module within your codebase. Allowing you to keep the resolver logic succinct. The following example is similar to the one above, except it returns an AsyncGenerator to the ASGI server which is responsible for streaming subscription results until the Generator exits. ```python import strawberry import asyncio import asyncio.subprocess as subprocess from asyncio import streams from typing import Any, AsyncGenerator, AsyncIterator, Coroutine, Optional async def wait_for_call(coro: Coroutine[Any, Any, bytes]) -> Optional[bytes]: """ wait_for_call calls the supplied coroutine in a wait_for block. This mitigates cases where the coroutine doesn't yield until it has completed its task. In this case, reading a line from a StreamReader; if there are no `\n` line chars in the stream the function will never exit """ try: return await asyncio.wait_for(coro(), timeout=0.1) except asyncio.TimeoutError: pass async def lines(stream: streams.StreamReader) -> AsyncIterator[str]: """ lines reads all lines from the provided stream, decoding them as UTF-8 strings. """ while True: b = await wait_for_call(stream.readline) if b: yield b.decode("UTF-8").rstrip() else: break async def exec_proc(target: int) -> subprocess.Process: """ exec_proc starts a sub process and returns the handle to it. """ return await asyncio.create_subprocess_exec( "/bin/bash", "-c", f"for ((i = 0 ; i < {target} ; i++)); do echo $i; sleep 0.2; done", stdout=subprocess.PIPE, ) async def tail(proc: subprocess.Process) -> AsyncGenerator[str, None]: """ tail reads from stdout until the process finishes """ # Note: race conditions are possible here since we're in a subprocess. In # this case the process can finish between the loop predicate and the call # to read a line from stdout. This is a good example of why you need to # be defensive by using asyncio.wait_for in wait_for_call(). while proc.returncode is None: async for l in lines(proc.stdout): yield l else: # read anything left on the pipe after the process has finished async for l in lines(proc.stdout): yield l @strawberry.type class Query: @strawberry.field def hello() -> str: return "world" @strawberry.type class Subscription: @strawberry.subscription async def run_command(self, target: int = 100) -> AsyncGenerator[str, None]: proc = await exec_proc(target) return tail(proc) schema = strawberry.Schema(query=Query, subscription=Subscription) ``` [pep-525]: https://www.python.org/dev/peps/pep-0525/ ## Unsubscribing subscriptions In GraphQL, it is possible to unsubscribe from a subscription. Strawberry supports this behaviour, and is done using a `try...except` block. In Apollo-client, closing a subscription can be achieved like the following: ```javascript const client = useApolloClient(); const subscriber = client.subscribe({query: ...}).subscribe({...}) // ... // done with subscription. now unsubscribe subscriber.unsubscribe(); ``` Strawberry can capture when a subscriber unsubscribes using an `asyncio.CancelledError` exception. ```python import asyncio from typing import AsyncGenerator from uuid import uuid4 import strawberry # track active subscribers event_messages = {} @strawberry.type class Subscription: @strawberry.subscription async def message(self) -> AsyncGenerator[int, None]: try: subscription_id = uuid4() event_messages[subscription_id] = [] while True: if len(event_messages[subscription_id]) > 0: yield event_messages[subscription_id] event_messages[subscription_id].clear() await asyncio.sleep(1) except asyncio.CancelledError: # stop listening to events del event_messages[subscription_id] ``` ## GraphQL over WebSocket protocols Strawberry support both the legacy [graphql-ws](https://github.com/apollographql/subscriptions-transport-ws) and the newer recommended [graphql-transport-ws](https://github.com/enisdenjo/graphql-ws) WebSocket sub-protocols. The `graphql-transport-ws` protocols repository is called `graphql-ws`. However, `graphql-ws` is also the name of the legacy protocol. This documentation always refers to the protocol names. Note that the `graphql-ws` sub-protocol is mainly supported for backwards compatibility. Read the [graphql-ws-transport protocols announcement](https://the-guild.dev/blog/graphql-over-websockets) to learn more about why the newer protocol is preferred. Strawberry allows you to choose which protocols you want to accept. All integrations supporting subscriptions can be configured with a list of `subscription_protocols` to accept. By default, all protocols are accepted. ### AIOHTTP ```python from strawberry.aiohttp.views import GraphQLView from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL from api.schema import schema view = GraphQLView( schema, subscription_protocols=[GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL] ) ``` ### ASGI ```python from strawberry.asgi import GraphQL from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL from api.schema import schema app = GraphQL( schema, subscription_protocols=[ GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL, ], ) ``` ### Django + Channels ```python import os from django.core.asgi import get_asgi_application from strawberry.channels import GraphQLProtocolTypeRouter os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") django_asgi_app = get_asgi_application() # Import your Strawberry schema after creating the django ASGI application # This ensures django.setup() has been called before any ORM models are imported # for the schema. from mysite.graphql import schema application = GraphQLProtocolTypeRouter( schema, django_application=django_asgi_app, ) ``` Note: Check the [channels integraton](../integrations/channels.md) page for more information regarding it. ### FastAPI ```python from strawberry.fastapi import GraphQLRouter from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL from fastapi import FastAPI from api.schema import schema graphql_router = GraphQLRouter( schema, subscription_protocols=[ GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL, ], ) app = FastAPI() app.include_router(graphql_router, prefix="/graphql") ``` ### Quart ```python from strawberry.quart.views import GraphQLView from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL from quart import Quart from api.schema import schema view = GraphQLView.as_view( "graphql_view", schema=schema, subscription_protocols=[ GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL, ], ) app = Quart(__name__) app.add_url_rule( "/graphql", view_func=view, methods=["GET"], websocket=True, ) ``` ## Single result operations In addition to _streaming operations_ (i.e. subscriptions), the `graphql-transport-ws` protocol supports so called _single result operations_ (i.e. queries and mutations). This enables clients to use one protocol and one connection for queries, mutations and subscriptions. Take a look at the [protocol's repository](https://github.com/enisdenjo/graphql-ws) to learn how to correctly set up the graphql client of your choice. Strawberry supports single result operations out of the box when the `graphql-transport-ws` protocol is enabled. Single result operations are normal queries and mutations, so there is no need to adjust any resolvers. strawberry-graphql-0.287.0/docs/general/upgrades.md000066400000000000000000000014471511033167500222410ustar00rootroot00000000000000--- title: Upgrading Strawberry --- # Upgrading Strawberry We try to keep Strawberry as backwards compatible as possible, but sometimes we need to make updates to the public API. While we try to deprecate APIs before removing them, we also want to make it as easy as possible to upgrade to the latest version of Strawberry. For this reason, we provide a CLI command to run Codemods that can automatically upgrade your codebase to use the updated APIs. Keep an eye on our release notes and the [breaking changes](../breaking-changes.md) page to see if a new Codemod is available, or if manual changes are required. Here's an example of how to upgrade your codebase by running a Codemod using the Strawberry CLI's `upgrade` command: ```shell strawberry upgrade annotated-union . ``` strawberry-graphql-0.287.0/docs/guides/000077500000000000000000000000001511033167500177425ustar00rootroot00000000000000strawberry-graphql-0.287.0/docs/guides/accessing-parent-data.md000066400000000000000000000130131511033167500244170ustar00rootroot00000000000000--- title: Accessing parent's data in resolvers --- # Accessing parent's data in resolvers It is quite common to want to be able to access the data from the field's parent in a resolver. For example let's say that we want to define a `fullName` field on our `User`. This would be our code: ```python import strawberry @strawberry.type class User: first_name: str last_name: str full_name: str ``` ```graphql type User { firstName: String! lastName: String! fullName: String! } ``` In this case `full_name` will need to access the `first_name` and `last_name` fields, and depending on whether we define the resolver as a function or as a method, we'll have a few options! Let's start with the defining a resolver as a function. ## Accessing parent's data in function resolvers ```python import strawberry def get_full_name() -> str: ... @strawberry.type class User: first_name: str last_name: str full_name: str = strawberry.field(resolver=get_full_name) ``` Our resolver is a function with no arguments, in order to tell Strawberry to pass us the parent of the field, we need to add a new argument with type `strawberry.Parent[ParentType]`, like so: ```python def get_full_name(parent: strawberry.Parent[User]) -> str: return f"{parent.first_name} {parent.last_name}" ``` `strawberry.Parent` tells Strawberry to pass the parent value of the field, in this case it would be the `User`. > **Note:** `strawberry.Parent` accepts a type argument, which will then be used > by your type checker to check your code! ### Using root Historically Strawberry only supported passing the parent value by adding a parameter called `root`: ```python def get_full_name(root: User) -> str: return f"{root.first_name} {root.last_name}" ``` This is still supported, but we recommend using `strawberry.Parent`, since it follows Strawberry's philosophy of using type annotations. Also, with `strawberry.Parent` your argument can have any name, for example this will still work: ```python def get_full_name(user: strawberry.Parent[User]) -> str: return f"{user.first_name} {user.last_name}" ``` ## Accessing parent's data in a method resolver Both options also work when defining a method resolver, so we can still use `strawberry.Parent` in a resolver defined as a method: ```python import strawberry @strawberry.type class User: first_name: str last_name: str @strawberry.field def full_name(self, parent: strawberry.Parent[User]) -> str: return f"{parent.first_name} {parent.last_name}" ``` But, here's where things get more interesting. If this was a pure Python class, we would use `self` directly, right? Turns out that Strawberry also supports this! Let's update our resolver: ```python import strawberry @strawberry.type class User: first_name: str last_name: str @strawberry.field def full_name(self) -> str: return f"{self.first_name} {self.last_name}" ``` Much better, no? `self` on resolver methods is pretty convenient, and it works like it should in Python, but there might be cases where it doesn't properly follow Python's semantics. This is because under the hood resolvers are actually called as if they were static methods by Strawberry. Let's see a simplified version of what happens when you request the `full_name` field, to do that we also need a field that allows to fetch a user: ```python import strawberry @strawberry.type class Query: @strawberry.field def user(self) -> User: return User(first_name="Albert", last_name="Heijn") ``` When we do a query like this: ```graphql { user { fullName } } ``` We are pretty much asking to call the `user` function on the `Query` class, and then call the `full_name` function on the `User` class, similar to this code: ```python user = Query().user() full_name = user.full_name() ``` While this might work for this case, it won't work in other cases, like when returning a different type, for example when fetching the user from a database: ```python import strawberry @strawberry.type class Query: @strawberry.field def user(self) -> User: # let's assume UserModel fetches data from the db and it # also has `first_name` and `last_name` user = UserModel.objects.first() return user ``` In this case our pseudo code would break, since `UserModel` doesn't have a `full_name` function! But it does work when using Strawberry (provided that the `UserModel` has both `first_name` and `last_name` fields). As mentioned, this is because Strawberry class the resolvers as if they were plain functions (not bound to the class), similar to this: ```python # note, we are not instantiating the Query any more! user = Query.user() # note: this is a `UserModel` now full_name = User.full_name(user) ``` You're probably thinking of `staticmethod`s and that's pretty much what we are dealing with now! If you want to keep the resolver as a method on your class but also want to remove some of the magic around `self`, you can use the `@staticmethod` decorator in combination with `strawberry.Parent`: ```python import strawberry @strawberry.type class User: first_name: str last_name: str @strawberry.field @staticmethod def full_name(parent: strawberry.Parent[User]) -> str: return f"{parent.first_name} {parent.last_name}" ``` Combining `@staticmethod` with `strawberry.Parent` is a good way to make sure that your code is clear and that you are aware of what's happening under the hood, and it will keep your linters and type checkers happy! strawberry-graphql-0.287.0/docs/guides/authentication.md000066400000000000000000000052631511033167500233110ustar00rootroot00000000000000--- title: Authentication --- # Authentication Authentication is the process of verifying that a user is who they claim to be and should be handled by the framework you are using. Some already have a built-in authentication system (like Django); others, you have to provide it manually. It's not Strawberry's responsibility to authenticate the user, but it can be used to create a mutation that handles the authentication's process. It's also very important not to confuse authentication with authorization: authorization determines what an authenticated user can do or which data they can access. In Strawberry, this is managed with [`Permissions` classes](./permissions.md). Let's see how to put together these concepts with an example. First, we define a `login` mutation where we authenticate credentials and return `LoginSucces` or `LoginError` types depending on whether the user was successfully authenticated or not. ```python import strawberry from .types import User from typing import Annotated, Union @strawberry.type class LoginSuccess: user: User @strawberry.type class LoginError: message: str LoginResult = Annotated[ Union[LoginSuccess, LoginError], strawberry.union("LoginResult") ] @strawberry.type class Mutation: @strawberry.field def login(self, username: str, password: str) -> LoginResult: # Your domain-specific authentication logic would go here user = ... if user is None: return LoginError(message="Something went wrong") return LoginSuccess(user=User(username=username)) ``` ### Access authenticated user in resolver Its fairly common to require user information within a resolver. We can do that in a type safe way with a custom context dataclass. For example, in FastAPI this might look like this: ```python from functools import cached_property import strawberry from fastapi import FastAPI from strawberry.fastapi import BaseContext, GraphQLRouter @strawberry.type class User: ... # This is just a stub for an actual user object class Context(BaseContext): @cached_property def user(self) -> User | None: if not self.request: return None authorization = self.request.headers.get("Authorization", None) return authorization_service.authorize(authorization) @strawberry.type class Query: @strawberry.field def get_authenticated_user(self, info: strawberry.Info[Context]) -> User | None: return info.context.user async def get_context() -> Context: return Context() schema = strawberry.Schema(Query) graphql_app = GraphQLRouter( schema, context_getter=get_context, ) app = FastAPI() app.include_router(graphql_app, prefix="/graphql") ``` strawberry-graphql-0.287.0/docs/guides/convert-to-dictionary.md000066400000000000000000000005621511033167500245320ustar00rootroot00000000000000--- title: Convert to Dictionary --- # Convert to Dictionary Strawberry provides a utility function to convert a Strawberry object to a dictionary. You can use `strawberry.asdict(...)` function: ```python @strawberry.type class User: name: str age: int # should be {"name": "Lorem", "age": 25} user_dict = strawberry.asdict(User(name="Lorem", age=25)) ``` strawberry-graphql-0.287.0/docs/guides/custom-extensions.md000066400000000000000000000161031511033167500237740ustar00rootroot00000000000000--- title: Schema extensions --- # Schema extensions Strawberry provides support for adding extensions to your schema. Schema extensions can be used to hook into different parts of the GraphQL execution and to provide additional results to the GraphQL response. To create a custom extension you can extend from our `SchemaExtension` base class: ```python import strawberry from strawberry.extensions import SchemaExtension class MyExtension(SchemaExtension): def get_results(self): return {"example": "this is an example for an extension"} schema = strawberry.Schema(query=Query, extensions=[MyExtension]) ``` ## Hooks ### Resolve `resolve` can be used to run code before and after the execution of **all** resolvers. When calling the underlying resolver using `_next`, all of the arguments to resolve need to be passed to `_next`, as they will be needed by the resolvers. If you need to wrap only certain field resolvers with additional logic, please check out [field extensions](field-extensions.md). Note that `resolve` can also be implemented asynchronously. ```python from strawberry.extensions import SchemaExtension class MyExtension(SchemaExtension): def resolve(self, _next, root, info: strawberry.Info, *args, **kwargs): return _next(root, info, *args, **kwargs) ``` ### Get results `get_results` allows to return a dictionary of data or alternatively an awaitable resolving to a dictionary of data that will be included in the GraphQL response. ```python from typing import Any, Dict from strawberry.extensions import SchemaExtension class MyExtension(SchemaExtension): def get_results(self) -> Dict[str, Any]: return {} ``` ### Lifecycle hooks Lifecycle hooks runs before graphql operation occur and after it is done. Lifecycle hooks uses generator syntax. In example: `on_operation` hook can be used to run code when a GraphQL operation starts and ends. ```python from strawberry.extensions import SchemaExtension class MyExtension(SchemaExtension): def on_operation(self): print("GraphQL operation start") yield print("GraphQL operation end") ```
Extend error response format ```python class ExtendErrorFormat(SchemaExtension): def on_operation(self): yield result = self.execution_context.result if getattr(result, "errors", None): result.errors = [ StrawberryGraphQLError( extensions={"additional_key": "additional_value"}, nodes=error.nodes, source=error.source, positions=error.positions, path=error.path, original_error=error.original_error, message=error.message, ) for error in result.errors ] @strawberry.type class Query: @strawberry.field def ping(self) -> str: raise Exception("This error occurred while querying the ping field") schema = strawberry.Schema(query=Query, extensions=[ExtendErrorFormat]) ```
#### Supported lifecycle hooks: - Validation `on_validate` can be used to run code on the validation step of the GraphQL execution. ```python from strawberry.extensions import SchemaExtension class MyExtension(SchemaExtension): def on_validate(self): print("GraphQL validation start") yield print("GraphQL validation end") ``` - Parse `on_parse` can be used to run code on the parsing step of the GraphQL execution. ```python from strawberry.extensions import SchemaExtension class MyExtension(SchemaExtension): def on_parse(self): print("GraphQL parsing start") yield print("GraphQL parsing end") ``` - Execution `on_execute` can be used to run code on the execution step of the GraphQL execution. ```python from strawberry.extensions import SchemaExtension class MyExtension(SchemaExtension): def on_execute(self): print("GraphQL execution start") yield print("GraphQL execution end") ``` #### Examples:
In memory cached execution ```python import json import strawberry from strawberry.extensions import SchemaExtension # Use an actual cache in production so that this doesn't grow unbounded response_cache = {} class ExecutionCache(SchemaExtension): def on_execute(self): # Check if we've come across this query before execution_context = self.execution_context self.cache_key = ( f"{execution_context.query}:{json.dumps(execution_context.variables)}" ) if self.cache_key in response_cache: self.execution_context.result = response_cache[self.cache_key] yield execution_context = self.execution_context if self.cache_key not in response_cache: response_cache[self.cache_key] = execution_context.result schema = strawberry.Schema( Query, extensions=[ ExecutionCache, ], ) ```
Rejecting an operation before executing it ```python import strawberry from strawberry.extensions import SchemaExtension class RejectSomeQueries(SchemaExtension): def on_execute(self): # Reject all operations called "RejectMe" execution_context = self.execution_context if execution_context.operation_name == "RejectMe": self.execution_context.result = GraphQLExecutionResult( data=None, errors=[GraphQLError("Well you asked for it")], ) schema = strawberry.Schema( Query, extensions=[ RejectSomeQueries, ], ) ```
Operation Extensions (Requires GraphQL 3.3) ```python import time import strawberry from strawberry.extensions import SchemaExtension class QueryStatsExtension(SchemaExtension): def on_operation(self): execution_context = self.execution_context if execution_context.operation_extensions: if execution_context.operation_extensions.get("stats", False): start_time = time.time() yield end_time = time.time() self.execution_context.extensions_results["stats"] = { "query_time": end_time - start_time } return yield schema = strawberry.Schema( Query, extensions=[ QueryStatsExtension, ], ) ```
### Execution Context The `SchemaExtension` object has an `execution_context` property on `self` of type `ExecutionContext`. This object can be used to gain access to additional GraphQL context, or the request context. Take a look at the [`ExecutionContext` type](https://github.com/strawberry-graphql/strawberry/blob/main/strawberry/types/execution.py) for available data. ```python from strawberry.extensions import SchemaExtension from mydb import get_db_session class MyExtension(SchemaExtension): def on_operation(self): self.execution_context.context["db"] = get_db_session() yield self.execution_context.context["db"].close() ``` strawberry-graphql-0.287.0/docs/guides/dataloaders.md000066400000000000000000000264441511033167500225610ustar00rootroot00000000000000--- title: DataLoaders --- # DataLoaders Strawberry comes with a built-in DataLoader, a generic utility that can be used to reduce the number of requests to databases or third party APIs by batching and caching requests. DataLoaders provide an async API, so they only work in async context Refer the official DataLoaders [specification](https://github.com/graphql/dataloader) for an advanced guide on DataLoaders. ## Basic usage Here's how you'd use a DataLoader, first we need to define a function that allows to fetch data in batches. Let's say that we have a user type, that has only an id: ```python import strawberry @strawberry.type class User: id: strawberry.ID ``` we need to define a function that returns a list of users based on a list of keys passed: ```python from typing import List async def load_users(keys: List[int]) -> List[User]: return [User(id=key) for key in keys] ``` Normally this function would interact with a database or 3rd party API, but for our example we don't need that. Now that we have a loader function, we can define a DataLoader and use it: ```python from strawberry.dataloader import DataLoader loader = DataLoader(load_fn=load_users) user = await loader.load(1) ``` This will result in a call to `load_users` with keys equal to `[1]`. Where this becomes really powerful is when you make multiple requests, like in this example: ```python import asyncio [user_a, user_b] = await asyncio.gather(loader.load(1), loader.load(2)) ``` This will result in a call to `load_users` with keys equal to `[1, 2]`. Thus reducing the number of calls to our database or 3rd party services to 1. Additionally by default DataLoader caches the loads, so for example the following code: ```python await loader.load(1) await loader.load(1) ``` Will result in only one call to `load_users`. And finally sometimes we'll want to load more than one key at a time. In those cases we can use the `load_many` method. ```python [user_a, user_b, user_c] = await loader.load_many([1, 2, 3]) ``` ### Errors An error associated with a particular key can be indicated by including an exception value in the corresponding position in the returned list. This exception will be thrown by the `load` call for that key. With the same `User` class from above: ```python from typing import List, Union from strawberry.dataloader import DataLoader users_database = { 1: User(id=1), 2: User(id=2), } async def load_users(keys: List[int]) -> List[Union[User, ValueError]]: def lookup(key: int) -> Union[User, ValueError]: if user := users_database.get(key): return user return ValueError("not found") return [lookup(key) for key in keys] loader = DataLoader(load_fn=load_users) ``` For this loader, calls like `await loader.load(1)` will return `User(id=1)`, while `await loader.load(3)` will raise `ValueError("not found")`. It's important that the `load_users` function returns exception values within the list for each incorrect key. A call with `keys == [1, 3]` returns `[User(id=1), ValueError("not found")]`, and doesn't raise the `ValueError` directly. If the `load_users` function raises an exception, even `load`s with an otherwise valid key, like `await loader.load(1)`, will raise that exception. ### Overriding Cache Key By default, the input is used as cache key. In the above examples, the cache key is always a scalar (int, float, string, etc.) and uniquely resolves the data for the input. In practical applications there are situations where it requires combination of fields to uniquely identify the data. By providing `cache_key_fn` argument to the `DataLoader` the behaviour of generating key is changed. It is also useful when objects are keys and two objects should be considered equivalent. The function definition takes an input parameter and returns a `Hashable` type. ```python from typing import List, Union from strawberry.dataloader import DataLoader class User: def __init__(self, custom_id: int, name: str): self.id: int = custom_id self.name: str = name async def loader_fn(keys): return keys def custom_cache_key(key): return key.id loader = DataLoader(load_fn=loader_fn, cache_key_fn=custom_cache_key) data1 = await loader.load(User(1, "Nick")) data2 = await loader.load(User(1, "Nick")) assert data1 == data2 # returns true ``` `loader.load(User(1, "Nick"))` will call `custom_cache_key` internally, passing the object as parameter to the function which will return `User.id` as key that is `1`. The second call will check the cache for the key returned by `custom_cache_key` and will return the cache object from the loader cache. The implementation relies on users to handle conflicts while generating the cache key. In case of conflict the data will be overriden for the key. ### Cache invalidation By default DataLoaders use an internal cache. It is great for performance, however it can cause problems when the data is modified (i.e., a mutation), as the cached data is no longer be valid! 😮 To fix it, you can explicitly invalidate the data in the cache, using one of these ways: - Specifying a key with `loader.clear(id)`, - Specifying several keys with `loader.clear_many([id1, id2, id3, ...])`, - Invalidating the whole cache with `loader.clear_all()` ### Importing data into cache While dataloaders are powerful and efficient, they do not support complex queries. If your app needs them, you'll probably mix dataloaders and direct database calls. In these scenarios, it is useful to import the data retrieved externally into the dataloader, in order to avoid reloading data afterwards. For example: ```python @strawberry.type class Person: id: strawberry.ID friends_ids: strawberry.Private[List[strawberry.ID]] @strawberry.field async def friends(self) -> List[Person]: return await loader.load_many(self.friends_ids) @strawberry.type class Query: @strawberry.field async def get_all_people(self) -> List[Person]: # Fetch all people from the database, without going through the dataloader abstraction people = await database.get_all_people() # Insert the people we fetched in the dataloader cache # Since "all people" are now in the cache, accessing `Person.friends` will not # trigger any extra database access loader.prime_many({person.id: person for person in people}) return people ``` ```graphql { getAllPeople { id friends { id } } } ``` ### Custom Cache DataLoader's default cache is per-request and it caches data in memory. This strategy might not be optimal or safe for all use cases. For example, if you are using DataLoader in a distributed environment, you might want to use a distributed cache. DataLoader let you override the custom caching logic, which can get data from other persistent caches (e.g Redis) `DataLoader` provides an argument `cache_map`. It takes an instance of a class which implements an abstract interface `AbstractCache`. The interface methods are `get`, `set`, `delete` and `clear` The `cache_map` parameter overrides the `cache_key_fn` if both arguments are provided. ```python from typing import List, Union, Any, Optional import strawberry from strawberry.asgi import GraphQL from strawberry.dataloader import DataLoader, AbstractCache from starlette.requests import Request from starlette.websockets import WebSocket from starlette.responses import Response class UserCache(AbstractCache): def __init__(self): self.cache = {} def get(self, key: Any) -> Union[Any, None]: return self.cache.get(key) # fetch data from persistent cache def set(self, key: Any, value: Any) -> None: self.cache[key] = value # store data in the cache def delete(self, key: Any) -> None: del self.cache[key] # delete key from the cache def clear(self) -> None: self.cache.clear() # clear the cache @strawberry.type class User: id: strawberry.ID name: str async def load_users(keys) -> List[User]: return [User(id=key, name="Jane Doe") for key in keys] class MyGraphQL(GraphQL): async def get_context( self, request: Union[Request, WebSocket], response: Optional[Response] ) -> Any: return {"user_loader": DataLoader(load_fn=load_users, cache_map=UserCache())} @strawberry.type class Query: @strawberry.field async def get_user(self, info: strawberry.Info, id: strawberry.ID) -> User: return await info.context["user_loader"].load(id) schema = strawberry.Schema(query=Query) app = MyGraphQL(schema) ``` ## Usage with GraphQL Let's see an example of how you can use DataLoaders with GraphQL: ```python from typing import List from strawberry.dataloader import DataLoader import strawberry @strawberry.type class User: id: strawberry.ID async def load_users(keys) -> List[User]: return [User(id=key) for key in keys] loader = DataLoader(load_fn=load_users) @strawberry.type class Query: @strawberry.field async def get_user(self, id: strawberry.ID) -> User: return await loader.load(id) schema = strawberry.Schema(query=Query) ``` Here we have defined the same loader as before, along side with a GraphQL query that allows to fetch a single user by id. We can use this query by doing the following request: ```graphql { first: getUser(id: 1) { id } second: getUser(id: 2) { id } } ``` ```json { "data": { "first": { "id": 1 }, "second": { "id": 2 } } } ``` Even if this query is fetching two users, it still results in one call to `load_users`. ## Usage with context As you have seen in the code above, the dataloader is instantiated outside the resolver, since we need to share it between multiple resolvers or even between multiple resolver calls. However this is a not a recommended pattern when using your schema inside a server because the dataloader will cache results for as long as the server is running. Instead a common pattern is to create the dataloader when creating the GraphQL context so that it only caches results with a single request. Let's see an example of this using our ASGI view: ```python from typing import List, Union, Any, Optional import strawberry from strawberry.asgi import GraphQL from strawberry.dataloader import DataLoader from starlette.requests import Request from starlette.websockets import WebSocket from starlette.responses import Response @strawberry.type class User: id: strawberry.ID async def load_users(keys) -> List[User]: return [User(id=key) for key in keys] class MyGraphQL(GraphQL): async def get_context( self, request: Union[Request, WebSocket], response: Optional[Response] ) -> Any: return {"user_loader": DataLoader(load_fn=load_users)} @strawberry.type class Query: @strawberry.field async def get_user(self, info: strawberry.Info, id: strawberry.ID) -> User: return await info.context["user_loader"].load(id) schema = strawberry.Schema(query=Query) app = MyGraphQL(schema) ``` You can now run the example above with any ASGI server, you can read [ASGI](../integrations/asgi.md)) to get more details on how to run the app. In case you choose uvicorn you can install it wih ```shell pip install uvicorn ``` and then, assuming we named our file above `schema.py` we start the app with ```shell uvicorn schema:app ``` strawberry-graphql-0.287.0/docs/guides/errors.md000066400000000000000000000150561511033167500216070ustar00rootroot00000000000000--- title: Dealing with errors --- # Dealing with errors There are multiple different types of errors in GraphQL and each can be handled differently. In this guide we will outline the different types of errors that you will encounter when building a GraphQL server. **Note**: By default Strawberry will log all execution errors to a `strawberry.execution` logger: [/docs/types/schema#handling-execution-errors](../types/schema#handling-execution-errors). ## GraphQL validation errors GraphQL is strongly typed and so Strawberry validates all queries before executing them. If a query is invalid it isn’t executed and instead the response contains an `errors` list: ```graphql { hi } ``` ```json { "data": null, "errors": [ { "message": "Cannot query field 'hi' on type 'Query'.", "locations": [ { "line": 2, "column": 3 } ], "path": null } ] } ``` Each error has a message, line, column and path to help you identify what part of the query caused the error. The validation rules are part of the GraphQL specification and built into Strawberry, so there’s not really a way to customize this behavior. You can disable all validation by using the [DisableValidation](../extensions/disable-validation) extension. ## GraphQL type errors When a query is executed each field must resolve to the correct type. For example non-null fields cannot return None. ```python import strawberry @strawberry.type class Query: @strawberry.field def hello() -> str: return None schema = strawberry.Schema(query=Query) ``` ```graphql { hello } ``` ```json { "data": null, "errors": [ { "message": "Cannot return null for non-nullable field Query.hello.", "locations": [ { "line": 2, "column": 3 } ], "path": ["hello"] } ] } ``` Each error has a message, line, column and path to help you identify what part of the query caused the error. ## Unhandled execution errors Sometimes a resolver will throw an unexpected error due to a programming error or an invalid assumption. When this happens Strawberry catches the error and exposes it in the top level `errors` field in the response. ```python import strawberry @strawberry.type class User: name: str @strawberry.type class Query: @strawberry.field def user() -> User: raise Exception("Can't find user") schema = strawberry.Schema(query=Query) ``` ```graphql { user { name } } ``` ```json { "data": null, "errors": [ { "message": "Can't find user", "locations": [ { "line": 2, "column": 2 } ], "path": ["user"] } ] } ``` ## Partial responses for failed resolvers By default, GraphQL allows partial responses when a resolver fails. This means that successfully resolved fields are still returned alongside errors. However, this applies only when the erroneous field is defined as optional. Consider the following example: ```python from typing import Optional import strawberry @strawberry.type class Query: @strawberry.field def successful_field(self) -> Optional[str]: return "This field works" @strawberry.field def error_field(self) -> Optional[str]: raise Exception("This field fails") schema = strawberry.Schema(query=Query) ``` ```graphql { successfulField errorField } ``` ```json { "data": { "successfulField": "This field works", "errorField": null }, "errors": [ { "message": "This field fails", "locations": [{ "line": 3, "column": 3 }], "path": ["errorField"] } ] } ``` The response includes both successfully resolved data and error details, demonstrating GraphQL's ability to return partial results. ## Expected errors If an error is expected then it is often best to express it in the schema. This allows the client to deal with the error in a robust way. This could be achieved by making the field optional when there is a possibility that the data won’t exist: ```python from typing import Optional import strawberry @strawberry.type class Query: @strawberry.field def get_user(self, id: str) -> Optional[User]: try: user = get_a_user_by_their_ID return user except UserDoesNotExist: return None ``` When the expected error is more complicated it’s a good pattern to instead return a union of types that either represent an error or a success response. This pattern is often adopted with mutations where it’s important to be able to return more complicated error details to the client. For example, say you have a `registerUser` mutation where you need to deal with the possibility that a user tries to register with a username that already exists. You might structure your mutation type like this: ```python import strawberry from typing import Annotated, Union @strawberry.type class RegisterUserSuccess: user: User @strawberry.type class UsernameAlreadyExistsError: username: str alternative_username: str # Create a Union type to represent the 2 results from the mutation Response = Annotated[ Union[RegisterUserSuccess, UsernameAlreadyExistsError], strawberry.union("RegisterUserResponse"), ] @strawberry.mutation def register_user(username: str, password: str) -> Response: if username_already_exists(username): return UsernameAlreadyExistsError( username=username, alternative_username=generate_username_suggestion(username), ) user = create_user(username, password) return RegisterUserSuccess(user=user) ``` Then your client can look at the `__typename` of the result to determine what to do next: ```graphql mutation RegisterUser($username: String!, $password: String!) { registerUser(username: $username, password: $password) { __typename ... on UsernameAlreadyExistsError { alternativeUsername } ... on RegisterUserSuccess { user { id username } } } } ``` This approach allows you to express the possible error states in the schema and so provide a robust interface for your client to account for all the potential outcomes from a mutation. --- ## Additional resources: [A Guide to GraphQL Errors | productionreadygraphql.com](https://productionreadygraphql.com/2020-08-01-guide-to-graphql-errors/) [200 OK! Error Handling in GraphQL | sachee.medium.com](https://sachee.medium.com/200-ok-error-handling-in-graphql-7ec869aec9bc) strawberry-graphql-0.287.0/docs/guides/federation-v1.md000066400000000000000000000143271511033167500227370ustar00rootroot00000000000000--- title: Federation V1 --- This guide refers to Apollo Federation 1, if you're looking for the 2.0 guide, please see the [federation v2](federation.md) guide. You can also see the [What's new in federation 2](https://www.apollographql.com/docs/federation/federation-2/new-in-federation-2/) for more details. # Apollo Federation V1 Guide Apollo Federation allows you to combine multiple GraphQL APIs into one. This can be extremely useful when working with a service oriented architecture. Strawberry supports [Apollo Federation](https://www.apollographql.com/docs/federation) out of the box, that means that you can create services using Strawberry and federate them via Apollo Gateway. We don’t have a gateway server, you’ll still need to use the Apollo Gateway for this. ## Federated schema example Let’s look at an example on how to implement Apollo Federation using Strawberry. Let's assume we have an application with two services that each expose a GraphQL API: 1. `books`: a service to manage all the books we have 2. `reviews`: a service to manage book reviews ### Books service Our `book` service might look something like this: ```python @strawberry.federation.type(keys=["id"]) class Book: id: strawberry.ID title: str def get_all_books() -> List[Book]: return [Book(id=1, title="The Dark Tower")] @strawberry.type class Query: all_books: List[Book] = strawberry.field(resolver=get_all_books) schema = strawberry.federation.Schema(query=Query) ``` We defined two types: `Book` and `Query`, where `Query` has only one field that allows us to fetch all the books. Notice that the `Book` type is using the `strawberry.federation.type` decorator, as opposed to the normal `strawberry.type`, this new decorator extends the base one and allows us to define federation-specific attributes on the type. Here, we are telling the federation system that the `Book`'s `id` field is its uniquely-identifying key. Federation keys can be thought of as primary keys. They are used by the gateway to query types between multiple services and then join them into the augmented type. ### Reviews service Now, let’s take a look at our review service: we want to define a type for a review but also extend the `Book` type to have a list of reviews. ```python @strawberry.type class Review: id: int body: str def get_reviews(root: "Book") -> List[Review]: return [ Review(id=id_, body=f"A review for {root.id}") for id_ in range(root.reviews_count) ] @strawberry.federation.type(extend=True, keys=["id"]) class Book: id: strawberry.ID = strawberry.federation.field(external=True) reviews_count: int reviews: List[Review] = strawberry.field(resolver=get_reviews) @classmethod def resolve_reference(cls, id: strawberry.ID): # here we could fetch the book from the database # or even from an API return Book(id=id, reviews_count=3) ``` Now things are looking more interesting; the `Review` type is a GraphQL type that holds the contents of the review. We've also been able to extend the `Book` type by using again `strawberry.federation.type`, this time passing `extend=True` as an argument. This is important because we need to tell federation that we are extending a type that already exists, not creating a new one. We have also declared three fields on `Book`, one of which is `id` which is marked as `external` with `strawberry.federation.field(external=True)`. This tells federation that this field is not available in this service, and that it comes from another service. The other fields are `reviews` (the list of `Reviews` for this book) and `reviews_count` (the number of reviews for this book). Finally, we also have a class method, `resolve_reference`, that allows us to instantiate types when they are referred to by other services. The `resolve_reference` method is called when a GraphQL operation references an entity across multiple services. For example, when making this query: ```graphql { # query defined in the books service books { title # field defined in the reviews service reviews { body } } } ``` `resolve_reference` is called with the `id` of the book for each book returned by the books service. Recall that above we defined the `id` field as the `key` for the `Book` type. In this example we are creating an instance of `Book` with the requested `id` and a fixed number of reviews. If we were to add more fields to `Book` that were stored in a database, this would be where we could perform queries for these fields' values. Now we need to do is to define a `Query` type, even if our service only has one type that is not used directly in any GraphQL query. This is because the GraphQL spec mandates that a GraphQL server defines a Query type, even if it ends up being empty/unused. Finally we also need to let Strawberry know about our Book and Review types. Since they are not reachable from the `Query` field itself, Strawberry won't be able to find them by default. ```python @strawberry.type class Query: _hi: str = strawberry.field(resolver=lambda: "Hello world!") schema = strawberry.federation.Schema(query=Query, types=[Book, Review]) ``` ## The gateway Now we have our services up and running, we need to configure a gateway to consume our services. Apollo Gateway is the official gateway server for Apollo Federation. Here's an example on how to configure the gateway: ```js const { ApolloServer } = require("apollo-server"); const { ApolloGateway } = require("@apollo/gateway"); const gateway = new ApolloGateway({ serviceList: [ { name: "books", url: "http://localhost:8000" }, { name: "reviews", url: "http://localhost:8080" }, ], }); const server = new ApolloServer({ gateway }); server.listen().then(({ url }) => { console.log(`🚀 Server ready at ${url}`); }); ``` When running this example you'll be able to run query like the following: ```graphql { allBooks { id reviewsCount reviews { body } } } ``` We have provided a full example that you can run and tweak to play with Strawberry and Federation. The repo is available here: [https://github.com/strawberry-graphql/federation-demo](https://github.com/strawberry-graphql/federation-demo) strawberry-graphql-0.287.0/docs/guides/federation.md000066400000000000000000000236701511033167500224140ustar00rootroot00000000000000--- title: Federation 2 --- # Apollo Federation 2 Guide This guide refers to Apollo Federation 2, if you're looking for the 1.0 guide, please see the [federation v1](federation-v1.md) guide. Apollo Federation allows you to combine multiple GraphQL APIs into one. This can be extremely useful when working with a service oriented architecture. Strawberry supports [Apollo Federation 2](https://www.apollographql.com/docs/federation/federation-2/new-in-federation-2/) out of the box, that means that you can create services using Strawberry and federate them via Apollo Gateway or Apollo Router. ## Federated schema example Let’s look at an example on how to implement Apollo Federation using Strawberry. Let's assume we have an application with two services that each expose a GraphQL API: 1. `books`: a service to manage all the books we have 2. `reviews`: a service to manage book reviews Our folder structure will look something like this: ```text my-app/ ├─ books/ │ ├─ app.py ├─ reviews/ │ ├─ app.py ``` This guide assumes you've installed strawberry in both the books and reviews service ### Books service Let's create the `books` service, copy the following inside `books/app.py` ```python from typing import List import strawberry @strawberry.federation.type(keys=["id"]) class Book: id: strawberry.ID title: str def get_all_books() -> List[Book]: return [Book(id=strawberry.ID("1"), title="The Dark Tower")] @strawberry.type class Query: all_books: List[Book] = strawberry.field(resolver=get_all_books) schema = strawberry.federation.Schema(query=Query) ``` Strawberry supports Apollo Federation 2 only. By default, schemas use Federation version 2.11 (the latest supported version). You can specify a different version if needed: ```python schema = strawberry.federation.Schema( query=Query, federation_version="2.5" # Specify a specific version if needed ) ``` Supported versions: 2.0 - 2.11 We defined two types: `Book` and `Query`, where `Query` has only one field that allows us to fetch all the books. Notice that the `Book` type is using the `strawberry.federation.type` decorator, as opposed to the normal `strawberry.type`, this new decorator extends the base one and allows us to define federation-specific attributes on the type. Here, we are telling the federation system that the `Book`'s `id` field is its uniquely-identifying key. Federation keys can be thought of as primary keys. They are used by the gateway to query types between multiple services and then join them into the augmented type. ### Reviews service Now, let’s take a look at our review service: we want to define a type for a review but also extend the `Book` type to have a list of reviews. Copy the following inside `reviews/app.py`: ```python from typing import List import strawberry @strawberry.type class Review: id: int body: str def get_reviews(root: "Book") -> List[Review]: return [ Review(id=id_, body=f"A review for {root.id}") for id_ in range(root.reviews_count) ] @strawberry.federation.type(keys=["id"]) class Book: id: strawberry.ID reviews_count: int reviews: List[Review] = strawberry.field(resolver=get_reviews) @classmethod def resolve_reference(cls, id: strawberry.ID): # here we could fetch the book from the database # or even from an API return Book(id=id, reviews_count=3) @strawberry.type class Query: _hi: str = strawberry.field(resolver=lambda: "Hello World!") schema = strawberry.federation.Schema(query=Query, types=[Book, Review]) ``` Now things are looking more interesting; the `Review` type is a GraphQL type that holds the contents of the review. But we also have a `Book` which has 3 fields, `id`, `reviews_count` and `reviews`. In Apollo Federation 1 we'd need to mark the `Book` type as an extension and also we'd need to mark `id` as an external field, this is not the case in Apollo Federation 2. Finally, we also have a class method, `resolve_reference`, that allows us to instantiate types when they are referred to by other services. The `resolve_reference` method is called when a GraphQL operation references an entity across multiple services. For example, when making this query: ```graphql { # query defined in the books service allBooks { title # field defined in the reviews service reviews { body } } } ``` `resolve_reference` is called with the `id` of the book for each book returned by the books service. Recall that above we defined the `id` field as the `key` for the `Book` type. In this example we are creating an instance of `Book` with the requested `id` and a fixed number of reviews. If we were to add more fields to `Book` that were stored in a database, this would be where we could perform queries for these fields' values. We also defined a `Query` type that has a single field, `_hi`, which returns a string. This is required because the GraphQL spec mandates that a GraphQL server defines a Query type, even if it ends up being empty/unused. Finally we also need to let Strawberry know about our Book and Review types. Since they are not reachable from the `Query` field itself, Strawberry won't be able to find them. If you don't need any custom logic for your resolve_reference, you can omit it and Strawberry will automatically instanciate the type for you. For example, if we had a `Book` type with only an `id` field, Strawberry would be able to instanciate it for us based on the data returned by the gateway. ```python @strawberry.federation.type(keys=["id"]) class Book: id: strawberry.ID reviews: List[Review] = strawberry.field(resolver=get_reviews) ``` ## Let's run our services Before starting Apollo Router to compose our schemas we need to run the services. In two terminal windows, run the following commands: ```shell cd books strawberry dev --port 3500 app ``` ```shell cd reviews strawberry dev --port 3000 app ``` ## Apollo Router Now we have our services up and running, we need to configure a gateway to consume our services. Apollo provides a router that can be used for this. Before continuing we'll need to install Apollo Router by following [their installation guide](https://www.apollographql.com/docs/router/quickstart/) and we'll need to [install Apollo's CLI](https://www.apollographql.com/docs/rover/getting-started) to compose the schema. Composing the schema means combining all our service's schemas into a single schema. The composed schema will be used by the router to route requests to the appropriate services. Create a file called `supergraph.yaml` with the following contents: ```yaml federation_version: 2 subgraphs: reviews: routing_url: http://localhost:3000 schema: subgraph_url: http://localhost:3000 books: routing_url: http://localhost:3500 schema: subgraph_url: http://localhost:3500 ``` This file will be used by rover to compose the schema, which can be done with the following command: ```shell # Creates prod-schema.graphql or overwrites if it already exists rover supergraph compose --config ./supergraph.yaml > supergraph-schema.graphql ``` Now that we have the composed schema, we can start the router. ```shell ./router --supergraph supergraph-schema.graphql ``` Now that router is running we can go to [http://localhost:4000](http://localhost:4000) and try to run the following query: ```graphql { allBooks { id reviewsCount reviews { body } } } ``` if everything went well we should get the following result: ```json { "data": { "allBooks": [ { "id": "1", "reviewsCount": 3, "reviews": [ { "body": "A review for 1" }, { "body": "A review for 1" }, { "body": "A review for 1" } ] } ] } } ``` We have provided a full example that you can run and tweak to play with Strawberry and Federation. The repo is available here: [https://github.com/strawberry-graphql/federation-demo](https://github.com/strawberry-graphql/federation-demo) ## Federated schema directives Strawberry provides implementations for [Apollo federation-specific GraphQL directives](https://www.apollographql.com/docs/federation/federated-types/federated-directives/) up to federation spec v2.11. Some of these directives may not be necessary to directly include in your code, and are accessed through other means. - `@interfaceObject` (for more details, see [Extending interfaces](https://strawberry.rocks/docs/federation/entity-interfaces)) - `@key` (for more details, see [Entities (Apollo Federation)](https://strawberry.rocks/docs/federation/entities)) - `@link` (is automatically be added to the schema when any other federated schema directive is used) Other directives you may need to specifically include when relevant. - `@composeDirective` - `@external` - `@inaccessible` - `@override` - `@provides` - `@requires` - `@shareable` - `@tag` - `@authenticated` - `@requiresScopes` - `@policy` For example, adding the following directives: ```python import strawberry from strawberry.federation.schema_directives import Inaccessible, Shareable, Tag @strawberry.type(directives=[Key(fields="id"), Tag(name="experimental")]) class Book: id: strawberry.ID @strawberry.type(directives=[Shareable()]) class CommonType: foo: str woops: bool = strawberry.field(directives=[Inaccessible()]) ``` Will result in the following GraphQL schema: ```graphql schema @link( url: "https://specs.apollo.dev/federation/v2.11" import: ["@key", "@inaccessible", "@shareable", "@tag"] ) { query: Query mutation: Mutation } type Book @tag(name: "experimental") @key(fields: "id", resolveable: true) { id: ID! } type CommonType @shareable { foo: String! } ``` ## Additional resources [Apollo Federation Quickstart](https://www.apollographql.com/docs/federation/quickstart/setup/) strawberry-graphql-0.287.0/docs/guides/field-extensions.md000066400000000000000000000133361511033167500235520ustar00rootroot00000000000000--- title: Field extensions --- # Field extensions Field extensions are a great way to implement reusable logic such as permissions or pagination outside your resolvers. They wrap the underlying resolver and are able to modify the field and all arguments passed to the resolver. The following examples only cover sync execution. To use extensions in async contexts, please have a look at [Async Extensions and Resolvers](#async-extensions-and-resolvers) ```python import strawberry from strawberry.extensions import FieldExtension class UpperCaseExtension(FieldExtension): def resolve( self, next_: Callable[..., Any], source: Any, info: strawberry.Info, **kwargs ): result = next_(source, info, **kwargs) return str(result).upper() @strawberry.type class Query: @strawberry.field(extensions=[UpperCaseExtension()]) def string(self) -> str: return "This is a test!!" ``` In this example, the `UpperCaseExtension` wraps the resolver of the `string` field (`next`) and modifies the resulting string to be uppercase. The extension will be called instead of the resolver and receives the resolver function as the `next` argument. Therefore, it is important to not modify any arguments that are passed to `next` in an incompatible way. ```graphql query { string } ``` ```json { "string": "THIS IS A TEST!!" } ``` ## Modifying the field Most of the `StrawberryField` API is not stable and might change in the future without warning. Stable features include: `StrawberryField.type`, `StrawberryField.python_name`, and `StrawberryField.arguments`. In some cases, the extended field needs to be compatible with the added extension. `FieldExtension` provides an `apply(field: StrawberryField)` method that can be overriden to modify the field. It is called during _Schema Conversion_. In the following example, we use `apply` to add a directive to the field: ```python import time import strawberry from strawberry.extensions import FieldExtension from strawberry.schema_directive import Location from strawberry.types.field import StrawberryField @strawberry.schema_directive(locations=[Location.FIELD_DEFINITION]) class Cached: time: int = 100 class CachingExtension(FieldExtension): def __init__(self, caching_time=100): self.caching_time = caching_time self.last_cached = 0.0 self.cached_result = None def apply(self, field: StrawberryField) -> None: field.directives.append(Cached(time=self.caching_time)) def resolve( self, next_: Callable[..., Any], source: Any, info: strawberry.Info, **kwargs ) -> Any: current_time = time.time() if self.last_cached + self.caching_time > current_time: return self.cached_result self.cached_result = next_(source, info, **kwargs) return self.cached_result ``` ```python @strawberry.type class Client: @strawberry.field(extensions=[CachingExtensions(caching_time=200)]) def analyzed_hours(self, info) -> int: return do_expensive_computation() ``` ```graphql type Client { analyzedHours: Int! @Cached(time=200) } ``` ## Combining multiple field extensions When chaining multiple field extensions, the last extension in the list is called first. Then, it calls the next extension until it reaches the resolver. The return value of each extension is passed as an argument to the next extension. This allows for creating a chain of field extensions that each perform a specific transformation on the data being passed through them. ```python @strawberry.field(extensions=[LowerCaseExtension(), UpperCaseExtension()]) def my_field(): return "My Result" ``` **Order matters**: the last extension in the list will be executed first, while the first extension in the list extension will be applied to the field first. This enables cases like adding relay pagination in front of an extension that modifies the field's type. ## Async Extensions and Resolvers Field Extensions support async execution using the `resolve_async` method. A field extension can either support `sync`, `async`, or both. The appropriate resolve function will be automatically chosen based on the type of resolver and other extensions. Since sync-only extensions cannot await the result of an async resolver, they are not compatible with async resolvers or extensions. The other way around is possible: you can add an async-only extension to a sync resolver, or wrap sync-only extensions with it. This is enabled by an automatic use of the `SyncToAsyncExtension`. Note that after adding an async-only extension, you cannot wrap it with a sync-only extension anymore. To optimize the performance of your resolvers, it's recommended that you implement both the `resolve` and `resolve_async` methods when using an extension on both sync and async fields. While the `SyncToAsyncExtension` is convenient, it may add unnecessary overhead to your sync resolvers, leading to slightly decreased performance. ```python import strawberry from strawberry.extensions import FieldExtension class UpperCaseExtension(FieldExtension): def resolve( self, next: Callable[..., Any], source: Any, info: strawberry.Info, **kwargs ): result = next(source, info, **kwargs) return str(result).upper() async def resolve_async( self, next: Callable[..., Awaitable[Any]], source: Any, info: strawberry.Info, **kwargs ): result = await next(source, info, **kwargs) return str(result).upper() @strawberry.type class Query: @strawberry.field(extensions=[UpperCaseExtension()]) async def string(self) -> str: return "This is a test!!" ``` strawberry-graphql-0.287.0/docs/guides/file-upload.md000066400000000000000000000163341511033167500224740ustar00rootroot00000000000000--- title: File Upload --- # File Upload All Strawberry integrations support multipart uploads as described in the [GraphQL multipart request specification](https://github.com/jaydenseric/graphql-multipart-request-spec). This includes support for uploading single files as well as lists of files. ## Security Note that multipart file upload support is disabled by default in all integrations. Before enabling multipart file upload support, make sure you address the [security implications outlined in the specification](https://github.com/jaydenseric/graphql-multipart-request-spec/blob/master/readme.md#security). Usually, this entails enabling CSRF protection in your server framework (e.g., the `CsrfViewMiddleware` middleware in Django). To enable file upload support, pass `multipart_uploads_enabled=True` to your integration's view class. Refer to the integration-specific documentation for more details on how to do this. ## Upload Scalar Uploads can be used in mutations via the `Upload` scalar. The type passed at runtime depends on the integration: | Integration | Type | | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | [AIOHTTP](/docs/integrations/aiohttp) | [`io.BytesIO`](https://docs.python.org/3/library/io.html#io.BytesIO) | | [ASGI](/docs/integrations/asgi) | [`starlette.datastructures.UploadFile`](https://www.starlette.io/requests/#request-files) | | [Channels](/docs/integrations/channels) | [`django.core.files.uploadedfile.UploadedFile`](https://docs.djangoproject.com/en/dev/ref/files/uploads/#django.core.files.uploadedfile.UploadedFile) | | [Django](/docs/integrations/django) | [`django.core.files.uploadedfile.UploadedFile`](https://docs.djangoproject.com/en/dev/ref/files/uploads/#django.core.files.uploadedfile.UploadedFile) | | [FastAPI](/docs/integrations/fastapi) | [`fastapi.UploadFile`](https://fastapi.tiangolo.com/tutorial/request-files/#file-parameters-with-uploadfile) | | [Flask](/docs/integrations/flask) | [`werkzeug.datastructures.FileStorage`](https://werkzeug.palletsprojects.com/en/latest/datastructures/#werkzeug.datastructures.FileStorage) | | [Quart](/docs/integrations/quart) | [`quart.datastructures.FileStorage`](https://github.com/pallets/quart/blob/main/src/quart/datastructures.py) | | [Sanic](/docs/integrations/sanic) | [`sanic.request.File`](https://sanic.readthedocs.io/en/stable/sanic/api/core.html#sanic.request.File) | | [Starlette](/docs/integrations/starlette) | [`starlette.datastructures.UploadFile`](https://www.starlette.io/requests/#request-files) | In order to have the correct runtime type in resolver type annotations you can set a scalar override based on the integrations above. For example with Starlette: ```python import strawberry from starlette.datastructures import UploadFile from strawberry.file_uploads import Upload schema = strawberry.Schema( ... scalar_overrides={UploadFile: Upload} ) ``` ## ASGI / FastAPI / Starlette Since these integrations use asyncio for communication, the resolver _must_ be async. Additionally, these servers rely on the `python-multipart` package, which is not included by Strawberry by default. It can be installed directly, or, for convenience, it is included in extras: `strawberry-graphql[asgi]` (for ASGI/Starlette) or `strawberry-graphql[fastapi]` (for FastAPI). For example: - if using Pip, `pip install 'strawberry-graphql[fastapi]'` - if using Poetry, `strawberry-graphql = { version = "...", extras = ["fastapi"] }` in `pyproject.toml`. Example: ```python import typing import strawberry from strawberry.file_uploads import Upload @strawberry.input class FolderInput: files: typing.List[Upload] @strawberry.type class Mutation: @strawberry.mutation async def read_file(self, file: Upload) -> str: return (await file.read()).decode("utf-8") @strawberry.mutation async def read_files(self, files: typing.List[Upload]) -> typing.List[str]: contents = [] for file in files: content = (await file.read()).decode("utf-8") contents.append(content) return contents @strawberry.mutation async def read_folder(self, folder: FolderInput) -> typing.List[str]: contents = [] for file in folder.files: content = (await file.read()).decode("utf-8") contents.append(content) return contents ``` ## Sanic / Flask / Django / Channels / AIOHTTP Example: ```python import typing import strawberry from strawberry.file_uploads import Upload @strawberry.input class FolderInput: files: typing.List[Upload] @strawberry.type class Mutation: @strawberry.mutation def read_file(self, file: Upload) -> str: return file.read().decode("utf-8") @strawberry.mutation def read_files(self, files: typing.List[Upload]) -> typing.List[str]: contents = [] for file in files: content = file.read().decode("utf-8") contents.append(content) return contents @strawberry.mutation def read_folder(self, folder: FolderInput) -> typing.List[str]: contents = [] for file in folder.files: contents.append(file.read().decode("utf-8")) return contents ``` ## Sending file upload requests The tricky part is sending the HTTP request from the client because it must follow the GraphQL multipart request specifications mentioned above. The `multipart/form-data` POST request's data must include: - `operations` key for GraphQL request with query and variables - `map` key with mapping some multipart-data to exact GraphQL variable - and other keys for multipart-data which contains binary data of files Assuming you have your schema up and running, here there are some requests examples: ### Sending one file ```shell curl localhost:8000/graphql \ -F operations='{ "query": "mutation($file: Upload!){ readFile(file: $file) }", "variables": { "file": null } }' \ -F map='{ "file": ["variables.file"] }' \ -F file=@a.txt ``` ### Sending a list of files ```shell curl localhost:8000/graphql \ -F operations='{ "query": "mutation($files: [Upload!]!) { readFiles(files: $files) }", "variables": { "files": [null, null] } }' \ -F map='{"file1": ["variables.files.0"], "file2": ["variables.files.1"]}' \ -F file1=@b.txt \ -F file2=@c.txt ``` ### Sending nested files ```shell curl localhost:8000/graphql \ -F operations='{ "query": "mutation($folder: FolderInput!) { readFolder(folder: $folder) }", "variables": {"folder": {"files": [null, null]}} }' \ -F map='{"file1": ["variables.folder.files.0"], "file2": ["variables.folder.files.1"]}' \ -F file1=@b.txt \ -F file2=@c.txt ``` strawberry-graphql-0.287.0/docs/guides/pagination/000077500000000000000000000000001511033167500220735ustar00rootroot00000000000000strawberry-graphql-0.287.0/docs/guides/pagination/connections.md000066400000000000000000000363751511033167500247550ustar00rootroot00000000000000--- title: Pagination - Implementing the Relay Connection Specification --- # Implementing the Relay Connection Specification We naively implemented cursor based pagination in the [previous tutorial](./cursor-based.md). To ensure a consistent implementation of this pattern, the Relay project has a formal [specification](https://relay.dev/graphql/connections.htm) you can follow for building GraphQL APIs which use a cursor based connection pattern. By the end of this tutorial, we should be able to return a connection of users when requested. ```graphql query getUsers { getUsers(first: 2) { users { edges { node { id name occupation age } } cursor } pageInfo { endCursor hasNextPage } } } ``` ```json { "data": { "getUsers": { "users": { "edges": [ { "node": { "id": 1, "name": "Norman Osborn", "occupation": "Founder, Oscorp Industries", "age": 42 }, "cursor": "dXNlcjox" }, { "node": { "id": 2, "name": "Peter Parker", "occupation": "Freelance Photographer, The Daily Bugle", "age": 20 }, "cursor": "dXNlcjoy" } ] }, "pageInfo": { "endCursor": "dXNlcjoz", "hasNextPage": true } } } } ``` ## Connections A Connection represents a paginated relationship between two entities. This pattern is used when the relationship itself has attributes. For example, we might have a connection of users to represent a paginated list of users. Let us define a Connection type which takes in a Generic ObjectType. ```py # example.py from typing import Generic, TypeVar import strawberry GenericType = TypeVar("GenericType") @strawberry.type class Connection(Generic[GenericType]): page_info: "PageInfo" = strawberry.field( description="Information to aid in pagination." ) edges: list["Edge[GenericType]"] = strawberry.field( description="A list of edges in this connection." ) ``` Connections must have atleast two fields: `edges` and `page_info`. The `page_info` field contains metadata about the connection. Following the Relay specification, we can define a `PageInfo` type like this: ```py line=22-38 # example.py from typing import Generic, TypeVar import strawberry GenericType = TypeVar("GenericType") @strawberry.type class Connection(Generic[GenericType]): page_info: "PageInfo" = strawberry.field( description="Information to aid in pagination." ) edges: list["Edge[GenericType]"] = strawberry.field( description="A list of edges in this connection." ) @strawberry.type class PageInfo: has_next_page: bool = strawberry.field( description="When paginating forwards, are there more items?" ) has_previous_page: bool = strawberry.field( description="When paginating backwards, are there more items?" ) start_cursor: Optional[str] = strawberry.field( description="When paginating backwards, the cursor to continue." ) end_cursor: Optional[str] = strawberry.field( description="When paginating forwards, the cursor to continue." ) ``` You can read more about the `PageInfo` type at: - https://graphql.org/learn/pagination/#pagination-and-edges - https://relay.dev/graphql/connections.htm The `edges` field must return a list type that wraps an edge type. Following the Relay specification, let us define an Edge that takes in a generic ObjectType. ```py line=41-49 # example.py from typing import Generic, TypeVar import strawberry GenericType = TypeVar("GenericType") @strawberry.type class Connection(Generic[GenericType]): page_info: "PageInfo" = strawberry.field( description="Information to aid in pagination." ) edges: list["Edge[GenericType]"] = strawberry.field( description="A list of edges in this connection." ) @strawberry.type class PageInfo: has_next_page: bool = strawberry.field( description="When paginating forwards, are there more items?" ) has_previous_page: bool = strawberry.field( description="When paginating backwards, are there more items?" ) start_cursor: Optional[str] = strawberry.field( description="When paginating backwards, the cursor to continue." ) end_cursor: Optional[str] = strawberry.field( description="When paginating forwards, the cursor to continue." ) @strawberry.type class Edge(Generic[GenericType]): node: GenericType = strawberry.field(description="The item at the end of the edge.") cursor: str = strawberry.field(description="A cursor for use in pagination.") ``` EdgeTypes must have atleast two fields - `cursor` and `node`. Each edge has it's own cursor and item (represented by the `node` field). Now that we have the types needed to implement pagination using Relay Connections, let us use them to paginate a list of users. For simplicity's sake, let our dataset be a list of dictionaries. ```py line=7-32 # example.py from typing import Generic, TypeVar import strawberry user_data = [ { "id": 1, "name": "Norman Osborn", "occupation": "Founder, Oscorp Industries", "age": 42, }, { "id": 2, "name": "Peter Parker", "occupation": "Freelance Photographer, The Daily Bugle", "age": 20, }, { "id": 3, "name": "Harold Osborn", "occupation": "President, Oscorp Industries", "age": 19, }, { "id": 4, "name": "Eddie Brock", "occupation": "Journalist, The Eddie Brock Report", "age": 20, }, ] GenericType = TypeVar("GenericType") @strawberry.type class Connection(Generic[GenericType]): page_info: "PageInfo" = strawberry.field( description="Information to aid in pagination." ) edges: list["Edge[GenericType]"] = strawberry.field( description="A list of edges in this connection." ) @strawberry.type class PageInfo: has_next_page: bool = strawberry.field( description="When paginating forwards, are there more items?" ) has_previous_page: bool = strawberry.field( description="When paginating backwards, are there more items?" ) start_cursor: Optional[str] = strawberry.field( description="When paginating backwards, the cursor to continue." ) end_cursor: Optional[str] = strawberry.field( description="When paginating forwards, the cursor to continue." ) @strawberry.type class Edge(Generic[GenericType]): node: GenericType = strawberry.field(description="The item at the end of the edge.") cursor: str = strawberry.field(description="A cursor for use in pagination.") ``` Now is a good time to think of what we could use as a cursor for our dataset. Our cursor needs to be an opaque value, which doesn't usually change over time. It makes sense to use base64 encoded IDs of users as our cursor, as they fit both criteria. While working with Connections, it is a convention to base64-encode cursors. It provides a unified interface to the end user. API clients need not bother about the type of data to paginate, and can pass unique IDs during pagination. It also makes the cursors opaque. Let us define a couple of helper functions to encode and decode cursors as follows: ```py line=3,35-43 # example.py from base64 import b64encode, b64decode from typing import Generic, TypeVar import strawberry user_data = [ { "id": 1, "name": "Norman Osborn", "occupation": "Founder, Oscorp Industries", "age": 42, }, { "id": 2, "name": "Peter Parker", "occupation": "Freelance Photographer, The Daily Bugle", "age": 20, }, { "id": 3, "name": "Harold Osborn", "occupation": "President, Oscorp Industries", "age": 19, }, { "id": 4, "name": "Eddie Brock", "occupation": "Journalist, The Eddie Brock Report", "age": 20, }, ] def encode_user_cursor(id: int) -> str: """ Encodes the given user ID into a cursor. :param id: The user ID to encode. :return: The encoded cursor. """ return b64encode(f"user:{id}".encode("ascii")).decode("ascii") def decode_user_cursor(cursor: str) -> int: """ Decodes the user ID from the given cursor. :param cursor: The cursor to decode. :return: The decoded user ID. """ cursor_data = b64decode(cursor.encode("ascii")).decode("ascii") return int(cursor_data.split(":")[1]) GenericType = TypeVar("GenericType") @strawberry.type class Connection(Generic[GenericType]): page_info: "PageInfo" = strawberry.field( description="Information to aid in pagination." ) edges: list["Edge[GenericType]"] = strawberry.field( description="A list of edges in this connection." ) @strawberry.type class PageInfo: has_next_page: bool = strawberry.field( description="When paginating forwards, are there more items?" ) has_previous_page: bool = strawberry.field( description="When paginating backwards, are there more items?" ) start_cursor: Optional[str] = strawberry.field( description="When paginating backwards, the cursor to continue." ) end_cursor: Optional[str] = strawberry.field( description="When paginating forwards, the cursor to continue." ) @strawberry.type class Edge(Generic[GenericType]): node: GenericType = strawberry.field(description="The item at the end of the edge.") cursor: str = strawberry.field(description="A cursor for use in pagination.") ``` Let us define a `get_users` field which returns a connection of users, as well as an `UserType`. Let us also plug our query into a schema. ```python line=104-174 # example.py from base64 import b64encode, b64decode from typing import List, Optional, Generic, TypeVar import strawberry user_data = [ { "id": 1, "name": "Norman Osborn", "occupation": "Founder, Oscorp Industries", "age": 42, }, { "id": 2, "name": "Peter Parker", "occupation": "Freelance Photographer, The Daily Bugle", "age": 20, }, { "id": 3, "name": "Harold Osborn", "occupation": "President, Oscorp Industries", "age": 19, }, { "id": 4, "name": "Eddie Brock", "occupation": "Journalist, The Eddie Brock Report", "age": 20, }, ] def encode_user_cursor(id: int) -> str: """ Encodes the given user ID into a cursor. :param id: The user ID to encode. :return: The encoded cursor. """ return b64encode(f"user:{id}".encode("ascii")).decode("ascii") def decode_user_cursor(cursor: str) -> int: """ Decodes the user ID from the given cursor. :param cursor: The cursor to decode. :return: The decoded user ID. """ cursor_data = b64decode(cursor.encode("ascii")).decode("ascii") return int(cursor_data.split(":")[1]) GenericType = TypeVar("GenericType") @strawberry.type class Connection(Generic[GenericType]): page_info: "PageInfo" = strawberry.field( description="Information to aid in pagination." ) edges: list["Edge[GenericType]"] = strawberry.field( description="A list of edges in this connection." ) @strawberry.type class PageInfo: has_next_page: bool = strawberry.field( description="When paginating forwards, are there more items?" ) has_previous_page: bool = strawberry.field( description="When paginating backwards, are there more items?" ) start_cursor: Optional[str] = strawberry.field( description="When paginating backwards, the cursor to continue." ) end_cursor: Optional[str] = strawberry.field( description="When paginating forwards, the cursor to continue." ) @strawberry.type class Edge(Generic[GenericType]): node: GenericType = strawberry.field(description="The item at the end of the edge.") cursor: str = strawberry.field(description="A cursor for use in pagination.") @strawberry.type class User: id: int = strawberry.field(description="The id of the user.") name: str = strawberry.field(description="The name of the user.") occupation: str = strawberry.field(description="The occupation of the user.") age: int = strawberry.field(description="The age of the user.") @strawberry.type class Query: @strawberry.field(description="Get a list of users.") def get_users( self, first: int = 2, after: Optional[str] = None ) -> Connection[User]: if after is not None: # decode the user ID from the given cursor. user_id = decode_user_cursor(cursor=after) else: # no cursor was given (this happens usually when the # client sends a query for the first time). user_id = 0 # filter the user data, going through the next set of results. filtered_data = list(filter(lambda user: user["id"] > user_id, user_data)) # slice the relevant user data (Here, we also slice an # additional user instance, to prepare the next cursor). sliced_users = filtered_data[: first + 1] if len(sliced_users) > first: # calculate the client's next cursor. last_user = sliced_users.pop(-1) next_cursor = encode_user_cursor(id=last_user["id"]) has_next_page = True else: # We have reached the last page, and # don't have the next cursor. next_cursor = None has_next_page = False # We know that we have items in the # previous page window if the initial user ID # was not the first one. has_previous_page = user_id > 0 # build user edges. edges = [ Edge( node=User(**user), cursor=encode_user_cursor(id=user["id"]), ) for user in sliced_users ] if edges: # we have atleast one edge. Get the cursor # of the first edge we have. start_cursor = edges[0].cursor else: # We have no edges to work with. start_cursor = None if len(edges) > 1: # We have atleast 2 edges. Get the cursor # of the last edge we have. end_cursor = edges[-1].cursor else: # We don't have enough edges to work with. end_cursor = None return Connection( edges=edges, page_info=PageInfo( has_next_page=has_next_page, has_previous_page=has_previous_page, start_cursor=start_cursor, end_cursor=end_cursor, ), ) schema = strawberry.Schema(query=Query) ``` you can start the dev server with the following command: ```shell strawberry dev example:schema ``` Here's an example query to try out: ```graphql query getUsers { getUsers(first: 2) { edges { node { id name occupation age } cursor } pageInfo { endCursor hasNextPage } } } ``` strawberry-graphql-0.287.0/docs/guides/pagination/cursor-based.md000066400000000000000000000257431511033167500250210ustar00rootroot00000000000000--- title: Pagination - Cursor based --- # Implementing Cursor Pagination Make sure to check our introduction to pagination [here](./overview.md)! Let us implement cursor based pagination in GraphQL. By the end of this tutorial, we should be able to return a paginated list of users when requested. ```graphql query getUsers { getUsers(limit: 2) { users { id name occupation age } pageMeta { nextCursor } } } ``` ```json { "data": { "getUsers": { "users": [ { "id": 1, "name": "Norman Osborn", "occupation": "Founder, Oscorp Industries", "age": 42 }, { "id": 2, "name": "Peter Parker", "occupation": "Freelance Photographer, The Daily Bugle", "age": 20 } ], "pageMeta": { "nextCursor": "dXNlcjoz" } } } } ``` The server needs to return a `cursor` along with the sliced user data, so that our client can know what to query for next. The client could also provide a `limit` value, to specify how much users it wants at a time. Let us model our schema like this: ```py # example.py from typing import List, Optional, Dict, Any, cast import strawberry @strawberry.type class User: id: str = strawberry.field(description="ID of the user.") name: str = strawberry.field(description="The name of the user.") occupation: str = strawberry.field(description="The occupation of the user.") age: int = strawberry.field(description="The age of the user.") @staticmethod def from_row(row: Dict[str, Any]) -> "User": return User( id=row["id"], name=row["name"], occupation=row["occupation"], age=row["age"] ) @strawberry.type class PageMeta: next_cursor: Optional[str] = strawberry.field( description="The next cursor to continue with." ) @strawberry.type class UserResponse: users: List[User] = strawberry.field(description="The list of users.") page_meta: PageMeta = strawberry.field(description="Metadata to aid in pagination.") @strawberry.type class Query: @strawberry.field(description="Get a list of users.") def get_users(self) -> UserResponse: ... schema = strawberry.Schema(query=Query) ``` For simplicity's sake, our dataset is going to be an in-memory list. ```py line=7-32 # example.py from typing import List, Optional, Dict, Any, cast import strawberry user_data = [ { "id": 1, "name": "Norman Osborn", "occupation": "Founder, Oscorp Industries", "age": 42, }, { "id": 2, "name": "Peter Parker", "occupation": "Freelance Photographer, The Daily Bugle", "age": 20, }, { "id": 3, "name": "Harold Osborn", "occupation": "President, Oscorp Industries", "age": 19, }, { "id": 4, "name": "Eddie Brock", "occupation": "Journalist, The Eddie Brock Report", "age": 20, }, ] @strawberry.type class User: id: str = strawberry.field(description="ID of the user.") name: str = strawberry.field(description="The name of the user.") occupation: str = strawberry.field(description="The occupation of the user.") age: int = strawberry.field(description="The age of the user.") @staticmethod def from_row(row: Dict[str, Any]) -> "User": return User( id=row["id"], name=row["name"], occupation=row["occupation"], age=row["age"] ) @strawberry.type class PageMeta: next_cursor: Optional[str] = strawberry.field( description="The next cursor to continue with." ) @strawberry.type class UserResponse: users: List[User] = strawberry.field(description="The list of users.") page_meta: PageMeta = strawberry.field(description="Metadata to aid in pagination.") @strawberry.type class Query: @strawberry.field(description="Get a list of users.") def get_users(self) -> UserResponse: ... schema = strawberry.Schema(query=Query) ``` Now is a good time to think of what we could use as a cursor for our dataset. Our cursor needs to be an opaque value, which doesn't usually change over time. It makes sense to use base64 encoded IDs of users as our cursor, as they fit both criteria. It is good practice to base64-encode cursors, to provide a unified interface to the end user. API clients need not bother about the type of data to paginate, and can pass unique IDs during pagination. It also makes the cursor opaque. Let us define a couple of helper functions to encode and decode cursors as follows: ```py line=3,35-43 # example.py from base64 import b64encode, b64decode from typing import List, Optional, Dict, Any, cast import strawberry user_data = [ { "id": 1, "name": "Norman Osborn", "occupation": "Founder, Oscorp Industries", "age": 42, }, { "id": 2, "name": "Peter Parker", "occupation": "Freelance Photographer, The Daily Bugle", "age": 20, }, { "id": 3, "name": "Harold Osborn", "occupation": "President, Oscorp Industries", "age": 19, }, { "id": 4, "name": "Eddie Brock", "occupation": "Journalist, The Eddie Brock Report", "age": 20, }, ] def encode_user_cursor(id: int) -> str: """ Encodes the given user ID into a cursor. :param id: The user ID to encode. :return: The encoded cursor. """ return b64encode(f"user:{id}".encode("ascii")).decode("ascii") def decode_user_cursor(cursor: str) -> int: """ Decodes the user ID from the given cursor. :param cursor: The cursor to decode. :return: The decoded user ID. """ cursor_data = b64decode(cursor.encode("ascii")).decode("ascii") return int(cursor_data.split(":")[1]) @strawberry.type class User: id: str = strawberry.field(description="ID of the user.") name: str = strawberry.field(description="The name of the user.") occupation: str = strawberry.field(description="The occupation of the user.") age: int = strawberry.field(description="The age of the user.") @staticmethod def from_row(row: Dict[str, Any]) -> "User": return User( id=row["id"], name=row["name"], occupation=row["occupation"], age=row["age"] ) @strawberry.type class PageMeta: next_cursor: Optional[str] = strawberry.field( description="The next cursor to continue with." ) @strawberry.type class UserResponse: users: List[User] = strawberry.field(description="The list of users.") page_meta: PageMeta = strawberry.field(description="Metadata to aid in pagination.") @strawberry.type class Query: @strawberry.field(description="Get a list of users.") def get_users(self) -> UserResponse: ... schema = strawberry.Schema(query=Query) ``` We're going to use the dataset we defined in our `get_users` field resolver. Our field is going to accept two arguments, `limit` and `cursor`, to control pagination. Let us implement the pagination logic as follows. Now, let us implement the pagination logic. ```py line=79-115 # example.py from base64 import b64encode, b64decode from typing import List, Optional, Dict, Any, cast import strawberry user_data = [ { "id": 1, "name": "Norman Osborn", "occupation": "Founder, Oscorp Industries", "age": 42, }, { "id": 2, "name": "Peter Parker", "occupation": "Freelance Photographer, The Daily Bugle", "age": 20, }, { "id": 3, "name": "Harold Osborn", "occupation": "President, Oscorp Industries", "age": 19, }, { "id": 4, "name": "Eddie Brock", "occupation": "Journalist, The Eddie Brock Report", "age": 20, }, ] def encode_user_cursor(id: int) -> str: """ Encodes the given user ID into a cursor. :param id: The user ID to encode. :return: The encoded cursor. """ return b64encode(f"user:{id}".encode("ascii")).decode("ascii") def decode_user_cursor(cursor: str) -> int: """ Decodes the user ID from the given cursor. :param cursor: The cursor to decode. :return: The decoded user ID. """ cursor_data = b64decode(cursor.encode("ascii")).decode("ascii") return int(cursor_data.split(":")[1]) @strawberry.type class User: id: str = strawberry.field(description="ID of the user.") name: str = strawberry.field(description="The name of the user.") occupation: str = strawberry.field(description="The occupation of the user.") age: int = strawberry.field(description="The age of the user.") @staticmethod def from_row(row: Dict[str, Any]) -> "User": return User( id=row["id"], name=row["name"], occupation=row["occupation"], age=row["age"] ) @strawberry.type class PageMeta: next_cursor: Optional[str] = strawberry.field( description="The next cursor to continue with." ) @strawberry.type class UserResponse: users: List[User] = strawberry.field(description="The list of users.") page_meta: PageMeta = strawberry.field(description="Metadata to aid in pagination.") @strawberry.type class Query: @strawberry.field(description="Get a list of users.") def get_users(self, limit: int, cursor: Optional[str] = None) -> UserResponse: if cursor is not None: # decode the user ID from the given cursor. user_id = decode_user_cursor(cursor=cursor) else: # no cursor was given (this happens usually when the # client sends a query for the first time). user_id = 0 # filter the user data, going through the next set of results. filtered_data = [user for user in user_data if user["id"] >= user_id] # slice the relevant user data (Here, we also slice an # additional user instance, to prepare the next cursor). sliced_users = filtered_data[: limit + 1] if len(sliced_users) > limit: # calculate the client's next cursor. last_user = sliced_users.pop(-1) next_cursor = encode_user_cursor(id=last_user["id"]) else: # We have reached the last page, and # don't have the next cursor. next_cursor = None sliced_users = [User.from_row(x) for x in sliced_users] return UserResponse( users=sliced_users, page_meta=PageMeta(next_cursor=next_cursor) ) schema = strawberry.Schema(query=Query) ``` Did you notice that cursor argument we defined is optional? That's because the client doesn't know the cursor initially, when it makes the first request. Now, let us start a dev server with our schema! ```shell strawberry dev example:schema ``` We should be able to query for users on the GraphiQL explorer. Here's a sample query for you! ```graphql query getUsers { getUsers(limit: 2) { users { id name occupation age } pageMeta { nextCursor } } } ``` strawberry-graphql-0.287.0/docs/guides/pagination/offset-based.md000066400000000000000000000170571511033167500247710ustar00rootroot00000000000000--- title: Pagination - Offset based --- # Implementing Offset-Based Pagination Make sure to check our introduction to pagination [here](./overview.md)! Let us implement offset-based pagination in GraphQL. By the end of this tutorial, we should be able to return a sorted, filtered, and paginated list of users. Let us model the `User` type, which represents one user, with a name, occupation, and age. ```python # example.py from typing import List, TypeVar, Dict, Any, Generic import strawberry @strawberry.type class User: name: str = strawberry.field(description="The name of the user.") occupation: str = strawberry.field(description="The occupation of the user.") age: int = strawberry.field(description="The age of the user.") @staticmethod def from_row(row: Dict[str, Any]): return User(name=row["name"], occupation=row["occupation"], age=row["age"]) ``` Let us now model the `PaginationWindow`, which represents one "slice" of sorted, filtered, and paginated items. ```python Item = TypeVar("Item") @strawberry.type class PaginationWindow(Generic[Item]): items: List[Item] = strawberry.field( description="The list of items in this pagination window." ) total_items_count: int = strawberry.field( description="Total number of items in the filtered dataset." ) ``` Note that `PaginationWindow` is generic - it can represent a slice of users, or a slice of any other type of items that we might want to paginate. `PaginationWindow` also contains `total_items_count`, which specifies how many items there are in total in the filtered dataset, so that the client knows what the highest offset value can be. Let's define the query: ```python @strawberry.type class Query: @strawberry.field(description="Get a list of users.") def users( self, order_by: str, limit: int, offset: int = 0, name: str | None = None, occupation: str | None = None, ) -> PaginationWindow[User]: filters = {} if name: filters["name"] = name if occupation: filters["occupation"] = occupation return get_pagination_window( dataset=user_data, ItemType=User, order_by=order_by, limit=limit, offset=offset, filters=filters, ) schema = strawberry.Schema(query=Query) ``` Now we'll define a mock dataset and implement the `get_pagination_window` function, which is used by the `users` query. For the sake of simplicity, our dataset will be an in-memory list containing four users: ```python user_data = [ { "id": 1, "name": "Norman Osborn", "occupation": "Founder, Oscorp Industries", "age": 42, }, { "id": 2, "name": "Peter Parker", "occupation": "Freelance Photographer, The Daily Bugle", "age": 20, }, { "id": 3, "name": "Harold Osborn", "occupation": "President, Oscorp Industries", "age": 19, }, { "id": 4, "name": "Eddie Brock", "occupation": "Journalist, The Eddie Brock Report", "age": 20, }, ] ``` Here's the implementation of the `get_pagination_window` function. Note that it is generic and should work for all item types, not only for the `User` type. ```python def get_pagination_window( dataset: List[GenericType], ItemType: type, order_by: str, limit: int, offset: int = 0, filters: dict[str, str] = {}, ) -> PaginationWindow: """ Get one pagination window on the given dataset for the given limit and offset, ordered by the given attribute and filtered using the given filters """ if limit <= 0 or limit > 100: raise Exception(f"limit ({limit}) must be between 0-100") if filters: dataset = list(filter(lambda x: matches(x, filters), dataset)) dataset.sort(key=lambda x: x[order_by]) if offset != 0 and not 0 <= offset < len(dataset): raise Exception(f"offset ({offset}) is out of range " f"(0-{len(dataset) - 1})") total_items_count = len(dataset) items = dataset[offset : offset + limit] items = [ItemType.from_row(x) for x in items] return PaginationWindow(items=items, total_items_count=total_items_count) def matches(item, filters): """ Test whether the item matches the given filters. This demo only supports filtering by string fields. """ for attr_name, val in filters.items(): if val not in item[attr_name]: return False return True ``` The above code first filters the dataset according to the given filters, then sorts the dataset according to the given `order_by` field. It then calculates `total_items_count` (this must be done after filtering), and then slices the relevant items according to `offset` and `limit`. Finally, it converts the items to the given strawberry type, and returns a `PaginationWindow` containing these items, as well as the `total_items_count`. In a real project, you would probably replace this with code that fetches from a database using `offset` and `limit`. If you're using Strawberry with the Django web framework, you might want to make use of the Django pagination API. You can check it out [here](https://docs.djangoproject.com/en/4.0/topics/pagination/). ## Running the Query Now, let us start the server and see offset-based pagination in action! ```shell strawberry dev example:schema ``` You will get the following message: ```text Running strawberry on http://0.0.0.0:8000/graphql 🍓 ``` Go to [http://0.0.0.0:8000/graphql](http://0.0.0.0:8000/graphql) to open **GraphiQL**, and run the following query to get first two users, ordered by name: ```graphql { users(orderBy: "name", offset: 0, limit: 2) { items { name age occupation } totalItemsCount } } ``` The result should look like this: ```json { "data": { "users": { "items": [ { "name": "Eddie Brock", "age": 20, "occupation": "Journalist, The Eddie Brock Report" }, { "name": "Harold Osborn", "age": 19, "occupation": "President, Oscorp Industries" } ], "totalItemsCount": 4 } } } ``` The result contains: - `items` - A list of the users in this pagination window - `totalItemsCount` - The total number of items in the filtered dataset. In this case, since no filter was given in the request, `totalItemsCount` is 4, which is equal to the total number of users in the in-memory dataset. Get the next page of users by running the same query, after incrementing `offset` by `limit`. Repeat until `offset` reaches `totalItemsCount`. ## Running a Filtered Query Let's run the query again, but this time we'll filter out some users based on their occupation. ```graphql { users(orderBy: "name", offset: 0, limit: 2, occupation: "ie") { items { name age occupation } totalItemsCount } } ``` By supplying `occupation: "ie"` in the query, we are requesting only users whose occupation contains the substring "ie". This is the result: ```json { "data": { "users": { "items": [ { "name": "Eddie Brock", "age": 20, "occupation": "Journalist, The Eddie Brock Report" }, { "name": "Harold Osborn", "age": 19, "occupation": "President, Oscorp Industries" } ], "totalItemsCount": 3 } } } ``` Note that `totalItemsCount` is now 3 and not 4, because only 3 users in total match the filter. strawberry-graphql-0.287.0/docs/guides/pagination/overview.md000066400000000000000000000126461511033167500242740ustar00rootroot00000000000000--- title: Pagination - Overview --- # Pagination Whenever we deal with lists in GraphQL, we usually need to limit the number of items returned. Surely, we don't want to send massive lists of items that take a considerable toll on the server! The goal of this guide is to help you get going fast with pagination! ## Pagination at a Glance Let us take a look at some of the common ways pagination can be implemented today! ### Offset-Based Pagination This type of pagination is widely used, and it is similar to the syntax we use when looking up database records. Here, the client specifies: - `limit`: The number of items to be obtained at a time, and - `offset`: The number of items to be skipped from the beginning. Implementing offset-based pagination with an SQL database is straightforward. We use the `limit` and `offset` values given to query for the items. Offset-based pagination also provides us the ability to skip ahead to any offset, without first needing to get all the items before it. Let us understand offset-based pagination better, with an example. Let us assume that we want to request a list of users, two at a time, from a server. We start by sending a request to the server, with the desired `limit` and `offset` values. ```json { "limit": 2, "offset": 0 } ``` We are not sending GraphQL requests here, don't worry about the request format for now! We are looking into pagination conceptually. We'll implement pagination in GraphQL later! The response from the server would be: ```json { "users": [ { "id": 1, "name": "Norman Osborn", "occupation": "Founder, Oscorp Industries", "age": 42 }, { "id": 2, "name": "Peter Parker", "occupation": "Freelance Photographer, The Daily Bugle", "age": 20 } ] } ``` To get the next two users, we can send another request, incrementing `offset` by the value of `limit`. ```json { "limit": 2, "offset": 2 } ``` We can repeat this process, incrementing `offset` by the value of `limit`, until we get an empty result. #### Pagination Metadata In the example above, the result contained no metadata, only the items at the requested offset and limit. It may be useful to add metadata to the result. For example, the metadata may specify how many items there are in total, so that the client knows what the greatest offset value can be. ```json { "users": [ ... ] "metadata": { "count": 25 } } ``` #### Using page_number Instead of offset Instead of using `limit` and `offset` as the pagination parameters, it may be more useful to use `page_number` and `page_size`. In such a case, the metadata in the result can be `pages_count`. The client starts the pagination at `page_number` 1, incrementing by 1 each time to get the next page, and ending when `page_size` is reached. This approach may be more in line with what a typical client actually needs when paginating. #### Limitations of Offset-Based Pagination Offset-based pagination has a few limitations: - It is not suitable for large datasets, because we need to access offset + limit number of items from the dataset, before discarding the offset and only returning the requested items. - It doesn't work well in environments where records are frequently added or removed, because in such cases, the page window becomes inconsistent and unreliable. This may result in duplicate items or skipped items across pages. However, it provides a quick way to get started, and works well with small-medium datasets. When your dataset scales, you will need a reliable and consistent way to handle pagination. ### Cursor based pagination Strawberry provides a cursor based pagination implementing the [relay spec](https://relay.dev/docs/guides/graphql-server-specification/). You can read more about it in the [relay](../relay) page. Cursor based pagination, also known as keyset pagination, works by returning a pointer to a specific item in the dataset. On subsequent requests, the server returns results after the given pointer. This method addresses the drawbacks of using offset pagination, but does so by making certain trade offs: - The cursor must be based on a unique, sequential identifier in the given source. - There is no concept of the total number of pages or results in the dataset. - The client can’t jump to a specific page. Let us understand cursor based pagination better, with the example given below. We want to request a list of users, 2 at a time, from the server. We don't know the cursor initially, so we will assign it a null value. ```json { "limit": 2, "cursor": null } ``` The response from the server would be: ```json { "users": [ { "id": 3, "name": "Harold Osborn", "occupation": "President, Oscorp Industries", "age": 19 }, { "id": 4, "name": "Eddie Brock", "occupation": "Journalist, The Eddie Brock Report", "age": 20 } ], "next_cursor": "3" } ``` The next cursor returned by the server can be used to get the next set of users from the server. ```json { "limit": 2, "cursor": "3" } ``` This is an example of forward pagination, but pagination can be done backwards too! ## Implementing pagination in GraphQL Let us look at how we can implement pagination in GraphQL. - [Implementing Offset Pagination](./offset-based.md) - [Implementing Cursor Pagination](./cursor-based.md) - [Implementing the Relay Connection Specification](./connections.md) strawberry-graphql-0.287.0/docs/guides/permissions.md000066400000000000000000000153631511033167500226470ustar00rootroot00000000000000--- title: Permissions --- # Permissions Permissions can be managed using `Permission` classes. A `Permission` class extends `BasePermission` and has a `has_permission` method. It can be added to a field using the `permission_classes` keyword argument. A basic example looks like this: ```python import typing import strawberry from strawberry.permission import BasePermission class IsAuthenticated(BasePermission): message = "User is not authenticated" # This method can also be async! def has_permission( self, source: typing.Any, info: strawberry.Info, **kwargs ) -> bool: return False @strawberry.type class Query: user: str = strawberry.field(permission_classes=[IsAuthenticated]) ``` Your `has_permission` method should check if this request has permission to access the field. Note that the `has_permission` method can also be asynchronous. If the `has_permission` method returns a truthy value then the field access will go ahead. Otherwise, an error will be raised using the `message` class attribute. Take a look at our [Dealing with Errors Guide](/docs/guides/errors) for more information on how errors are handled. ```json { "data": null, "errors": [ { "message": "User is not authenticated" } ] } ``` ## Accessing user information Accessing the current user information to implement your permission checks depends on the web framework you are using. Most frameworks will have a `Request` object where you can either access the current user directly or access headers/cookies/query parameters to authenticate the user. All the Strawberry integrations provide this Request object in the `info.context` object that is accessible in every resolver and in the `has_permission` function. You can find more details about a specific framework integration under the "Integrations" heading in the navigation. In this example we are using `starlette` which uses the [ASGI](/docs/integrations/asgi) integration: ```python import typing from myauth import authenticate_header, authenticate_query_param from starlette.requests import Request from starlette.websockets import WebSocket from strawberry.permission import BasePermission class IsAuthenticated(BasePermission): message = "User is not authenticated" def has_permission( self, source: typing.Any, info: strawberry.Info, **kwargs ) -> bool: request: typing.Union[Request, WebSocket] = info.context["request"] if "Authorization" in request.headers: return authenticate_header(request) if "auth" in request.query_params: return authenticate_query_params(request) return False ``` Here we retrieve the `request` object from the `context` provided by `info`. This object will be either a `Request` or `Websocket` instance from `starlette` (see: [Request docs](https://www.starlette.io/requests/) and [Websocket docs](https://www.starlette.io/websockets/)). In the next step we take either the `Authorization` header or the `auth` query parameter out of the `request` object, depending on which is available. We then pass those on to some authenticate methods we've implemented ourselves. Beyond providing hooks, Authentication is not currently Strawberry's responsibility. You should provide your own helpers to figure out if a request has the permissions you expect. _For more discussion on Authentication see_ _[Issue #830](https://github.com/strawberry-graphql/strawberry/issues/830)._ ## Custom Error Extensions & classes In addition to the message, permissions automatically add pre-defined error extensions to the error, and can use a custom `GraphQLError` class. This can be configured by modifying the `error_class` and `error_extensions` attributes on the `BasePermission` class. Error extensions will be propagated to the response as specified in the [GraphQL spec](https://strawberry.rocks/docs/types/exceptions). ```python import typing from strawberry.permission import BasePermission from your_business_logic import GQLNotImplementedError class IsAuthenticated(BasePermission): message = "User is not authenticated" error_class = GQLNotImplementedError error_extensions = {"code": "UNAUTHORIZED"} def has_permission( self, source: typing.Any, info: strawberry.Info, **kwargs ) -> bool: return False ``` # Advanced Permissions Internally, permissions in strawberry use the `PermissionsExtension` field extension. The following snippet ```python import strawberry @strawberry.type class Query: user: str = strawberry.field(permission_classes=[IsAuthenticated]) ``` is internally equivalent to ```python import strawberry from strawberry.permission import PermissionExtension @strawberry.type class Query: @strawberry.field(extensions=[PermissionExtension(permissions=[IsAuthenticated()])]) def name(self) -> str: return "ABC" ``` Using the new `PermissionExtension` API, permissions support even more features: ## Silent errors In some cases, it is practical to avoid throwing an error when the user has no permission to access the field and instead return `None` or an empty list to the client. To return `None` or `[]` instead of raising an error, the `fail_silently ` keyword argument on `PermissionExtension` can be set to `True`: Note that this will only work if the field returns a type that is nullable or a list, e.g. `Optional[str]` or `List[str]`. ```python import strawberry from strawberry.permission import PermissionExtension, BasePermission from typing import Optional @strawberry.type class Query: @strawberry.field( extensions=[ PermissionExtension(permissions=[IsAuthenticated()], fail_silently=True) ] ) def name(self) -> Optional[str]: return "ABC" ``` Please note than in many cases, defensive programming is a better approach than using `fail_silently`. Clients will no longer be able to distinguish between a permission error and an empty result. Before implementing `fail_silently`, consider if it is possible to use alternative solutions like the `@skip` or `@include` directives to dynamically exclude fields from the query for users without permission. Check the GraphQL documentation for more information on [directives](https://graphql.org/learn/queries/#directives). ## Customizable Error Handling To customize the error handling, the `on_unauthorized` method on the `BasePermission` class can be used. Further changes can be implemented by subclassing the `PermissionExtension` class. ## Schema Directives Permissions will automatically be added as schema directives to the schema. This behavior can be altered by setting the `add_directives` to `False` on `PermissionExtension`, or by setting the `_schema_directive` class attribute of the permission class to a custom directive. strawberry-graphql-0.287.0/docs/guides/query-batching.md000066400000000000000000000067141511033167500232160ustar00rootroot00000000000000--- title: Query Batching --- # Query Batching Query batching is a feature in Strawberry GraphQL that allows clients to send multiple queries, mutations, or a combination of both in a single HTTP request. This can help optimize network usage and improve performance for applications that make frequent GraphQL requests. This document explains how to enable query batching, its configuration options, and how to integrate it into your application with an example using FastAPI. --- ## Enabling Query Batching To enable query batching in Strawberry, you need to configure the `StrawberryConfig` when defining your GraphQL schema. The batching configuration is provided as a typed dictionary. Batching is disabled by default, if no configuration is provided. ### Basic Configuration ```python from strawberry.schema.config import StrawberryConfig config = StrawberryConfig(batching_config={"max_operations": 10}) ``` ### Configuring Maximum Operations To set a limit on the number of operations in a batch request, use the `max_operations` key: ```python from strawberry.schema.config import StrawberryConfig config = StrawberryConfig(batching_config={"max_operations": 5}) ``` When batching is enabled, the server can handle a list of operations (queries/mutations) in a single request and return a list of responses. ## Example Integration with FastAPI Query Batching is supported on all Strawberry GraphQL framework integrations. Below is an example of how to enable query batching in a FastAPI application: ```python import strawberry from fastapi import FastAPI from strawberry.fastapi import GraphQLRouter from strawberry.schema.config import StrawberryConfig @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "Hello World" schema = strawberry.Schema( Query, config=StrawberryConfig(batching_config={"max_operations": 5}), ) graphql_app = GraphQLRouter(schema) app = FastAPI() app.include_router(graphql_app, prefix="/graphql") ``` ### Running the Application 1. Save the code in a file (e.g., `app.py`). 2. Start the FastAPI server: ```bash uvicorn app:app --reload ``` 3. The GraphQL endpoint will be available at `http://127.0.0.1:8000/graphql`. ### Testing Query Batching You can test query batching by sending a single HTTP request with multiple GraphQL operations. For example: #### Request ```bash curl -X POST -H "Content-Type: application/json" \ -d '[{"query": "{ hello }"}, {"query": "{ hello }"}]' \ http://127.0.0.1:8000/graphql ``` #### Response ```json [{ "data": { "hello": "Hello World" } }, { "data": { "hello": "Hello World" } }] ``` ### Error Handling #### Batching Disabled If batching is not enabled in the server configuration and a batch request is sent, the server will respond with a 400 status code and an error message: ```text Batching is not enabled ``` #### Too Many Operations If the number of operations in a batch exceeds the `max_operations` limit, the server will return a 400 status code and an error message: ```text Too many operations ``` ### Limitations #### Multipart Subscriptions Query batching does not support multipart subscriptions. Attempting to batch such operations will result in a 400 error with a relevant message. ### Additional Notes Query batching is particularly useful for clients that need to perform multiple operations simultaneously, reducing the overhead of multiple HTTP requests. Ensure your client library supports query batching before enabling it on the server. strawberry-graphql-0.287.0/docs/guides/relay.md000066400000000000000000000323731511033167500214100ustar00rootroot00000000000000--- title: Relay --- # Relay Guide Until Strawberry 0.268.0 we used to have a `GlobalID` in the GraphQL schema, that has been changed to `ID`. ## What is Relay? The relay spec defines some interfaces that GraphQL servers can follow to allow clients to interact with them in a more efficient way. The spec makes two core assumptions about a GraphQL server: 1. It provides a mechanism for refetching an object 2. It provides a description of how to page through connections. You can read more about the relay spec [here](https://relay.dev/docs/guides/graphql-server-specification/). ### Relay implementation example Suppose we have the following type: ```python @strawberry.type class Fruit: name: str weight: str ``` We want it to have a globally unique ID, a way to retrieve a paginated results list of it and a way to refetch if if necessary. For that, we need to inherit it from the `Node` interface, annotate its attribute that will be used for the `ID` field with `relay.NodeID` and implement its `resolve_nodes` abstract method. ```python import strawberry from strawberry import relay @strawberry.type class Fruit(relay.Node): code: relay.NodeID[int] name: str weight: float @classmethod def resolve_nodes( cls, *, info: strawberry.Info, node_ids: Iterable[str], required: bool = False, ): return [ all_fruits[int(nid)] if required else all_fruits.get(nid) for nid in node_ids ] # In this example, assume we have a dict mapping the fruits code to the Fruit # object itself all_fruits: Dict[int, Fruit] ``` Explaining what we did here: - We annotated `code` using `relay.NodeID[int]`. This makes `code` a [Private](../types/private.md) type, which will not be exposed to the GraphQL API, and also tells the `Node` interface that it should use its value to generate its `id: ID!` for the `Fruit` type. - We also implemented the `resolve_nodes` abstract method. This method is responsible for retrieving the `Fruit` instances given its `id`. Because `code` is our id, `node_ids` will be a list of codes as a string. The `ID` gets generated by getting the base64 encoded version of the string `:`. In the example above, the `Fruit` with a code of `1` would have its `ID` as `base64("Fruit:1")` = `RnJ1aXQ6MQ==` Now we can expose it in the schema for retrieval and pagination like: ```python @strawberry.type class Query: node: relay.Node = relay.node() @relay.connection(relay.ListConnection[Fruit]) def fruits(self) -> Iterable[Fruit]: # This can be a database query, a generator, an async generator, etc return all_fruits.values() ``` This will generate a schema like this: ```graphql interface Node { id: ID! } type PageInfo { hasNextPage: Boolean! hasPreviousPage: Boolean! startCursor: String endCursor: String } type Fruit implements Node { id: ID! name: String! weight: Float! } type FruitEdge { cursor: String! node: Fruit! } type FruitConnection { pageInfo: PageInfo! edges: [FruitEdge!]! } type Query { node(id: ID!): Node! fruits( before: String = null after: String = null first: Int = null last: Int = null ): FruitConnection! } ``` With only that we have a way to query `node` to retrieve any `Node` implemented type in our schema (which includes our `Fruit` type), and also a way to retrieve a list of fruits with pagination. For example, to retrieve a single fruit given its unique ID: ```graphql query { node(id: "") { id ... on Fruit { name weight } } } ``` Or to retrieve the first 10 fruits available: ```graphql query { fruits(first: 10) { pageInfo { firstCursor endCursor hasNextPage hasPreviousPage } edges { # node here is the Fruit type node { id name weight } } } } ``` The connection resolver for `relay.ListConnection` should return one of those: - `List[]` - `Iterator[]` - `Iterable[]` - `AsyncIterator[]` - `AsyncIterable[]` - `Generator[, Any, Any]` - `AsyncGenerator[, Any]` ### The node field As demonstrated above, the `Node` field can be used to retrieve/refetch any object in the schema that implements the `Node` interface. It can be defined in the `Query` objects in 4 ways: - `node: Node`: This will define a field that accepts a `ID!` and returns a `Node` instance. This is the most basic way to define it. - `node: Optional[Node]`: The same as `Node`, but if the given object doesn't exist, it will return `null`. - `node: List[Node]`: This will define a field that accepts `[ID!]!` and returns a list of `Node` instances. They can even be from different types. - `node: List[Optional[Node]]`: The same as `List[Node]`, but the returned list can contain `null` values if the given objects don't exist. ### Max results for connections The implementation of `relay.ListConnection` will limit the number of results to the `relay_max_results` configuration in the [schema's config](../types/schema-configurations.md) (which defaults to `100`). That can also be configured on a per-field basis by passing `max_results` to the `@connection` decorator. For example: ```python @strawerry.type class Query: fruits: ListConnection[Fruit] = relay.connection(max_results=10_000) ``` ### Custom connection pagination The default `relay.Connection` class doesn't implement any pagination logic, and should be used as a base class to implement your own pagination logic. All you need to do is implement the `resolve_connection` classmethod. The integration provides `relay.ListConnection`, which implements a limit/offset approach to paginate the results. This is a basic approach and might be enough for most use cases. `relay.ListConnection` implementes the limit/offset by using slices. That means that you can override what the slice does by customizing the `__getitem__` method of the object returned by your nodes resolver. For example, when working with `Django`, `resolve_nodes` can return a `QuerySet`, meaning that the slice on it will translate to a `LIMIT`/`OFFSET` in the SQL query, which will fetch only the data that is needed from the database. Also note that if that object doesn't have a `__getitem__` attribute, it will use `itertools.islice` to paginate it, meaning that when a generator is being resolved it will only generate as much results as needed for the given pagination, the worst case scenario being the last results needing to be returned. Now, suppose we want to implement a custom cursor-based pagination for our previous example. We can do something like this: ```python import strawberry from strawberry import relay @strawberry.type class FruitCustomPaginationConnection(relay.Connection[Fruit]): @classmethod def resolve_connection( cls, nodes: Iterable[Fruit], *, info: Optional[Info] = None, before: Optional[str] = None, after: Optional[str] = None, first: Optional[int] = None, last: Optional[int] = None, ): # NOTE: This is a showcase implementation and is far from # being optimal performance wise edges_mapping = { relay.to_base64("fruit_name", n.name): relay.Edge( node=n, cursor=relay.to_base64("fruit_name", n.name), ) for n in sorted(nodes, key=lambda f: f.name) } edges = list(edges_mapping.values()) first_edge = edges[0] if edges else None last_edge = edges[-1] if edges else None if after is not None: after_edge_idx = edges.index(edges_mapping[after]) edges = [e for e in edges if edges.index(e) > after_edge_idx] if before is not None: before_edge_idx = edges.index(edges_mapping[before]) edges = [e for e in edges if edges.index(e) < before_edge_idx] if first is not None: edges = edges[:first] if last is not None: edges = edges[-last:] return cls( edges=edges, page_info=strawberry.relay.PageInfo( start_cursor=edges[0].cursor if edges else None, end_cursor=edges[-1].cursor if edges else None, has_previous_page=( first_edge is not None and bool(edges) and edges[0] != first_edge ), has_next_page=( last_edge is not None and bool(edges) and edges[-1] != last_edge ), ), ) @strawberry.type class Query: @relay.connection(FruitCustomPaginationConnection) def fruits(self) -> Iterable[Fruit]: # This can be a database query, a generator, an async generator, etc return all_fruits.values() ``` In the example above we specialized the `FruitCustomPaginationConnection` by inheriting it from `relay.Connection[Fruit]`. We could still keep it generic by inheriting it from `relay.Connection[relay.NodeType]` and then specialize it when defining the field, making it possible to use our custom pagination logic with more than one type. ### Custom connection arguments By default the connection will automatically insert some arguments for it to be able to paginate the results. Those are: - `before`: Returns the items in the list that come before the specified cursor - `after`: Returns the items in the list that come after the " "specified cursor - `first`: Returns the first n items from the list - `last`: Returns the items in the list that come after the " "specified cursor You can still define extra arguments to be used by your own resolver or custom pagination logic. For example, suppose we want to return the pagination of all fruits whose name starts with a given string. We could do that like this: ```python @strawberry.type class Query: @relay.connection(relay.ListConnection[Fruit]) def fruits_with_filter( self, info: strawberry.Info, name_endswith: str, ) -> Iterable[Fruit]: for f in fruits.values(): if f.name.endswith(name_endswith): yield f ``` This will generate a schema like this: ```graphql type Query { fruitsWithFilter( nameEndswith: String! before: String = null after: String = null first: Int = null last: Int = null ): FruitConnection! } ``` ### Convert the node to its proper type when resolving the connection The connection expects that the resolver will return a list of objects that is a subclass of its `NodeType`. But there may be situations where you are resolving something that needs to be converted to the proper type, like an ORM model. In this case you can subclass the `relay.Connection`/`relay.ListConnection` and provide a custom `resolve_node` method to it, which by default returns the node as is. For example: ```python import strawberry from strawberry import relay from db import models @strawberry.type class Fruit(relay.Node): code: relay.NodeID[int] name: str weight: float @strawberry.type class FruitDBConnection(relay.ListConnection[Fruit]): @classmethod def resolve_node(cls, node: FruitDB, *, info: strawberry.Info, **kwargs) -> Fruit: return Fruit( code=node.code, name=node.name, weight=node.weight, ) @strawberry.type class Query: @relay.connection(FruitDBConnection) def fruits_with_filter( self, info: strawberry.Info, name_endswith: str, ) -> Iterable[models.Fruit]: return models.Fruit.objects.filter(name__endswith=name_endswith) ``` The main advantage of this approach instead of converting it inside the custom resolver is that the `Connection` will paginate the `QuerySet` first, which in case of django will make sure that only the paginated results are fetched from the database. After that, the `resolve_node` function will be called for each result to retrieve the correct object for it. We used django for this example, but the same applies to any other other similar use case, like SQLAlchemy, etc. ### The GlobalID type The `GlobalID` type is a special object that contains all the info necessary to identify and retrieve a given object that implements the `Node` interface. It can for example be useful in a mutation, to receive an object and retrieve it in its resolver. For example: ```python @strawberry.type class Mutation: @strawberry.mutation async def update_fruit_weight( self, info: strawberry.Info, id: relay.GlobalID, weight: float, ) -> Fruit: # resolve_node will return an awaitable that returns the Fruit object fruit = await id.resolve_node(info, ensure_type=Fruit) fruit.weight = weight return fruit @strawberry.mutation def update_fruit_weight_sync( self, info: strawberry.Info, id: relay.GlobalID, weight: float, ) -> Fruit: # resolve_node will return the Fruit object fruit = id.resolve_node_sync(info, ensure_type=Fruit) fruit.weight = weight return fruit ``` In the example above, you can also access the type name directly with `id.type_name`, the raw node ID with `id.id`, or even resolve the type itself with `id.resolve_type(info)`. strawberry-graphql-0.287.0/docs/guides/schema-export.md000066400000000000000000000020571511033167500230470ustar00rootroot00000000000000--- title: Schema export --- # Schema export Sometimes IDE plugins and code generation tools require you to provide a GraphQL schema definition. Strawberry provides a command to export your schema definition. The exported schema will be described in the GraphQL schema definition language (SDL). To use the command line tools, you have to ensure Strawberry was installed with `strawberry-graphql[cli]`. You can export your schema using the following command: ```bash strawberry export-schema package.module:schema ``` where `schema` is the name of a Strawberry schema symbol or a callable symbol that returns a Strawberry schema and `package.module` is the qualified name of the module containing the symbol. The symbol name defaults to `schema` if not specified. In order to store the exported schema in a file, pipes or redirection can be utilized: ```bash strawberry export-schema package.module:schema > schema.graphql ``` Alternatively, the `--output` option can be used: ```bash strawberry export-schema package.module:schema --output schema.graphql ``` strawberry-graphql-0.287.0/docs/guides/server.md000066400000000000000000000016461511033167500216010ustar00rootroot00000000000000--- title: Builtin server --- # Builtin server Sometimes you need to quickly prototype an API and don’t really need to use a framework like Flask or Django. Strawberry’s built in server helps with this use case. It allows to quickly have a development server by running the following command: ```shell strawberry dev package.module:schema ``` where `schema` is the name of a Strawberry schema symbol and `package.module` is the qualified name of the module containing the symbol. The symbol name defaults to `schema` if not specified. When running that command you should be able to see a GraphiQL playground at this url [http://localhost:8000/graphql](http://localhost:8000/graphql). ## Automatic reloading Strawberry's built in server automatically reloads when changes to the module containing the `schema` are detected. This way you can spend more time prototyping your API rather than restarting development servers. strawberry-graphql-0.287.0/docs/guides/tools.md000066400000000000000000000026541511033167500214330ustar00rootroot00000000000000--- title: Tools --- # Tools Strawberry provides some utility functions to help you build your GraphQL server. All tools can be imported from `strawberry.tools` --- ### `create_type` Create a Strawberry type from a list of fields. ```python def create_type( name: str, fields: List[StrawberryField], is_input: bool = False, is_interface: bool = False, description: Optional[str] = None, directives: Optional[Sequence[object]] = (), extend: bool = False, ) -> Type: ... ``` Example: ```python import strawberry from strawberry.tools import create_type @strawberry.field def hello(info) -> str: return "World" def get_name(info) -> str: return info.context.user.name my_name = strawberry.field(name="myName", resolver=get_name) Query = create_type("Query", [hello, my_name]) schema = strawberry.Schema(query=Query) ``` ```graphql type Query { hello: String! myName: String! } ``` --- ### `merge_types` Merge multiple Strawberry types into one. Example: ```python import strawberry from strawberry.tools import merge_types @strawberry.type class QueryA: @strawberry.field def perform_a(self) -> str: ... @strawberry.type class QueryB: @strawberry.field def perform_b(self) -> str: ... ComboQuery = merge_types("ComboQuery", (QueryB, QueryA)) ``` ```graphql type ComboQuery { performB: String! performA: String! } ``` strawberry-graphql-0.287.0/docs/images/000077500000000000000000000000001511033167500177275ustar00rootroot00000000000000strawberry-graphql-0.287.0/docs/images/index-query-example.png000066400000000000000000001235001511033167500243410ustar00rootroot00000000000000PNG  IHDR[PLTEjvfsrs_lvdpbnvZg]jnlů}{tnzWd~Ӥݮz؎؂]yv။q<ʙеPsٕܛ7ȂʵЈ.çiڰ*XԈˌl܏e|¸Ҝkνߓ ˜ԯ>c@x66yk/y뭧<ծƷ͉V[kтs{JS|ݨ[ֺɕЖi陓*gԒHAotʩ̫찛eCǥǙdxҔ|GVqaŝvRxOJ(CxdB<9IDATxn0 PyAT֟):G-7pm\|Ηۧ%&8G~MCWypdJR3N'm$>fI~TUi]0%T"z<9q\iQjgމGf t?;3 A3o?-Ark0JA3{=:TmsQ Xdڙ1S;Im t mi{诩u@/irb6NN@|w/MQsNJ !%D\E,!!+V\6\X <<3$Pu:+N&Wo28+)|\t] f< &(eEpLf&oE9Ť¤p<݂bC/J6_B82p49IsiUgRbS_\WA H`Gگ[rY@Q/1DylY$߼ DNp 6Q?ŖFfGFخ̹.J[p<Wd]{]^Uω(&ʿh<Ѵ3OQПn˗eUIkn;8,nPEb}/.Y>}~{V=gYI-\"+ LGzL߂_x|~a.Ct7 2%ЛV:Y"'1,LJ哃[fo˿i9J)$M$Ւ i%*z%ExirMh az^f7FUc8Ĩ&4 3s"AD_^ۻ} rGmLB&EAT[Гe +D,f (aP=X/vyaxvSe& ]I*SE5 Lp\ 406j ^Z,?wPgDߴ,[NS=,[*HYzv !-h.Ibyv ;t8^o<|qwQs?vB;g N8 ul*pt^6@/,ngx,UH)7@ ‰W ̛?E8N\/0b[ܮL9vtgČS^ x__0w?-d28E`@Yj=vҢ$w@@xʜYLBT?p:4 n0F}o32QN|$R S~G}/_ $dST''{g` 1`6 lؕ,'E k$$ E"j7x7ȏk(=IV 6|Bۛ?d>fgv UРpW\1  _gk@hQ(Je$%dKCĸp  I &0[ŜYD,!+BA/)RYB% A@^HTQ|MJ Z%5M*M`$M@_'re0ZA$5H5À4% A@HOle A$5 @7:(V ۜf` VhhAy} A@^MA"+0Z) H Oa [Hj @vK Y%HTARR hmpqk7U;`Y @J " 䲸Z1yϨf3]%ʉ: /Ыbl6Pn+z` p\V^& @Y YTZBAAZ,V.+)@3}=08{u&_xNx rs7S:Y}߿*\6_d`(6 "vE1 n [Rq|/fw @ PgSY7i1^oTv߇y ( ]k(̋ʫsyD)lJ=416P?= ]#7j,j.Qr+% @nTjm; H40}H٧޺w|(Ճ +{3޾ayrDFM5mr܅d5I d s st. ;A&39o 5MIQ" 0gr ྐྵtvl @&`A(F`m7F[;Qӻ_#xӲbղ,]'[T&p @l~:>]v90|s&glW,@9`PE@taLY㹼2@\I,4n1#賞;yl.܏੡luOT{Ezc6ϪʟD:>L/dlsQྵ{ٗ-m\vɪԻ? 6h(U,Dt4G\L ITtyLF/D Qs!vz>0Nt|Ekׄq)}q*E5-F )h& @_+,pKV; CEwou8atBؘ<{ϻMn&.s%YPo.G/_\X .@.8 )IdW 6$# qyZt >zgD=8O=gD=xe }X|3?@W8}}ɜn,ɶeb[%P0 Ow@./SI2{e .wN,ԟ|82_~ck!|Hr͏';wv&gCۈ J/t^^Kzz ȿϿ=>~_=y'(( Id0 sW}]N"I?6%u G ~9~&$wGTNPܯ XŚ 8F@BTA 7b("'[0EU\e`\rw׃#~8NbGe0yxgG%"tg;NKx܈kD}O/. Y>xI3FYy(QN8VcqXvMS-KU \ =H W"M&D m @}+P|~{2%`)}(Hb_N=/驽8Y7[.WSW?XC7O +` V( ^#D@KG&4fnIr?BuO  []Xs*(r~_@,M࿶# pU3{Ys_EXZRDO{t4GvGV`z Z//wIiSMKPpՁx -XV ¶8` T8*x! ~!v{940 _ Ɔ\=>aA׽`Xi5r   8 "@;PRyn~cuP/Jw !ؔ0e*K! {WO/Rp }*D>6l w`l-v/bB6`èSsE' N wMCǓvmvnE49$P,dbnN&LP"=8ԋq ;8^8@oyT|I6m]7s{/zV { rٹ}:ω[3/VD̺ykWkWyX0(8&u @4,:p}@]AxSC )"  y2#I#KXRH1.̥~f͐NଫXP:% .U"+{M Yja-b&4"cIfD6@75DbAQ$evibr81o?<9UKөS @Rʼn 9gvck>(dW̓Ƌㅼ*)nRG7x/o8ipЬ;7wO\ƼU5c-7WO㢒QhEKyr˵ )ϫrgMinº v#+e6r}@:F >+[ɽlCYUeR IU L^EURI^rja%;H0znn.?de`|Xv1n|*6 $ 3(IX r,Jeqi ZxXQ,3bY,OEE* |B,Bva!%g7 ɒ( y(+۟U$p` %ֻ]ZoIu=n|/ LM}Zx٥gj؍ewj08G ߅*NY_CO>#/F@^}a" $/=_Q!gܜJp] vB %|Vz %_|)Ech$2A_a(Ҏ'!D2X(s,3YZ880APA A,.k/1tiu (9@Y \ǯo'ȑ8tavvvcB_P՗>qWz>oFl#i.4cK!# c ed0'k@2fBg} KSzl?[^'rc#yh;Kڪ$ z#6L *c" zl.#MW/2yU";U xEA8p-F>#r_Nc149lF O@o $ȑ9f7cĝF @V <kQ #C󗘚F˧zģoBx5#-2}U! ;`^fsW!bm@Pk;.c Ak ::W?Y8 {j67FNm1cp#V?Kw`oz8 Oí"<l6ˑ=+^/"xjZ$@x{wEp'xh /2C'[\,hAHA) ҹÊ%AH躇0dk-ҧKB$Jb, 04[s[<::cc[8axn`-mgWj~ ;P!EjR'>ݕR{"FF.%r>6o"炫ub 'S@›ެEB'ϫcjp0˴V 68շl=xf]eୄc!9 0 89tAKS̩c^sgkf./0H!>c{SHa~[;WvXJ@''~ K\ 0񬭜Br .p燎ҫjo.)qqdbmdD:;Q x^Ba{6T>75VޭhOV ̥pϔj1!= dIQ>*2@XZ@Ɛ) 骏x`+_Ba9DԄ^]?2@"ydj fh ;WfC(8\v{-)?5y,ܴ[% z~=_rN`w)B _|Zrn]8RĉIn\@Mm9k1!]r9 ;A<,9Y`cTQFV̶fR o4 cnY))M b#f99B:{Mު`ct~q֞=oF8گ,8Q`1qbq֯_^LL+_DMg](ie(Ss9~Ss̒ J(37%Rի+ݡU"-]s炰8IB,n-YpH,7~Jf^e_@rzAQૉegB@oao>5nY%i%N g@E-u3 &S Cc'Y֋dIPDXWG3y7T=/X ʿ*jHѻ5OG~Ȫ90z蚳"?283&goqr~F/9;#}mƽ[;W_q `Թi%J %Yf@30KH΢Q?җ\Ί 8 ˳HmԚ4F '5C<[gYk[Y߽;&0ʴ'Wfa@~9*_*#d$ H'ng6C/9XEz)Ff>.:%WR_͈(H,}Gj"v_;Qs}s@pY,lp5'NgXte'eK{J3tC 7lo(IZM ,)?|Ee$[9D@q`u=] yXg&-*C =,SB 6 m?`mK56zf9kdΜA4ph ܗWAi"~u|g:@DsL9yXӞDudTF#gD04%rfy)h (`CόO:=Ƴl6\fӃ<$4u'J  '۔;e7QPgU &h E &5|^7 UYm)`?$+!:h7|B-A3Nu3iaex'AOz:Y2NY_=$5̣BqZq!]V rxOc!}@H ϴ c b?al` 2[2v9^` rJi. t!x;a@H?0Rq-_\1wB; T. qO*iXyZiB*&3J\֭,u f@H0R9=/9]6g AcwϞ={l@ n(5.v=᷋ɧwކ ƓH0R9MGՠ0E!ݘvH#%neHAX0J  QXH!zv2(HjmޱxZI8IH) BUn@[+dSi w{~YDHf59ai XdB4 ۥMB/ zPXyx&YD$ɘd.`W%]Jz1@(s< ϲ|?`SG].l-e{و%bM7u8!*|'ukح'5Pd,?a<ٚg&*'U&=t5^Y0/F$ L,P@M$ =PFJ^`bX=:ŦGEI%!+cE1'1ؽ0>SŏH;t[0| 42cT>P`gy-Ң|@ex: JɼOPAJ!: {EE I)1pA@<QYeYvǧe+Đ2TH8I餺t>0BH3tm-HGVZL(r([0tDpa}Ty'ຉ4D? htTw,^!@2`6ð\EߨexX2y8/^ qs_=gTawиBH3tW8P RyG'9Ԩ(|Xxz'8,b(`U1;)".&b~l@vŲU֊ꀱòQWRʿp7CO0㜣?RH)s&H,;82XfCad!' Ilc 'ϖ#dxA>4Ş$S1{>\** C nW$Zn@ڡhiL|{(E H Kd ԻrrhΞ400e}z7g<1;sW, I$B$2p&@(T+HS1Pq˅KHKssӯbhnM`jkr8F.U5ݴX\Q͖uTxI{/W]JOu!2Ի \ڕArn.µO$+6D39:[ԒS .w%/ŷ8i@Z 9tPܓJZu;X'B4-ZWYCh_ !8Ɵ?kwٶ\;OJLvJV%?A(%ʁrrpq .$9P\3;|^>;<tE5( Fu}'@)m[n@B o +Ճw`Ἶ҆ZP*ʻ_X!_ 6wk^g3_#gP qt|aH7M XbYW.ͫlWN\?b+`*߿3^=t_2' ;"F$(7s+\kڇ^c1[vۿ+ &nKISʾCw%ֻUЍY'Г7%'j ID64 k)ɂf{MSz7@_z w-p sCuMp wX!*daS;D4_?rkyXm =q6˦;=Wpv  Fe'Uط|IK0Vѿd#fZ%XvMn$?  T5M \e%/[l6;K\8ϫHw\hlw;H+Tv(.,@TM+Jf!VqM[b`6VM] ۑմgw`]1kI?i-jvik*}} iS5`'e5j*`faNN,П ]ӻjuF> `Ue/_fj/.fkC)q.k xBemG$ OFs4ZMkظ&ݮ5cf-avώ5\(4Bh "-.YNV, BJ&6młLhotk 5p@D۠d|}2k-< @H P\@mݏ9ƺG,BuI&S;'ebQ0CX ,EŸ"ˉxb2)S)ZS_Eyg.#z sߠ)^|p8) RA Ҙy") H%_A=7,ǐR@9 Ѓ@CX?Lv)EW_Ub)m6Qc3E>eЋ"($roΜ zXRـ $䋛%(*5ʍ BIIt?n<Xs r<@n{hb(%` b!zRD`5*rnE43+()~a #zSD?{=XfiV8.&Ok'\r<k  W7s@0`OKK\xh$g7slـ 0nr@\_yfdRH̞#W8f6ɔt=ÔnSvȓ{>J) sr:C)9  k /3AtL'\H$p4eId#Gf+ apvk=qc T"V)H Qϥs-Ԗ*؏N`!a@tn;idY0kHedgSxe R$4K@Z:Nɷ gffQCmjuјTcZgp;y3C_}Ƹ#"@ȅIpLp 'o3%&'"9 \<m I 1m,PZTV) @Ârs;q77c$Q±V CGr@ _ѷd|M;pHz@`rUzc`@r־Qr@<'M 7<&$8z1GVz`ghn p.G&I] )Aq ٮ)0 }.aO9g!^V_3.IRUFB"<us @(jF:b^Ɣ9lp.*]d 11])67DTv%2 ܯ7MT-|3/u,3!@ohu>tUE@| l<1 O/R8bkl '.3֡]L)io+H(!й C\ 6߇ߜ۟Le}￑ ;tZ m|XdDMSfKwUdZ+Ibx)*lQZRQK`jr~lTtZ E/6.߷G+X7>xisOb?7ч zuT.G.9 X}gm0!ڟWptCsp8#nlAR@ώwY9)/eZWX,w>/_5 V$ͤ .f^p6GzNE}ˬrGxuxWG F(<6?[H.~| X8ݾ p2Vi>Y'g3y+tu 4PE+TiQmؾ`03O t>Pm-/~ I*ik$$uĨ1<blbٹppeQ(]H 5q|њ([@J 75%v'i'R.V,N.T\-<0 g"mAz!_:X G'`f}8?( hi&7Gu-M!iBM&e(1 b\:0urнCză}ߜ<9GgWT @튫KM hsiFюpXQÉc9 M2t6%hj6͵.()3㆏z5M m@\-g4xG*~$@@'3\~ C@g@@@@=#':*n=[m-x[P6n?^! v@p ]\Dd8_,7yq @* | -T, HF(NZK `oĊSH,VyJ|f.-pw9It+|k\'n5xˀ# @:2Sݴij|H?{mr&Y `GF\pu)*L8ȅrm/:~BcTɱH_}1dJ^W u 1oXR>\r {jy4%`7`+sn~)$#,v-6\ {ˍ̱27[%&LF-UyÏm b8Pyp22oQ C`'EoȪ b;fW3::52 muWڳu6Q|gNP4jZ\y{&.sr!U X&u"UqMue%cF77A5F6cl /Ƅs{׵=[{}ޮ|z2H[Rv7<> B{\(P.u'w/P\>RnfJ(K[_OdX#\nt }X#8a' ŒmA -%-iqx8_ɇ멵>WgLJ )!N$p82(ݶP.T/LÁ56 w68>d>TDVP(*zzoքo#?`eغrgW O9[)e3R_ݠ8.b/*ߣQ(GZ&oȖɞ]"ex3#,@Շ?;Q@Sh!*!5-T -pM/":d V1,C;36ewgbFxN* ,ϵ'wf|]6P޹x~'^` ۫*V NqXZ_0§65|:GG7ψjWj~T_T~6sw߲ϋ`ٖ7J(}mgލ-UYߤ@]P.v ,q푅zt20>A bA-$EQPo[вfvP И99RpĢar9xjl ;*JPj3vABUXL/3cy]*|y"Wk8Ep80, " A ?u($%uyO"fa44^{j G5y.)#KvR u-Ed,S\ɜ0ϐ p^`Hp"!I2hH}AI`+K_ph b `.ֻ'#UUǃƼc sՠO/J>H X5ԼB5,l _IIV @7jGSf4h YMښNBQ\3լ_LanNħfHMK*#1ZyUR{ۂ0NwkZ9Y;qH=z{7U&g:23顦JBktTLjl,o{|LW(*0rI_p`~~]Uv\嫼r8&s.RA޳ʜ?^aG ;K1D&ZX :L. FUZ;6AM-5BÙvÜW](ܤWPEiQ *^4" w$DDE3J $G:0:NԋݭPT$>f᭪{ۜ{7P(x\tfKe@6?P(*`^qxk}՛^+'@\_U 8@aL<8X O5dy9dyn-" p^<ZmZ1Jqx?T<{ٰ,KgѸѰx p{$ $7/ Cu zBIjF~A!CB8Tn!dp`Yg0 @.X8w?@-]NFtG' 4`mT/~sSz41fK`wo"ңZw/LJbqC$'[xsiv /1uLJ\ A,Gѐ<ƛ&1/ R#34_8b(iۊm:@P D r9Yg57% eۥL&j}t.Пk-\٠13-{=xx/bWӜp"TX@ha{`3p>W  f|/+Z CC}pGAv-_1}Xt oW8G׮ 7z[5\@&Qpd\ IxGC;/4"NI^#,qp4#Jį1 4,H I?7 \30.h0 @땀X1;BPX$@A+O|x :ڕ0M"`17ԣ."S1ܒl\!~!+H3 | 9<CWHVF'{KHX̑-FQ& }E)N<|s** E['L/sc&9wă"Z&㑢"ȨZ 4U>SE.;`/]X;Z nPqlC'%⑦lgS({} B`q^Z 9@z0e t[w!%Y! 9uڈ#_;1&"uHFN#P{O,_^BPX%V^e(KW$$ Ga^Jau%@]N" r>[(5+;}^+GK\kI͠7-.P(T?ЄKK/z $[T_8|0j|P{8[x, HG5tJ:/.<"o]Uj%*9-/+P(T.LM<6c_x@309J0kswэS0JJ$Ja l@ d^+ю9 E3F`# ٘'@QC N< yDM~ @AHBVB]Θac# uS' AYFCRd]pTQJ4344$AgS9x~>C+6~m=sC͎[x`* C?Aܾ5gv[Z]{w2Lڌ%_;}{1rƭU 3N+sԿFPM0Z?u˒%3xRK/4.eDNW+ҿ4eo {<$BhOh+ ``,a`_IѤi͡sB;tbo](,$@w? %ZzQ: 62aNPP(*E|ko}u?SU.|\3q=f#og>ԋu0ֻ- xB[a**~{6 "+ܨ" ZR&: eݪIIh6Pdo@w֪C Ba#{OLծadT\xFSOTCRc,'_к< #>'vOf3ֽY;?PUhDbdh *(u:@@zC%x8IIhFlN{Q#m up7̝jqZzL?Z( @ P$ʗf+Ҭ/Pxg|#+y荷/ @tyّ s'wL$WMOX+;LCM5}GjC -Id1}cXL;#1'HWfZ:}k[]Й`DBDmZW x6Rhg =y ^Vɤ.$\d(otO/m=QGF('@B"ܻ up|c5M!ݳ(V{"CoNRGvX+Rﹼ9*=Y.Yk5\\H 5 B0@PTJ-nN[yT+ler?-v`Q|rj2t !g;<.H,pDJÇv,G٨=לSGQ9`ARFqs  2h`z-_jj\Lbm @c4O@d(3ǸTg=zS.nZaD}ⴵc\(  KtJ B*vY/ε)OeNexZmFb(sSRdΕgC2k P҆/Ox,2?IyPW HP( :n' +v"`^/n'0\ؽ0o6 ݝd4醌뉸~'ƪG V)c >1rs˪W2WWu}&"(" RU9*_buڀOTӍ:=KU$#DK(-d\5|@2  2(`+"-ֽ'Jr-k1"]g^= {ɏJ|Xw W6v>J][%pK+K ƂsD?؄)/h'!5ao# @J#XtμR-u=!H<ܼJ;3#^#OѺ깥 ' _DBPX&1Wv 塻9gHF" $rn#<  >Y%Qo\9+DB9 ڇq=jYQ\q '"`uPP(*?ZW*&`<b,bl^!xD.CnR(g oaR)` @AøT}_iCQ?$̏ӐUxYP7V+E9^ r&Ǿcö}$r%~}ٝ];춢33y ǘ  Ph<kSa(ao SݓIF5mU$$)Z4m;q {&˦v?G I -^8ӥ۲iZzTD2wf#o 9탲g/%A E奮o` 4%Ã'/^ ! څ@K(]^URmpU#BZ<.+I4aِfF `}=`?dM#x y5^e%>*(ɧ$ &;m}; d@@ PP15dPpN < _8D %(o d))p,{ hu:- _0"\]-J#$}!HznwQP=B6&JBr[q!WVJm'ȓ h:M' d#W㪔Ӹ}0>nD@:O$|!:ⱀn/_Yƿ8r҈:g`uP'ʻ3dYYdFAū ,_WUp/\C GX)x]aJu oz"*"B 6ľ=C8~y[7Ci`)+loZ7OV喉pZ&V- yUl¯υ~t-Hm1yVl&l~;gCO{ ;\ G N&(bYݗ qf  u@/HA *_=_{40#_ 8(xh@Ut$yÀQCtBPD~kY"a?L֟I'e.-Yi {ŀ ssY (sgpA j?Y*}c3Fp~XuR}9tpC1h0j'iO杷>oQ 현HeDU-{Y-JQ[M|ҬACbY /_ "qy6i l  Q߂ AV8izzתLA@"F F@OSfA@`U_@ԡCjDAxp(g.m@CBN}cGA^7}?O~nJgŊU=rk?w=C#M߯Lk2_%[$>n6t03㦑5_r+bc1|$"ypl;P]Gj] gR۞<0?F9Z<0?*ʙ" `Pt2V ;ٯ:(E"@GB@ \0OX5(øWFYAQ# ʐff͍lBV"HlI)Hg&/`C2lHǝ{h"a@=k@_֯${`{v4wiU@‡#gC^Yy̿%[!ai=xʭ50Cj_{U.= mPeY.P$=m,|Yb%@!q7= -$Gm5ME3A#CrF7vC0? `t SG@ K& _e@C0PC0мXY `t+PC0Ѝ6B `!`t 9j F8f'F``B@,  ͼ M!6(" `D _+"bm"( I- A^r!*pXg ):pIDH& vGe$ -B/ q [V]E uw@[ 8pL; db Tʥdx1+n2 y@y@9 <[LoyR9:G^)N y!@xbUsaE0ز{8,&`1M,_`̦bf /k nA=OOUg@ ~%$h82#@@fȌ@ 3dF 2#@@fȌ@ШX趝]:P#DNY=\(@ "Y/G@bfZƤ^{] @ "?@ b౺Y*9 .ZH@_@P#$sd j蝽@RPޞ,oHBdt6{_ݭNAlR:jyn0$$MiH(|*$CI bH37sS|wf.l w:gY=\B ӷ˫[3!2iYG'~/D `ޫ-D6D pn-!{6@8pH+f*Md@Ѭ%2bՌ_+`KdhѬ  m +CCd@guaB9Щ^K!8pHF P$ #D@a(@"0HF:@Կ3!'# p1@'# p1@'# p1@'# p1@'# p1@'# p1@'# p1@'# choqH@'# p1@'# p1@'# p1@'# p1@'# p1@'# p1@'# p1@'3vaP93D8#@3D8#@3D8#@3D8#@3D8#@3D8#@3D8#@3Du@0kgC b8N Fb8N Fb8N Fb8N Fb8N Fb8N Fb8N Fbu@0kgC N Fb8N Fb8N Fb8N Fb8N Fb8N Fb8N Fb8N Fn ڙ8N Fb8N Fb8N Fb8N Fb8N Fb8N Fb8N Fb8N f  s) g"pF g"pF g"pF g"pF g"pF g"pF g"pF g0vaP# p1@'# p1@'# p1@'# p1@'# p1@'# p1@'# p1@'# 0vaP@'# p1@'# p1@'# p1@'# p1@'# p1@'# p1@'# p1@'#:@Կ3!p1@'# p1@'# p1@'# p1@'# p1@'# p1@'# p1@'# p1@حA[R @3D8#@3D8#@3D8#@3D8#@3D8#@3D8#@3D8#@`  9) Fb8N Fb8N Fb8N Fb8N Fb8N Fb8N Fb8N F`  9)8N Fb8N Fb8N Fb8N Fb8N Fb8N Fb8N Fb8N Fu@0kgC b8N Fb8N Fb8N Fb8N Fb8N Fb8N Fb8N Fb8[4v!@ g"pF g"pF g"pF g"pF g"pF g"pF g"pF حCbM@ OC01@##01@##01@##01@##01@##01@##01@##01<~NiM8²mV0i H=%уh@hɡ/$//`#x)\W @3*O묓1C%.&Ƙ4My61hC!.Jko_^ㇻӲʜog >(:meY%@:{9@…κ?Sk.LA:p ٦xX @iܡw8یrЂ m'kGЂ kC[a%hA Ieq%%%@['ڥ5hA |?9~n޾9@~ w1sŋÆt >";`@pD@0OP qm|f]~=!5@`:/:o_5[P ֤=K BOv2%-@Pi04no5@'xg @ .l^Y߽ۏ@ .lL˜U3zpqf4" @.`lk{E?*c_YM& \?>:J ػc`)([~,l"ZDY !~`uC y^ .o?u3 h#`p;ߝ.Ӷ,˶\>BK00082e @(#PF!B@2e @(#PF!B@2e @(#PF!B@n = g"pF g"pF g"pF g"pF g"pF g"pF g"pF gس{F(+,ka#yARL!E~?^#-4p;d$#  HFA@2@d$#  HFA@2@d$#  HFA@2`u=;}wˡn?!`UԺvjy܏;Y0nwѕٛ[u},$!`Z8esݕ8( 0~ enxr' 0ttZf^ۗC A5 ߧ_ze#  `w%h>7 A<@3ԭF98 \tj0N fVrUu^$" ++R˝[ W ȅ~ lSz5,k-r3mnL~_udk@j r? 59G!xG!xG!xGy8*.C TqT.{h@: ra`(qnd'zRA v4=h w/F*"D@B\ୟ|8x ʺ-:"D@RϞk"4@@Z@ rະWg0fX S} Զz)rCq 2OT76^зm \@B)zlj޵41rKQH L.0ЪT͗<p2_-hE^ml!"EM@* ot?~5j- 9@$:@Կ3!'# p1@'# p1@'# p1@'# p1@'# p1@'# p1@'# p1@'# c\B ( ^zhIcMZ[b$nff|wAd  $AA&((2@@@@I L2PPd1`FrܼBQ'IFe=Aip'˗Ӿb'&RMD@ס˽^A뾼 iP \V$JBW6VM>fS4 (Nܭ>JV͐I i / A@'p@R +y  L&'d^bs.>l"^+]?A/WG]U\ 均 ݲ: usߨ伇rr_ *AƘܩ? Ywa"L(8(+{a%My[Q$1!ء6W rC^$òu#9Z _\@w@HpM_!QzQª W} HD(4(AV'NM~k+odx y%d|a2AjBojŚQ$XQe+f'|U,95ͳ1@CtIK=w2*}ꬺӂ5' 1C~ݣb UNXe7 1a얱m Ҽ yWQ* @j?-̗k;Q$0SRa,'n ♥sHHpOFӋ\B8v]C`ݘv9'c%?o ``]'@FHSN 2L?dT@ଢ଼ 8$m `_l|gbBGy^l%SfzݲqgAA@*v :L? YT)d)|z>?[& wF Ź 7'U@~ɪd +W:i8XfZS섋O/y͜);[Wl}+i2SN,3TÇ) BLyn hz2L5Ayw.r?q4E)>ͧItH^ 4eܫǣe:geX˂Zr4LFf._H1VW (f[ EP 1"t5UayCr ɚlsJ!ҰoT_U!T7=9=ym+ 5/:1*ce[y3<*dGe Bd{ AY*䕾x#|vg}?qo {Vvgxۼ9vdqE8x;o;"p=k#΅7G?FA#ސwS+\AY r 7i( qZNWWOI511%;5^'Hb‡tNzmOz޽o@ IB0!Ĵ0~r- U Z;%ߟuC@06$!`2+!< >WZV #,\h;Ck"'Q}~qOXk!?6xML%y6u` nZgp+xR@} PETHh @*-/ey}!'fZmӻq+"eZ RvJut 6*UK3;{`i1TpeL/:;,z&"`:"=QrtÂx\%`\&hE _`1@w=~y-W4M˷gg&LU (E,\h`8dZpq1?` 슦d9_?&X2vnyPxd p?{#єYL.ļ$`EsG@avG@ M)<&g{O4>OX%>&[| -x,PYP\@+ݥJ߉EDի:kG"3 Lxt //vd^8G y dJ* ʓz9)>;&sr76"u{& 0N OKI\h$;~_kΆEPMTE~ Sf "h8weWxe @gg%bW⇳8,\6}PqT@FSӄ JΩ#-t:EWՋa*r ]-p;`/ri6F4D4H&@%y_~\ ggo`@eӉpMǶ ],`g'i}{T,WL&x軮#5/p?d k, v 2, "hy.ɲrWj'!8!Fkj]߶~Y`8X,8Dp  =&Mc疴"Y>9y 6P o#$Jx4҇l`+j39%/! ẽ2Pʓ.M;3?KP,=U贝[S v>"KPFZy\H3S~Opy'#_>KOr&G]LM |`=~]PUaáIYd,;1JF,e=Cm`|9(pӕ]8f :Fn𹱘`4 l =څ d;{Ii,[we =GȋgcG*f`QD[Ħ,3 }d{#40wDka.i_E)~Gpv5K& =/r@A@̾}X;-fɟ^~p7]% ]OЩI?< sFF'@J9u^Md-4,h=$`4Nk_pe{fRRz1(%`GJ8c*iJxkuV$D't~痱FcFҊ'K+g9GZ'~ԭzT  ! \߳оehj4gO?I X5 OR.AwɺM>[<_;ڗୀEp%&=2\G[K|ف&t}ww pׄpHʱe)>}jd+* <׍,?p"%j" g{T)"Y\X?ȸf{$Z+5##m%75=æ?F=l<7x>ݙۡ7t3zf.WF_w;w8`X_FDN3'i62ā0.p?0 @v"K6&w@NX&fjӽ Fˬc\?6V9!Zѝ0ILwtRiל8%x/,M@M0؜c,b]a9l_I=A݄*B[vk&`63ef_\$Xp@X'j/}s,6[_b鿮g >Xw10ØN-Ckf*,%ڏRIXc$\d/OqUÌd?B9f jr;5Vm+ ը' S bi~4uD".`DU1ӵX -qۮ̦jJӚYp}@&v_FG3 =tSc۞LOp?9iCRYSQ~ÜD]LnC0d ,$ICn)rpp\qϫgFj[1_PVRXp>m On?H`GR٣E4/峅hBTatrx.p(~٧p5|JNx-щOs p%Uw%F?ޒo*AEճ>|BzTQ\="a7hgD#{. MtRS>:dXH'="Lߕʛ99(B1 e8MR~ۮ0x6hc?͗O^*7f}U>"l=k<VIDATxn@ Pt7?n0 c :G Yxm|Ηۧ%&8G~MCWypdJR3N'm$>fI~TUi]0%T"z<9q\iQjgމGf t?;3 A3o?-Ark0JA3{=:TmsQ Xdڙ1S;Im t mi{诩u@/irb6NN@|s7-OA50TE^|C/"Bx$=xP~ngm6/ ʀfǠs#}w97t_a*5CWyADZjq9wkwŕ6۲d)o^\Dž@ĦFLι?rq*.}h.+IYHD(d3Z $di3a:,0۟YrQ̤9,E~] B=Ja)DS~0rff0 ̵ cP:+{ lkd|D(%srfFh2jn~L,:"SBKJ4F)2B)Ji0rsmQ@QYJnZQPHpwB`StfV)YCf(%/%Q /EKĿSt0q pn4m5{YE=T? KGY0`DNgea&f Q晔"cc"K2F)21NME+>%SD%ty֫{Cb#ŘI$MpMO\vKBHn{@(~ӓOǖ'\*b*윛HHlF{IDuZbKpn.Sy?{@>]3߬q9acfJU[Ի@oT''vr FN Aqx|PI֨v[e} L@Cp]@4aEpXC=2U{p<\Հp(n{uJ}@Vn'ȼG)@΂H5Ћ!#hh @p'RɃ,o@pmB lSD\<~x -7P:j+\]^/,}/tg6x"ytB4 @;^KI:atWj6/έm+q'vb2l; - Xo=!Xە&m{T ۾D}2i`N4ɄSI_;ݩE VVI~`2K7@4%X`f!-B86lwow )QNĢR7DMMuh"^lE^fxG/ T@pݻEQ%QN2iB:@  $ Av)\pm o I&{-dFg!*@# Pp9{yBrS5Db=RToQvs}E%X}y8=E8bo6}pּz;aNd YB ZbOKf2C0(C>g!b8iR7=:X:x;/^\\EugqFJ<":8E$%yWunk`97p'VR5 QM2 r0ȸ+X=8#ܾP)s*ʝWW\./s\:0;yM_>]gF.gx~ر3XYDVJ!w=6FR_QGZE Z{heE,a@}G3@JIoGy1w!e)P9ȍty:M9n l" үG8u$ʕ<0~@}`õ11pą.h5\Ww#F zִ!U810rA0n&Ɉ{'!a҇IlDxBL=vLWP82g@]5q o75"}Ѩ'^.hzrϼ~S Oh> {A~^v&5M2q^!'po\')F'$~ɡ0L+")Us x>`Jxk!eR 4^狍{eş -':Rt.XT}4Rs #h(.b::.!6Ӟ}M^+a?3qw:=SnBt? @v ,ʾ@_/olnnm͍-/.P %-)c}Qzo,`|8Nɫ㛘xa/lxV ӈki gw9h~><;8QMT̐EAtm<_76`߯e!@yC  `(?tYvn,ާSi0r D> %IJEP8u"~d}0V=Hu*{7LbTX@* ^oll7u,;Y>!d J]u)Q 9C1opM+e>W ۘN;+nZB?1nk @s `kVkz0/[t~#$ Pnaضcx,"@u-88c|>oo @q%O?˼_85T/)hiu Mr>B=C%}p?K5PiL|QȖ7@9>G(|14)1'P%Mb<B`O@s)l@$! 5͈iuEJ31V9Dtbd3ZZ-n ȍJ!*WA<,MPFy{ @FKؖbeP&vͿWC^O.5.լFRn .D{,7@dM%hd MbkUJ&5+m`` 814o~fT%AR-3yĞ:|3oд ػV0_K+HgRnVBk ^dLc\Fh|2Fc&FbLԸi)2U dZ=Ϝ3C@"S3ZdkpdJ=:8fUAVG'p\ 4;&;q f]yKRDQ#*eq0>Lc2U%6O.@.+N$cZ,E&="d2ttntv!?:.*$l63]"zNFd3@6JN΢k(*)޻$q#miPI/gwK&XE) b+8.hjNQl8(u?r*<:odadb髫88J0&g5!;Y0|@HוW_t 9GHTA\d @r]D<~؞R@b/$:]V탚s'(E\` p8Q& ' K8J%O7_z%ŽJpKxKòIQ)ȕoi(0ec} 9<_v-鵬E=WXI8Y1s[M1CxVDZyw.sr$`)?lV`c#{7QWLg^4 Cς >Lj qd +[ rhP),S P+I" uȾ N >ٷ#ٝokU}¯+P.Joj`/K|S᫻-dcCZDdǭ^N^>]zӷNj^Hn<6@E@4 O.@CLhӌȘo4DMSG&m ˉmf12X;Ya1] F\`+KJ/ܼ 932"YH`,p1  ~~=?CA 7;@( 0q@y>Z];y}\v={>((:? @ja>F0=8>򳵵O(QT~)E} CUs88`݇do/vGol3"9 36B4R<JJpd@Nc؇0x>Ha?FAGH^ט)ӡHiE5VQ`#\ei8nmK0>Y} T* aпpհ I3>~7C㣱c ʳ'vfbjA/m+2Vm6P{7ulWv 6e g/M˱ H@z6 P{! E%1"pJѻ*nMtW6cit"l:ë.n8(]V˲Gd>k=2<ĉjU%?D 3*@(T)$hP>RUM }89pSV< |<:?D!Khcۍ}X4Rm#'U@H84" [Eg0N/**1$d?@Q7#v\m/x =A 9>$~vb$?N@dyW}=~2 PI WVVW811pj$ hPr%o> $<}w"Էnyq:=[eY6Y}TBC13{Ksyl\:z^t\3 Y=^YL2qB*V+T,{,io# rt>g3Ce2|T-3lf]? m_}e HLv_h*Sђ4 VL <tՑcl k,Um@jX[Pr}XytᴍTllyy%뜀8Y:汳W--] #Nll ;nتXt,+:;yc9beIX$@gowkT&?nQJ8?՚F~7ݾ/4(jhu^=u lϢB*ǔ4 cdWBb]SAՃp9]8OxU$5B\Z=--{Q4eѳt5,XF[w7p {E[F^B }vִq{j.U/΅p:d'$9' pE71 7VU @tt -ѯ׷\ĸHDS9m I(J,<Jsnnv%?HB=nVCb@ϹR+,oKi<ՕyfMxb? *tآcy xd% 0}ggB@Ow!|4Nx (+2 ! TL 䤟ܪ#x"o)@>?"WBTw)#Wx3^9d @}tZ$.=ژs%]g|VgTO|\ٌi 9h1@ⵡ2CF`sk̸V0i&lŬϫ: 'Nɋ@|\ i툤6cQpqT ,9 S%tM tjx b}=F@5.T'N6B\{?f/~P@$@~~ J)I '.܍GZ3)xzr[}:Nt6mXžK4Pu/yE| 7r^X؃xe2'A3 k/91xkY30'RǗCDM1%wu 27wi ΉY[)U{ s; qCT v>D ,WwhG,MI a4(*pݽ6FVVڙ6EZy+F2RV)"Jʅ0ՒI &7Kv/pΜY99s\͙9s|ϙ3[4hUqboz9vBz/"ztxm=A%@޶i0ҌUa}CesB=2%RLm(blxeH}1yўXّ9r>epG&m7JJ5̜#ۖ,?%J%z7T4e8Jd/{usKIe@ꫠ% "0 .T+%zח4# 4"dDPb'0Y[ KD^~Mf;̰@x#oKkQ7ޯTNz"IVUSV6* VlQumeѩQA\D] \_⦟CMi4yMf59̜{5<z}+|Nߊj 2!ʛH^𫁁؂i 9)^zq=7k s{eA~JI8>HaV7B;s2rB(5fl>Իv5uC2[.fE%}'@+^Ud^_Lׁ z濯( h{@R$kŏH'ɰMIL"̴ o& )86u?rTv3詺iNWljM=2!߉vPg|I[Ҙ?.]\N1|7o [::tGlfdb~{D{(J?O3dN\QY( UxFCD(F4((% ;Ãod$Fh* @F,;Exb[mq`GR{)+WA9̿bGI1gӸ@ؓRx ʉʬ9\5+eM\وmdz)zRVU||ّ r=(f8Ls%`vX]ɩAx]n1縪 dz(T(((#3;kҙ$'@X@|Fj\Do kC8L ~9Қ$+N8)%D I@N{N8WV"JE@x~cIU$ gb:c M2$v'N83vj0seu׵gPt#`qZZT@PA ,#LZVR!a# G@X@~08.5QAC8DuޝRp"P1VEGQX43wBiw,Uq&ѸMƸ=Lqlg hўe9379_.\q SX0wc`0Ձ\q=<ұ`*aK ;a .'tKFX0L`0We =>bֵ`0U\qxS.,z`+G\.? 3,0,MXֲQb".8Gfp92ރKn/?Nd63*z*\q&=wP548->B=:PYi( D3GfY'/aZP-X0W 7OG%AMZle5qTR D?98'|"*5 YYGqp&>80tvP5X0WN?\WQUnfv3Z7%{=)+n'(񲩡H8U9<}ԇS)3H|v%Sk8ax|5/MK!,nX0LU^G%z}^o石O7enim~C]vS|hnDwl1`drS#n[a֖]p"'Ԩ,7ZdViܳH %yk ,LA1fIl6?|=Hd##=Q# P:d(?co`b+Pk xhPk|k gIZ}MMf$[5aVp:6ƙy4ovo؅Rڧ{3Pp{mCN3/ FMd,4p(eN}5%urHO}7vM9xqC +"$jM"Gnͷ)c '$ ,TQ<+ռ0dAF QwifϺꎛ 5Q"M17U! ^nm_Aέ@$7Y% gzH( S7Bҝ[S&HC}ȍj'hٕ,Д I2qvW&pr.p#"$M{i;~ ɮPAZQ+yfЄ\dNjzRm;u:$  7R *MS*-&5k3L ꆉAV )G<4qKqIooMZX0Lxx+l MC2hBi.1až=VS>!QX-\*D>o@Q8 ;LP $J@j'n945_=o\E-QC'q{Ա Fay ҫq.%$Sj 4m;qn&3ʓNH IZX%im!_$cq1GJF)hw(T^~OhXuĵJ-Na$>0gT,HDH^(:"#X 58Il W#o&oT26 f4)|h }0 Hͧ@#|=3=ŁT#d`}`>:t*uq8>7 *vUM#gm!Y38g kK-g衆]H&+Y7 @Q: \K]NFL|:*t@ivPW4ƕu8\dNjnPWߠFN;mBoo+8'Gh6 NW7BJ@I2Kӷj]bz Q!Y#~-6rI\X%Sp]8ɔ@Cmu8 QsS @# d(^ك$)hI zX\۪A)S>`ߴ-z9 F2'&#zmq{Y0LuϠ2CB@2ⴰng͈kN)5i${medwr]yFm_DMD (8 ΧOΔ6궣 M 8y#-ƄsM^T9bx"ݔBw9aPBi0 ("^Ɵzv&, \!QMm)bx;ZѰ`* 6WP={UbCtV{J(NЅVO:1D}e -03n1&6Yi3H/6ԃ"sFHq/ؔ{H6o#[8SjyJiywwݮf uCG>bP)GD̯zsWfl$Jp n"]a+.HA($:UҟUZ Xs`j |kU֮ygaza/4 ]'`'drGUX$^atYA);_(]AzH N@X@r0 _ShW>L;Fh + 4bآ2gk1F_O@.<Yb2YY*2.LJxm4^ phjva_*:b1eYL񁐚@61BhNȦ)س Keq0K W:dJ=hټ}8i.,ȯع`3V|x%؎< ;$ V׸w > 51$n_h]` O* ΀=᳌ltw!wU|غw63jN.hq 뱛ܼO H<;}ԐDuo=@2|cY( fZ97Jgmo7$ņ—R͝ b OHR[M7o>JmPhXSf fnaMi XEB22):!-_;i^lA {nQ:}kE@ <y@gP7t(o'&r&jd10Ģ(\|aqSOmvp dVRzr9KiLyVJxjJm!&L e_Le{ZH^ iVzj5-r=n"4-.M+۴wAb!&[pZ@)\n㤽=2[yi3ʼn7ys qEŌ]xxizbTD CŽ+++HmQLѡ}v}~ܞ&ܳnם3\jt?54Jw.Buϖݜ\u-ėJ2.]RRgZ,; $b(GcR$t rX`f{M˱z R93B!GZK r?l oL+fl)MK)"2>  1pBK%j eR;0,3܈O۟Zʂz܃/2Ym[km[\RM0܈@LHUqR@ǘ!+17+ո r%Q[Kn<.X5- "wbpB)̬WD jo~|!{#ۤ@F "*}؋ +5 r#26?#e@F "*;)0b`tjiIo⛜RgD. "'] |* r!R8+RWD. "P?Vh eQ "b౥\ ㊰0M[DD. "T4[U0;2`,ՕX-öd:#%}܈ @k&[N\M3F#+E@F P oD=N܍~6 ?]MJfHjqE4`D,2ܨ]7рUd7ft^`Ю8^nh0ͺ;PueӋ@S-N¸t2CH9F_R[x5@"Wjx{P>٩5)Ե̱a ŏ|+ז|SO@Pwpwi(wԂBYKSK@Jm[шK3;PWg*4,K}{ P^7}^Ȳ1k h[Ƌfc;IO(ݙY[:g&l&bsڈ -6k9\YF y=`YqIm[ZJH39ř+34[l->;^\u7iCi۴ Pغ-@~i=dP)#u?/[!?_P}z0ȧ6ja\m }ͤA`D9sR6?@޶.ݳ"i~ R0dVhOY)FC-l;n41#g^B9' |Nn ;\p{^ՉCSy. pӠMjF!Ltw2ƕ}ȌjCcH:PRz8]i +mP8nwnwf L`0U&@Es9 93`fZP.IR/" ۠:Hvwh%ζCD\ySE/+r<߆I4JshC蜠'~eHAzAK6Yf#I$9-vׄ`')}Iv{ =TISp0˥ ӯ8bԒb+@HL䆠0 fKA~ق I=.ܫH=trj`*WkC(ۙ16ZPƒ`# ͋\uFVG7Gb v6c!3&I֢ ^{5n) 7o '=@,d!I`W fEj4 `8r=7paz 2 D;!}mM",D|ʻoA΀Gl d) f\#Wln.k`o= I)(m)\eC%!޿'N(I_f$m R2\!asqwz)a: ̀>,B<,!ÐeG4&MiΩ #PӉqEM. ~{_Ƕ $Ge.kY vm.?lysPLRКNhM{֡zs9zxLy-ޮ%>,B|?6+#r no/?> PMѠ2ݺhU N aEzRxxeʂ z=N Vh( PA$2.BhEE 29!)hda>4z+ja\ ӟ `Z8P'Pv䄹_@ [0vJA>}d2%W`[Z MBu_յ (7y+T`XH 2-t]]En0UT'{q->F>a <睷AqK8i+`Cm )w4G;|jNb` 5./Dno0&=.qb50UT/F =m^[֍e@sƓ]YԤn0>B螿ڹýN["Do="ڠ9* qzW?Dw04IB`.fw5/aYOAc\_(OT Sj ~~id(* ׻qFB.R*:yg.3_%(n+#z,!`M2(㷯 SDbjlqX`@hA\-6BaBqS5X0LvoWk?37LWpLN/ JXiߟ -\[mYP>.q E`CtuBe,3£X`<x= c 2,@\{:(dIg=.\:;P\f8ƽ~g5mO8z h\Xf{hHrP&Q%Fn oC%0b05ʾ `Ý`:z Fj5E],'~.tA nB%]ZE,Zl΍Q:)c52Dc֣$57i"ZD^/-rin۠ڿ}ȋy,(&=pl=0ڽ?ŌH½ld7<~{yy>^\5=Sqq-Yj'mףulSg "%뭺5QMkew-`*aj b #]G'2#A6(M7Bf:ד t>5 CgnCn##,ڭ̎WFc҇6xkY 3N;Fh|AI3іSBڴܼAP4طDr-w+,Gs$^x$eu)FOp2gT;;SzvQwg:!kX9\<˳ O )5/W(z3%"ИR:2 ~t%"qjJg{ML& F8jF[f$J߻˄_96#\hhy%hwqR=#3%q?1xP39tJXJJK@|\Z郯_d8>4AQ?R:ٍG/JuAs*O9_v -??^G̕ "`9r! @ "14eg"MbalioF%@ "1jf7;xNQ 4;0H㗨$Ic I%p@ "{~ex]WP#Dm}5j&yy5D7 G Qk0(kƆ]m "`9? xr {0:rUߝy=2 "xl Z [jO~u6dgOD `r[VIIh'`QXn1ccx'';elMܝcRAD "ع}\GFpSTd̐AD "C3VؙY<85"bkb@y-(bO@$q䐼5 @$w٥?Z(1] "\SHo,P10LvdZoPO z|rѺ@D '{$fDt@$wV:\]<9BA3.<}dy}z\7.M.eC8DkSs9\NM2DIS]׆BP*=8D֐0u,,zN 2dcIZ߱hY種!ki 6mW61]?2[ƬlB7D1뵐RQh`--|uj@tm ^]mGEK"zdH<62ڞ|'PxpG\|kR'F~TϩD3 "?5߾)}X hyv@5f(>w-Eq?-:ۃ:::])?n!0?ů#HD?r-]ݥu<ߛ}σsH=VE$㳅h4zU^H,@4t`O[Տ9y@Bs3HuzU" "14fP\X2#H3 lܸ"ߘ!yXLfK D@z2q R䁂fƅe3H= "5YJ@oQ:A~e=+ePÊǾd)2!Ԯ߂?7wy b'Pkq wV\|+Ac(@9{D%~J|^={BӁn)QnFqϣ`-Ԯ݂2gQ㽻.dž#ΎC}h =¼ n' "ηхxUsrѾX~Ko3}q=ƴټڊ:HN|oHgɱ q,5r$j#:G'~1`oSi4̠D"|Iݻ+T++ێxr}sعt__uW4~m ZcGJQ!qiD")57``X\0cX+Lv ׼{f3pAH9- sR(`"?`fl{/d9݈ٮ` iO}cz8Ŋb<Œ2:]YZJ)P(BYbP(ւZ$nqI4.Ѹ5Q1z;}AZvڢT{>7O23!L좟@QG_8~ܔݴ^wc:Sd-+Hn݁cd9g0f54ٍ֠#'] Js1v]!K<(<]lK:PUa/*讓PK$~ix&NICKg X,m1|چIc!7qK8UΈ1v䡴6ԏHu5o< i$qeOa;bĝb6`ʈM-Z;!1ySkarBd#N@QqaߜcVnJҢuov(WC8]HS TCLB}^i$ʉq~P# o2@33͆<ǁ1q]l72bnnD!kd q3$NdRYvx.d=s$Pii}`~0 +.: U}mԂ;bFkWI[vm oK\`IBEdVip$r"PTŠ'U w|>xh3l!;%[!3=rP 'E?k,P>+6A 3).{?Cn!{E[ mPhPT+ⅱ8>EYS^rkpve,AGO>~E2 $CN-w F/@'p%pO\u|2dp' BQ}OlT f"NN.`c)8?i|@`sa>mmXތAҹEהC ><%O%`g@,I@ 6(BdzmNjlK 5~ n&!T;H^Y;,x5AVe~ 9kd, zGM%thk2c) D {؈f/pznp-WUSKHƪߌy٪  (1]su=[!Ϗs@Ӛ\ar~P6yfm'&fF,U >zg/u`k !^zx~u rP _h+{x-2 taHU.`} س( D^&AgZn]E0/hcK(y<5Ί]寧Y[D02p9? YD-d)_`O B9ȭꀌxp A<@~G9Pخ!mY@5 kmuȯ~N1>3P8znQ!*Gn׏$^d# PĉV QއN F@GS3}FHK [=QSu dW4}Sܸ qOawv"#훩fF&ww-\Muy͛`iԢ؅ S8 >Q]B|!WW8bb ! Kʏ왁qeL4})IfEWYLAª5І`kF!APU6$Ck(6_"Kc(bc?lVgnmnnmmU4텎ܤ)JZ*PzhPT+I65(?E8WꌷIhPTpzS(Uh\p% @Q4L\L"["Q/~aEJ +SliXuڦ"xq7EEhPT@QEEhPT@QEEhPT@QEoO+U_ TŖOˌ2b+ KK^RJeq)[,{5FKqk4c>|Qg+ʔ%(po2w:s(JFEhPC( bhP%@QW d4(ꊡ@QE]14(+EQ2u(JF͇Fuk $7[25mv݅KE]r4.G'7=3Bk"f`nk3oyQ%wA.+u9[flpEF#y89vlxs`ld/2#F\Qqc:aE./j,x d˹*MoEs`zxuB Pc Y 19 unz.sc}Ph67m]}5#gooG}tn۟*m,SfTFvC}f[rEn1A㐆@S{ԙ"Mbs"6I}8l[s *smI] |6,%/vco4N{wjlviPl_p6BhbhRF#^: gPi'?rcG7>zd[]" k؅8!ّx1qwv'I#RKɻyH$*9B1~ |"Ri:div‹J F@ } ̃>f  P/%TTLR7*݌s2\%*&cX".0Py·AHx9Dy!`!VBNy~R`ʆp[a2P=*W(qgmhΟp)A ݃8I \+jZهڬ$[n&j?RVL&!R8 \)d"9H{X-Ci> T IV +}Cvk0R{n2 oKI!aTɛ@T/W^Fm瘘4!x -r7b|deB\ij (ssrljbBZ4 :?̧*d P|ElC,x;,qt7T0dnmeVbAE ZIwj#ې,I#dz;Yµid+Ǡ:"&"`4HftJ Ü@-Bb]W17'77Ԏ9fNyf:;dp(xN;~|𧛡"rn[0 'z$:L!{-Y!3Lm\Xg;-l]vqoҪdb]3sˠfMS]'YȊCb'd1Mf)#̌ _eC.|19ZuWd!脾Kf75H[dI} j_;AJkZpbV] _l͐qPo@6OZL2zI0x9x>lWQAcGs-]2@Dž H2Hi>ܣ,k\ir,gw ϫ Y<-Cclnؒb.c6()wr)j;$'dM;w1߫&dh"#>bLDt @Γ=on1"< aՎK.u]3Z6F!)z7G "!5؊ WP CuL)Q&B1Ťo$6 [BE5 !jY9tu4rP$)Jev-lC u*C3dklg; r0~(= G~[<Bȡ,:eKm-1x{rfnnna$1 ظE; Zl]DEQ4-{psl,B K#> 6Tj;δÒ`Oj'j ,7(3E |I,t`_2e4xh>T?S~Rh**~&cy6y'ѹ h !>eywV@uB=@EfQ V %,= 66#+{l75[kb{]hPE@'Z6&$'+*獅>ȫxrl\ŭ;<F䖶q2UqfZ(Hw0l`yMm/NV$09b[0Bf>^]-D  )v2R<$ q␴`8u~JE4p.~s7[  N}k}{=T1`iPEn80q~)< ɛlfcU#|Becxέ'|g#~ȮE&c+jb}Nc^˻׍spx`QOf [Dx<]q 0uʹp$ej`V G'w7;Z$lLdr4s, !(@KwC95Vi*# ~jtBAzD V/#jASBwSHcط 鎀a33\s/cl:YWOAsPr "׆U= 4eacu@Jf峙pBY ecܰEy.2, h hѣx<N䂌v!C,ԍB{?Ly1Ȭ0FnB!V޼U $cG#8/4 -6i,sYQ2 !7;.IBaS2yCK4nrlEV(Yclw? hx>L/--%K#Ư~!aC˫ּ]>j(  ×ly30@cXL{{M wQ@q)=-AD]NE֘ND\f08m. hPE߿Ջh v@4x,`x`{lPowCfs A~dwp‹E(\Y;l ?3lp0Iل"$3G=upC:pM`[,V`yjF'cX 3 hPE}?a7 E|O2`? ➂l ٠Wl nl?8养bϽ]G F5aᗴ@o]ZRte@X$%99?4e<vpO.VI'yiBZ=pjSD:g`q{hsވ0 -& 3h)nE=^ &Sީ@@Y/M(nphݨY1<{KݎIjenq1O^;-ĝLZ(u˄vg?x] U.@1ÎtNA᤯6̉EotX6h8 ٖ'#ìI.}ñͅWRl0O :n֍ .B"{ѿy}k/eÒnAi5'nFZtfJR_5cu3N5&pv{oz؈|x,- l!@ͽ<'͍MuzƝj:{t_ٻޤjDiD-:4PYwp687nC38Q;M4ㅗFj{  Vp(Oh&#cTßMQUNw^8l+:-xC7*Bov&:R)G:pBCȚpM܎ΰ(ڂ;E11,zS}Ԁ}}ѓBʀmnQ<, c(Cwjݎw\ IӢ}Y;lHjb̘伻Lw;)3̑^AtУ̃{̃'7!#1n?Zgĝng:~Vn(N#gL J܍ܚA!8([!|cQ̧y0*ܢ#p3(*;UEխN-ڛ]t>DŽ mݓy c9GP3=J)Or@Ԑ4?ͽE5F _AeP*ۀ]ʱo= Y}q|Sn}1eOD]^d _Bή yh_ySa__1no)Gu`^g)_42q Y&W rrRb53:pEyR%*SWrxB^_ESCɾhx'㑉k(=eLnFH~ZgQp-%? 5#)?7&3( #ayrZ.w3<Ɵ\8}Q c&ۑe.y$))+EdIr2&t:OZtti 9FTдZt'ZPJ߹ c@YƵ}ZtzEzT־ʀ_1-jW] ̼шZ6W6Q7Iv:[RR\S )! J PWz$"%1b *`qR[Ylf5@͘6]#Yo!'vMVDP3;(2ePm:%yjge0jc(q22fQmQWgd0jǤ`Q PaD "1@D "F{:HsDrqyD 2KJ*0l%ɧ.24@a:ID Ұ)@ag\ N^DZ Ұ| rrSIe@xDVeb+~drI Ұh2h5t@xDV]Rbh,yN@xߊ!jaxhxj}h%;6zPcIJR\1ǬЙQj6Ԗ^]g@5lm<j69_]bͺ,~[+?ʰgQ9۞cd#¶5Pc`,+wT@ԈD"!%:iu;+-njF/l:}fBy7a29ngJ-171Kݎ*mvuo.3Pg]/6`5$mϡb ćN[ڢIm+Qs9ǚPFP^a[a#{}A;Y}^ajTP@m; Ai"1w{,1 D,@UQV,A5sÏ6 +кZ.QTDX'<.;,Д{ @P̠% @Y|P:ZU00w>^!)s#-h0@6pfSXV,b0 {wJqR(ԢJxeb-K RBe#*D1hLqR⒨={K)gzNo|;3W+ѯxgDe.E\LzE_-6cPx~@cճ0Z C󹟉qH̅ollݦ́PfjXͭNzMiϥlwk,e`5(1-}SBi E4_ڵ@F-(b64(B-j`24JMB]Wc4>:H"~Ϡct{WywkqHT2$s(`?:v ʢ҂_Hviaʎ|Y4^|~!1nr:;i}PްZS}z6(M+b(,j*fg}n2adۻwE0]@QsUl@}SDpfo&L@bzԿr]\Ic"d A[Vnתw1CB_l8M0nc"3G&s0#c q{alvgZ]s*53dA6ů4C7q86?W į_zoPUa{<( ŘeSe) GH 0*{fQb0d bR@8n(@ ";N"lA3Fkݡ_r7h ;4Lv|ya g:0d>uyGH܎8KB΅WEj Cg[$7k"0%\EEZ)|c&3,\V}| @~"I8*$7&h0s iP\5ϾJQH|-JIhɷ8.vlP<3l@#e`bCB%P,1/C3 lҤ@\5D*O` Sw gs.(;o̖@^4K{ipv:uC3z~Yv}ۉoM[Rg3Ày%ʶ3(t3giM( ̖cOwYқۜ,xxzlxWtP yw`U$oBBdwlђ$P6ؠ>Iv|d^ߪ!߬R w!+{ޒ0sgFTXpK R*$riKL 惾EvP_}lf4ǻe@Qw>uWF[޷Fef#B %:ܓ ~s}и<FW]qipxIVc@fקo}?bAg#$۠Bg7/@!1!,.t0 ˜4a|Y?ku5 @VW 07e:Y6|sOVi>J#L ;S6T^KkB`s]57M1; `'ٝ3y[͆muԓKY6&F&BGHQZORS/8Np}`C = %Cd+M 5ƽMBXh{+ 'If @0 %@e %&D $0!8: f$i>yYאH תg޷+h;9 ݳ=+J`ƱA淯k,w-|]S iGAP@v_4 .9ukpҗ>x`&#Tx yEg͆gti+peƟ>'wP.p;XfKu],;mZ yc,0iqM:Cnr kd q\`ȯ-O$bG 2s&Yߝz 1>:Ƕi ދ> /5*qO֜=\N*=(aVE! jٝn<J? d,B- t^=6"IȖ k)lFjs^-F:Ʉ#YΫqvzaᤂ8`(2-SKpRpcy[CUF1丝OiPg>[mQQN&q.M]/q-`ι^6@7&lVKڦvBnhW$+YɹKP˹7V5GFucw45Nؾ`c8+.~90hC >hi@g_lcf(=-XafqGWhyRtK=v"t|U4 ʴ4{8ikte7FtEQss&*=p1{$GفIѹh5Kd dЩ\X`_V3 ńs 뎸?оug9嬐5M8'8m&rm,gơ00|qzMZ_~_<-^645"\u-CZȓó(A2itߏEUCF_*7;1֢ඉC<ό(hfS͐نMlH(0Xȑ<ܛDto" G]GPv(U~bJjHgCDkY|ɬ4X;sf |D?@AD>nyao$zݞ8V7T&Cd:m$!3NeGqD2fh+uQ -9NTy#;}I:Bh+4fbx XH[pfn )zڡ'kb95윖>$I'c>l+%?Y3h7Pxe:!3QpM32܀?Wv#(:\9X9u&j ȖAÆlvgӠJ@QUes.hI?;T 4(Hln6iY$k1H4lg .F _zq:dƟwd\,vbRhPT5@3/'|f47M9rGU *4k;U *4(*C( 24(*C( 24(*C( ػ0?3P`0/H-/C ڀ8-Ebe)L k~:_8p G!18#|Cc>p Gq#~+LS ljX6]v(] էKjBO7$_ .6Tȓ05%\\PTRjSM 8KĶ|xc2:-)-X%`!~g㹱[i7]0ձĸ D?~( lc A6x}_gs~ٻT !FvdrGEB-@r ~N&ud)bfju:Tra cZŗ9+f@y;x~`܉Wwz\{ei$MZ­۔խgW{͹\0섵VN/ |@r(by͝Xzz=ϡqe{w>q :59Z v`0mc#DD&+JAMQM}\n0G9StX!ݢ bRnWhUd*N];VPl'C&!LDI7ۡi5C_&e-eak1јt 5῀\r? wշ>~w:xG8NHNK>${+v/M&a:8>^k2GE>28xA+vȎ񈘮ɼjƈln)'\ Tס,˂~W[PO Idn+0Wr̾! pGcA^06heAyE\ۨ@r\rW >~W'?JEƚDiNm ϺA@voDnP>Ik"E:f~F Tf45ƹ󓪰 Ư@?iG~B%دtQ_5 (@e%v~ԒC:mK ~@׹S- Měλ`T ]<h=Nʻa qGYFnK,m('x/.? s]K |˨HH"-߈G[rynt.SzDgotNM9 B;T5bT&1J@S)~0 r(hx^ $ys @MsR 8_"=zP.N)Dԯ`߁dh-n4(+Pc#3(A Ae"9h >~3 hwD@ 9\~,撻2ӯ&dסUDt:\W1.Γto^ `z{'5j_9ˍ{b88_.bPwBc1.(ffu8ƥAKpEC>@]&_Fi jePڂ`hpZ _Gު,ㆀ:ޞ8FP%>ˏ\rW%ye\ -nCCo8v)na&E5t̯@]On! C桲6d5䵓N(tԷrͿm ` :{%zW::4@1I((#@uU.~A 8FCI`_Uæ0,.oE'F, UaRp"t׌YƆU~kdT=B3TViqЏ ӹwPfP *`R}fP s ?@8Y:fofxrP"jiW5͙oD9U=/kۦ@aRe rjs36k _,n [٫ga*W30 M 49Ew{4dVP}w {7 {G4s `3cȂjb [\BƻZ1*:vwoqt$w`H @۫\ g~jIeNIU:],w֮.[شTǞ񅗵:WCgΡNs/6X* *Za |d 0LPZՌwVX#i ''ꌃS; k얻q݄Sw"9J:oj8g3R2\X`J\=9w5^[&-uMzrd $]jjUC@;<* up@B/y=rwr&J@CsvPEt|qB)6q *Ff:&n,,*s_đOQξh)I[ u!P/J)$ "vAE5&8@ݠ$>qXL-(l-tmit JLןP1^ Af#8%H KS{zPg9' BMe iqa @[o=H֡1`IݠZW#祁'WKEi{hgj~a0QJc`0̊DVV2%PU1-2k'M%[~%`wzhE?4kM1b2?\_m\:PuFe '>}T7w~6;͈kn$ hu;e0b׺b/のk#T&H3h@Y;"13&QA:i 0{Ɵe(SJH 9k:KbeF|nDޘO7]bVi/#B>.: &qv(oz(L;w5KA Aߘk4ۂ07%H'38m6ܞfh*,};t0:yy}@=u}( x؋38eI T!+Q`^LnP ňy*sӵZs2Bv(Z8ycc%0ˎc{UԤQ2g=vhjuqX=jkj{}fe-R?-+a`@6h@cew o!SuA^4eAqPoI"_hGy ayiM2z|{ni#+&~-8!+l݁V4"o 5NN e(O9j(yF..? so,x~DǽTs ngiυm+w1AMrw\&k/>jegmj,37rCuԌrZbN.!m* A0JC(j6ծ95u4`97lm5zzU[`yX鄪3QAUjUƙASꨂD& u(;,wDC4o"1:H:\2?XUvK$AmQZjҎ{E˹h8lU?Uߌˏ\r+s`8.(fr#p.s6 s8ǿIҪ<X-`a3 87e\!,@3//s w*(q5`a/º{/ed;N8!V`?*ՌYk"u)>^_k4h.x;`+0 bX0 C`+0 bX0 C`+0 bX0 C`ٹF8㿖 ViwQf:PLۥ'B˱taq9-]]xHL/&xፚxawzr[NnC\-O>Sf p\0 QrPB8 B. BG@C@(`(!! ! 䂡x&T!SPRM(*[7 c:TS7nVH QaO< >{X4&sN#;%jB T]ЗG_=Ni7Pb>bҧV '.bk|ʂй<0d_dEt-574`Բ8rmޮٚ/TxXU-u8xOP()sUp,/f/넑\ntk01; Hvw杰 g^C_(-EW}^-x+D}Ϲ" p;s{)}ם;L.@ U_p8txQS=K alÃCw|3ЍdR# j( &%klV|"PEu{ Go?JJ &o7{rR傂f\鿖ľ462Ja[ ͐&`TSyI/ζ+()I &,=\.$4ڈxqQR(Jh?6IwjA jx a EJ8bDR}c|)۽J=J5Aw%֛:c65a[܃%9=ptg 7[demq8};bx 9%pRJv\fpěXS#(4])+|7pEQR(JZPn@7]@I2XTIׄ99. g ' 0b%j4lъ >q{H̳b plT<O &7,Z-ORt$42GPR?nQEi)Ǖ)(Z b;NL K0ґN_ &KfKm!yE&<^Y`AVk0Sqj[cFQQpfG4{F=RʹM> )Y}Y ٓryQ4$OiebZ/ +oYLZ6ZP; = O8g橈C$gb-9YUk03NJ#ž_ 8ȹ%i/[_Ƌx0PV F*f ׸V\"f;\?3-`=\[ܭֆsrǣ'\Q,~ BIѾ y՚ٹ =" `gi{I]t| O|Wdzc 93U f$ue'\Kl-uYj^qvQ/pwϙq3<8cxp ?88q3<8cxp ?88q3<B^w ǹi<8p!42=Xu"䄵1VT}8ƨq ;.hԱ]a}AY,> qZ[р%vl`^ HStGq9fFb8FFp $S$xQιtCfnkѷ߽ Z܈ɧ+Fqab-HmojhHrI(q3߷>y#o`k-Ѯw%Űφ"%:vi@9Tu>ktTϊ- ϊc84w\x'?!d;@}@K7F\t `U!3_`4u^z=pݐ \O~(#"e!>&.]eb+tE*6p۶q4VIn&lhLVCs)Vx*G Xn>6Lnl }&tz1~pu5 \0]0ƪƶݵnh{`vϵl_f5V0J*ascNˍlb5ǫC*ʒ=~`7U1v&rOR F_4ţPmS@Z}SeP)s\V8,wuE^9< 1o05XZ8MXEQtY /{eE &Sr= 2j6AW)rH;"K PdɚuQHǕFJ0Ovqe}#F[ Ƴ> 0vq6 (dD/Z<ҬV*kF!zo(q{JT_ Ӎ>x<;jʐP+=!"a1 qLQ"N U:<8xGP25%#"QIA3-dpͲбEE`.M)J=5bYJedjce7t$a2$Rmf+*,*a_?T-ĩqyJt cG:-aΊM%>_M'TEac| vbTЃ`&5mѽIUb\ӎ0\j:T8JjgT6oFWYqU|. MFLH۵˲ۭZٚwv,{1*ybl@ ?q_g.7n,6 hU:"ՓI߉F7%K{]|T~p~aLXZ2\UE/CN#t  ,ݰO摥'&Z4+`v%y P5L.)#=y}PU{7`L ;DgK "-C$\]QH+5 K!U0i `E},l gTؽ~^qGX\%{J\\&A@t@ȸ N@K9jL ٺ*tϷ<`T$EXͺRn.1@j*3gG:iwU,fiA.vɈ0_1(B5JofyD]_ +y.ilpNX[fIeͩ鄦RvN?;;I:]BYN0@/7 BUSE%M n9yqɼ 0;uR Ms:#F`fmjN1@ȳY8{twDp̒&h*0@Cƃ<83\Fk䧍nr)|9{rl <*{T>rr8)wA8{ҽfʖl{$Opެ;l `oZa-q#ĩ+hٙ3Ctg8Xil^r>hx%pdb;Ҕije!)GNw/3H+AM*0@tM IjS)qj52h~DMWi)9]g<&4lЄY<@rWG8i撗v!h(O*58W<_+[!勺XqFk3d1?rn*]s"z WFFqfoIpcLCvs@8m.k3hKI0J"}tIlpn6o d,kc7Rm^%J1<_6|Nf+K㗋񥮽Ҏ\ n*" ~څq(JnȽ`T{ǻkhjp M$BAy9 ,LӼ<;-izV$n0f! 0.1I'w]xWCO>2~ Y{1:i3ZHj Ǵ C`H*۸D,OPUt~/(&Yܜ~iLEhLyh OleQܔ1*Au2#&ԔۑKM)P'x[_;nLh:\xp*ҷHX*W: QZ\v- ^&4Rgr*X$S8xp9wýWW>a {DqZfcۨNN/@|TB*DQf5_;Z򾀵uM/܍=UA__ѩ\;xQ1ʭ3p5%n,&"9A޼ aTu$f$mhiDA;nHu(F2} |yQ7#g&l0Q"LIأ`yQCvDt;sr"x4qɗIx2Sz+בx??ҹ`"4*J &^, M3땅7EiZ9Cy+dx|h3? gr)|%AMǠH{\.Wa ,=-Qs熶&yߔI*o#mBEhZ 8j(9/w9] 엱@W< ױjT 6+-^jd: hZAN?,[ :z9(o^Zn*.77n^]C1(3bOoa{0$W*ɰ?킪إ,^TO1ɾtʨG?9 iYYCgڛVxyRf5}7[+K1XՍtG9 芽TwOgA7}stˋ'6(+\jp:{> 4|4,C= `6)p8!LkG59|&/!uP e'}Nm.9  B @-? rmdh PԢ sÖQ m!4'nٸkA(/hQޏ_Jp`X;~[ Sz(} B @-'nw/Y\5/O0fkޮ  BC@P`(! B !DB@H H(i0 ! u qp!V6BQ7+RfY]`9Jfe0}]v7,߷b cd$ (@1H1@2Pb cdV`}k%=G~9_zGgKqrs8ۻ3.7"wu48߿x~ކ_iD,``DqFIHҪ4h(&g gjtP4kdR2̀YYn$4uP"*RQ?h ﴒs\]?pǸ(~Ol Wo=Q9rMIڙD[MP\O/ʿ;n{Lc^'mGNE"H*qԩZ Ә?P|~2t:mNkPK"Q!`+`w)8'fzc@"޵}Xn,˧lOk{ dCI  lhlgyIRT00YT8V4l0`n\%XVP,Nڪ튴v:zN69@z2se?tc*n FM@Ʊ mmOx0t&g#'cAQ=&םk0E܊t R>/ݱ+jRL`&`%8N&[WO7Hd @2/fenp $:*d^`#x2X!<`,Ce C!x2X!~oo2ahSf̾23{1Ffƶ7&-[lE$DR\)RJKZۻz/!Ch"!4@ dM2&@ `X GaJϾ#`8* cX Gdn<v!Y<Ηilalح{m򴴸~U,~%3ۜjW ؅ gcsB]p ޽;Gmt'QM]p! eGMek&&؅ g_8htF4.pqq^.ݔ/չ}",BdE.!;cakHM°#}iĈm2*jP[ԝ5llXAb!b'$#RmvfD&o|gKNr!_l-J Nkkc]x?~s+z~GYue!?8Ӎe?U7|rb^rRBjYu x!mաճN#qL\_|nC@0eŮD ?!#0"\` Cpa.0 E!0"\` Cpa.0 €i50ƛ-*G8|j8 `ǂAk 3@ Fvk53h#I)[oC?s#j-j Q3mm= 'Ķ툝X?_ɪ%c7iPn*'ެcmVմ[Y.l@Fi8v򭆸l_'*qN Yl\rO:N&&S'\/]V8jxN;1! -{j}kMʊg `_W=A:ֻUyB FG qA% P2U^gAUXeEk^4_CuڑQm:~ oXKw}/Hj^'^mVvTj#ָuZq9ݤhXP I&YS #/r_ 'Li<>ޝqqAJT(ƭp;^o0`&ekBp6]6R.JznRU/UjUx!xk9Es<ϲi75=PYHQ`-pHQQvxpw 3% Btg3[PV@oX ̋CK0_,.+`[C.k| ]8pO'2/(uo=KKe!PJ"]*U[<#>[VmZ7 3ƄHuU$=)( v41 9^s_M e0u'cܻT5ʺ"H(#G`<_AVptMcqS\J!p@3^x}cB<,% JdlUm?ۦ`Ś/'!-߈-1{ 7\]j@|!&.l1[%1<QQlE.Wy!W_ 44{)9Qg>Q{|?/ sBܴ VWύ7f&hL dl&V= A4vXmpMj=65Պ\5AcmVփ"| H8gbV5܌6zuYTbJO\E.<B@. V[ͯ^Av:΍:5!mNn)ABD8%ea }kl4It^uxC' jgJYőׂa/5]b(<B0Tg5V9/ DtүP^)fq,PeHzj9wP)ִ9•1l&S"-/0 y!m(ƪBwcj.Wf#i!7C;5ָеOn8(t_;ŸdBo`^ Z?̈\pΩBayhv; ZL1$=)[1O(ǘɭ*2.?]pJ籥\ۚ58 B@N"Nw_CO>U!_t3BeX{SRp8v[o3@DZ i^ n a&xXx5C79y5rz{1pG+ Dt;>^[q{Թ͌|*zQU{Xb}GC͸JrfEE>ӆ$MRW.UvkP({ Q'9l9X7!Hz[!ڨu7e@8)Z)}, /A82-(?`e<2㑆S˷{^zJjwOy]7]s| ]A҇kYHZV_zl Aac[L{4;wnVh\:wM< H"0LJ]?U Jց\i BB[F~~mxQk YqhMA adhJh ڌ̆ҁ-YE,B=CG) zDK&avO 5_3e i;ld@t-&_GP8>;-B*|-W`WjpGCR Xprj( >v2(ĸ=>\}"K M;cD) T2Τ| VQ C_@Zݰ;V%P3PDZГ)~)ukۂmV с$s@zn >Hs ?\d5~lw\ڱRʡ85 iY\ʘZ-X<CVĄUBUSdaIPC)k+W<;/Xe44u6q#=+$THe j]^Kk뽞p8]7[H:?BS hl 5$A\NO&[n"B`;rŎ$N@DkgWKXh8jI5?C5Z40-c${@c=#ZH= X$8~P>'jzHFgCć4jTt;Jqޞ j3D <N&. < z˩=`-BgG~m@W*W n` uj0M\9 K -r3 x/6 RcbZ?lYYH1L,_Laa)AD:ƶ.E[@WJ(fUMqe@,Җgd\Yfq f_İ @￟XeY!gntD|YE=2(xڎ77d:Y^Y+jxֱ>['$%_%ĕT0Q4Jl`LQ̵$!@F-L/۰a>ev>i+E`P.9wIW52d^^L&훕xTCR>֐A،@9 XfE:1 0Gl7 $='6NltP E%[rk஛CިrfVԸ_e|MWy.MyW~V>o Ei[;dw>NLxUhG9Zxd"#RP8);t o T?҉Lp"}|k۞HmvM`0H' #(`B!(`B!(`B!(`B!(`B!(`B!(`B!(`B!(`B!tpxz!#tpg. 8clk #4Q1y$>L 5O@pkRre5f/n*-b%8\Iop~_%4beD{I5)>J0/t,m 3j7٥Q_DDCyj_䵉(υ{p⸤ƃ$1q1jhŝj* * 5P֭V&Sҡs[B»aGZ=Cts[{S~Z[DYegnjeG@A>:/WN+"u%ł,A >>:ϝ%Csg`!/v${fhѣ#%(Exop M?`x}E,BL&S]:;RT48SsrTxަ ¶3-n `P zO=7֋Ȳ}ƈu^TV6x0w h.N,=bt7ȭ=rk ҶN$ɷ8N>Z'"uKE:$9@9@K?][7w"s_\:xx;=NZ0(3-i9A&6ّa#Lpj8Z>Yr &>Wr A>Cp3 A>CpWv8g0C$OѰ&5dΔRThOA2F!D3.& ˖`e.dJpąGi<7=wޯ|7?Jg%3@  |PB>CL@E@}pt gmlC#LCv!ӭrHM]p ,E]pNɽcIzybMV]pVO;oJ|Jʭ5H/jbB1_&Xd'?v֡ 1Iv+j ξ%p2΀J^Rcr{ κ蜸5^yJ ;}mz"wb%͎_CB.4xG?/ʅ/j\mʭ%B/J@g1Gw\^4z)˅"\ tfcraUִͨr 7EO reHậ*J\Z"j`0}ShGmp`shS=؄ (JSvm#WNIzn'6r) erew9IvgB;PٝO56}oT8+IZ鸼l0JUp~wOH\=4ci9rjmz?hy&2G]p> > rX<7_|mSznuzmX]p> ΛʹmjiYQW/.`8Ā6G4S7sKp|]qÞ\wP-^л|طc8\\ *"<9%o-<+lCȆ)0jh8^wNzc;g:cѪE@暍Fm$/_46#cZ_,< ͽq+W"b~Z[ȋ/c#Z' /*Ӏ8p&LOoNE@ 69T'D@a(@"0HF P$ #D@a(@"0HF P$ #D@a(@"0^͝G:;z2" ?궛[&wbTgV"bb(./.L ^ġzA}l+".^|*ػ&8?rҋIwbFH,6[Ԙ` 4HjlC%R XĞ<^D_>JF*L W|>_ʿK=V ouc@ רh]ֱ J W`Ӓf/vd4 } h'E;$޼W"0X(hPQ5%gls|qr '`n}]l[ he#l&nK6Wd1 [GxK҃4Gvkg&C`hwKNi+7ߜ>gܽME W(t3v_tGwؿjE WIwzMl۰M#G8 P3[bl%W?%pM`1ezu|]&V\$sן @% !!82B@C!Cp d !!8Ov짱2RPf:c^9c1]h emҲua 5.%nq%F 0os23Mcs$p9}_2*C@(2*C@(2*C@(2*C@(2*C@(2˫/eCa@)c:]]&]8$)7>ߦ_޿|k}>kq {;ynVNu_Z'8KO`bT8/G(G &9IBLrBp͌#K_bT8Ϳ?>x~x}#k@Mz`Qq_`[ΚqdqiQRxwWW׏_OKG k@CWu<2<]#` U (`5,2fc\:k2c/l%ϭF޹'8wB ,v43OLn=7s N[,SWs=WNaq8|m^hDCiFs L˙ k5(ʿl USIιN2r%5 U πp'HIem`HָOؔ ^b=0\*k>#9A\ NGC!/t%) -moD]<3L5?zh#!% @jwR\ٙ< 9B!Bh<%ԸN!)!3|_'j Zv[@t v554G xa6]`M(鲇МCQšٚlvivp#m@Xג̖$4.>0w tͻ)e"c13O ~f)l?eVg=pf&x2&)ñtc WMFv_&F@ʁ̠z-)  P 2|,kAQ/0$S %QZĉ{lHIyMCUWG;Xl q󰇗i$܄A Uw[P JMqxbVܐ eTQ6#=|lV jf˖_{`;|D(p [u|c@;c [T,}.^U#&ν؎Q`[M9w*`އV1V.GJ;{cڙ9|ЄYY3P%  `0]ZC@ g^9~̫KGyoCIT@ t^o@b]@q Y@SoB?ݚF+pD1mYhJ]`bƕabJX NCsHGpGhD(pz 7^E _$2w'YV6R!v0;` 82i-W̦bĕɦ$3ocN*PʓPxRMat`σۧA!Np/h+@+4iel8 a>[rgx OB5"f͖ ] KāPax{l4 TAf)t@@Ľ Ձ _9ˬwB[ʟ`HyJeg`[ŝ`u/xMgf1-iŒO$ ay<%媹L?̶:-NG7f)#P7KgPRt7{p:X׮/yTY`L3Q Qi }95jJwj|_܏rPh&uw EZvfR(p +8~x_Gl ʕ]՟Я h١oء)T&fi%w #+aq 6Ư6^6fn$ |˖e>% B*}b]>xId!{bGN `R&OAhYFmkC9QEny1Xzl1'k--s<6hrr+M^rnݵpjm?B ׉y1,4lFIRa/m"ݎvػ%0O@3LAIYݴ4u͕N vں.Q]uQFuQtD7Vdy0̓Eq#.^o#9{w?<^DBcz PCHz+㲲LCrLa9Fe eǩЖ%Yi:Q \:2tev"[0َu;#ΉK9CEI&c bqHWƮtu|ƐCR{˙ig9g;ernBõBр ӄGl%'c|ԍ.N L㪢*o&SƧ.5$0eZVU}@e,:B-(S3;=)~q&G§C4dM:I.&)کQj!ISeM7eFLw]n~dUrY2%)Ԙf/iJs)BvU:;BXa)5)ϏQ7eWQG[[eɵ4 kiAk4}Iөj<xLIoyo,K\h5v҂]/6*+Q_{LmKe-M懫̥Tږ[K7=Ѥ*.,5?{ɔwUə֜fِy.r`S]XiĵKW=ă8׆ @@C,%N$!,#!!, |BX8`! p C@0A 8`! p C@0A 8`l>~| @, [Mww'wKkYD) vpS0<_ 'MIw ,ND=.-n}>AM&?M.Ir"޽Dq3 B5DT8:Xc#&iC(B ܦ愁aG]7+-t>޾W#C@Y Lly6kj)NJ\R+`ė_` φ+oI-f8K/[ڔGhg30dl9fzUDs1Ugz<7Y`{eXmk0Gun?,(VpE`řZ.IW܆ @ XG޳]؈Mz݉c lMt}/ٓh~Hҍ`XM%I};4ٖ'#Cg$]0\8#g.|"!g.|"!g/B^ / wL;i 8kZ4V-3Lufh҅-@e) ,e Eq{Dw%L4 Lb+* z>/4-99oGf떿E(W tʊ$J @Qo}ɶww6N]M,ǶWqNPBUPU.z9NZE=4(Ei\:Ʊu/ۛ&4(Ei\atdW@Q4(J3~A`iP Ҹ2&B( 3QA4` #(l] u,W@QJyc-(bKp.HMhPԹR0Ŝ[(Hq`eAp32R4\)C#4&s4ۣr Yw}zꞞWP:@ gjѿ2?gIU ͨ/| Jf!f?^P+ok >P4()UG[̦=Ͽ,: Y/f3s]_d`f gΐĉ$F$ȭ F۝H([fdY?@w3iκ܇*e.P4(I-VO󋶮sꇾuR=.YÙ3LrmqFq|^&BˊΉGȨdVqM!0Lmݓ@Q×] EΝt?C?uzǏz%'ۂ3g$6d ϵ':0'DFسp)-:XZ%EU+^jnDDJ]WWK8TvKºYSP"0 iGY57"疫88ߍ}U B~d!ύe@<~m!ᕥ*]Ȯh(N!窈$c=Hnƪ&KRU6Vt.I K"7PaW23+耻g$I͠$)3 rt/(ް@{hPƕ1,6/Xx<\(w*M;;;Dv 8m`*g^Rod'K&Cj ƥ˵H3zsbS(1eFMd4_B֜$q 2t`8{Iz=ZdXmdDd% z @'F^;n:e S87WY@z@^wn1e:ki:^*p2ژuKLCzPN&k*#cN@V'Q>s[,)us rpvj&L3?AEέrku (S mow|śQ zbG`֑1F +!dn:Ÿ1d6o@ҍ4A1qoG``Cʜp\9ݓ[+)3i;pf+n2Ɨ| s(ݷu@/#*:݃Ԅ kV g=3T4(*k|23"N/z?i{%"q vyNn `hs6BU椚\/!YK}|` Ӵaq¹AH3}'/:oUH`/ %`Au- 'G1@"XkMR 0{4Ei\9q7X6. W|]sQ.%g`"N|.)x"i+R 2y*a~ƌ-7c\=2Ӿ[T%ΕH@k3 dȽv5XbI "fߝ*d"+[ qe Wu錢:FTZ3~ m:1_ t^}3dՅ6&[]C>X>#0VƏR$/t*=2rr˭{IxN}W#e캲W&^UMfIC4pZ=+c[ G^<^p8_LRzc[vbg9UF77ԃPy 4ÇjR&n^-8a6YQR$O%jX>qEno*50I^laRg)@6.5n|x˔k1"ꒃp-@QJy ܨA"ȍ8xI7O0ߛT^7Rb^/RvQPH@K bI$d!xJ&1X dg=zL}@PĪ](+c^pmI갏.8x_ጕAh4G仫x\l#c0YWq CF󬻄 1+o|#T%9>YY|t'., 2V#U, GW+lZC4pS573P% G>|]d)Yoę|]|)u}9.p=1J&#-aV+p 2$FT C=R1]G َnJ뺮.dNwSSB[H U[AZKtk-kNXkkY=/RSL]$r-Q^_Nb) Q̌`zǕs'n=;O%j2M-Ab9=1LsYJu ܖ6&@ҩp[kV9|ԲluͲе,5~buک4 KQ i"Ɨ/25G'VH}M RԽT_ KOEs3J;` $/rJ@iݢrf];;E``$M\6*p8+~sVu4^`rAR0Mg#SNM o2M'+$p4a8WU%!R3gaU"e!\e!\e!\e!\e!\e!\e!\e!\e00pj{U4Rу3(o.T_<| #oT!UAoP}.lsc%DRBDK}^wv%0OQH$k2qLT:ZtnXL*Iq1PA4r\E MPsƣVgj#~vgw={x{ń2Rwm٧םNj#I{ZZ c.t\&/$?jl!#=,`X3\ѿ \::֔< v_y{^ k @[Sr:~(T~5v`m ak Xc7%jurFA @*]6.RLBN*GO*)#r9@d6f>hջB6 rE%ΗrIV`z*)קDTtORA@ \e@PWY+T} l@*I5956Md|;SkI@ T~cJ2hW\^^>h<jD"KG" s,\fҺXV"JoT2BU7A`*dg~GT>+,,,Yz1P `s@nj%O2 @*\3i1d.fׯ_͊$1bỒk Xd.n՟s&rTU#`7\+} (s`K - !8c< !8c< !bR8@A" ANȗ|7ERbj[[+jZ[KZSU/,'9~s;&C@Qd(!2 B !DF@H (i2!M" PBd4 BjB@B@H  B B6E 60(sM̜̮̌ȦPOFciC 5d4N=0v%to{ҒlEZg9X&!u?;(lqhܧ5xvIg]^\An)WB Ua~q;^GfX~ yaMj$.^~yاp/\\)\MI5!WN軟 Th؁U9Bg4 ݰpϣ%5jl5*Vߨ6a0 tiL6̀Pr4Zl'@+Q(E~N(Q7'M/ŠU0H eEeSfIkYÿh=(P7S- M3:vc*=ۧD5˦5yi/HSTi4=ޑ>6P@lJH:3iݳY'F#8ǔ%FACM)01 Sj \EԹ 7| 'tc;7q"Ys-k f9c@rڈry;m8-dƃ1 ^vXkmu0?r|90ty] y`2 9S(R'=k8|&nC%C=IP();>?{̾'_^}CA+(l@:'n) z+8!NX/HH/vL܄^ȿ Kx H8Mqo-$..p 켚 5n|7m1`o!Zww_xWv xp٠{frK d\Ndu. 8Bht(/NMF\JXrjH&sK̓L|ySk?=5y=5N>|dȚDF=M(YmA٤e(#F@H 0V,*D$ Β 9PRj=!폣d4i&!́:Wв^䵬P2h@!́:W䧊0} 9PRjqq=4 B\-채!Q,( ѹ=$} ޽4 !!5jjn Vp CmT P9ф7+C]K=ЫfHfGLi?[kq\k)TnI?8,g4d @8uТNwA3= hMzß," \捞6wӕB3zG En!-"R@BH[%}i8ީv5ʅzKɏK:,g6>i^guK."WFϢyv:0罗Ԫ `9ЗW;eRp@n  aRN.lEpXI2V^oeu=dftIj+H۹t=4X`>Hk0YBtY {z XdHk'Ka-[UzjR3\~=C *RQѦyz&?M+X,/OGQ):,`4M@8n = g"pF g"pF g"pF g"pF g"pF g"pF g"pF g;68?9[Qd¼0<kלp.sdAj-:׹ʢR#nB 4nP&Ԯd4#?)7]\7ϛK@@:t0zB*$nwf6vBxox@QzA 5$ќU*'s ` !Z P14ode>'[2dk< y"/ u`\ITI!wVPӀ uW+:'N;+BA@LI@?Z(H/O$k$1 =*=Y)A+h2:q vxG`@ɣL=0D)qxu3 w]1&slPbm~((/+ONT!sS&hgs(0v= -:u[PQd5pIl]!_*61*4 gdX[Yost 0~^`i{%:sJ  e52n^ sKe >A myg-CX@;C}SLRi!9j@NeĒnº?0 lMל(b&L)4Bxoxz|nؽNl,Eb>Op̥sXZX)?(*fDI %U[@o2AOov]Ζ(b"L)ENczOA{M` /6HY!C7k5'kSn`߲A'44V@_V 4WRcp71kOG(*#ٳ7oZ=k{P< W cl๸5d΀R}jcm@Y%UlWnD-㮖(b)lKp!@1.kO=iU+ =ͪ{,ֱ <@lxƯUl^ fC'AZ*P~U"t_ۜΣ(b) g4g99&pyl{į#|K_rWP]2gCA [K﶐*vwMǯ~@Be =3WLF/-JK5{@".?\3_]MQ/Ul@ƵS+6# YGa/W+0U܉&D9;D}B (tmUV„(+]7(0R4g@ô"'0 z'qF DiyO(Nbq6dv4/S=n9AvBoQ4U.Bd=(RWUȨ+?)syfdu+ڛᛧ_?s]udyq{vozvA9WGv]ji.v'Q!t9 c2cI_kFTʯЦTCAuV_hD@z´ zhD}m8~5M4]$6Tg6cԨWHAE ?=s]έt{<ݷwH!@6<B@}!C35 j?@~3}ľO@Ԉa 0tP 0~)z`*!O33@ =~E {k,m@ VJ8e()]|)4to"dU P?{O. ):]j{ ř6v Vi$4zn'>sPo{#04=y<]'G@G G`TyqV78G@"k~{g9S %?䷵KX=2:mCχNHu^>`i`CdV>4 >$oAbڿ<ײs HC9X#8Z8=̕rʡ[+v5R5<(Q| }4<8;bX'EٹЄ&܈V~ VC$;A60w|pn &LƭOv VQ\GSz{i Hf`. x]<E<) 2Km/`^[mĞaw޽: DqNiLc4 ZcK'o|` > Apk~W~3L;Z6gSp5̙3v;b=&G!0ʖ` ޲ )X~=@fS q<qlxj : 2i>Ez'558zA9xrJY09<Vj@J#,@U@;|'`ҢyK0<\3 ?&vKd# j@X*Mkz_srOq=ԇj /ɻqsnA2ToirDZ1~P+O0 `5t7ʻm"нV@N @8?H1?t `i8ӫL1As6aw4˰ \@0p=8PjD6j[8z+u`)0 w DT@J}kh&JWA@d~OqN|V?nXהu&sӴ # @e9$lӤ9@1'˦i+lN8xܥ,=9bLwEy/_2b9Q 0@ 6d< `M?- oFDf!'07SȸXnG7 ,FTV/|V8@'#iT6&sN-FߧmoԊ)@ MYE}(F)h( @\Nm(g~x VM  XmVYk4Բbϗ1n;,+qP^l_,&X2A+X7&J!3ؖe"sm~`o=C(t_Gt=<\d/2>uAh6Ìϋ8npd XvX/e@[T$κJϚ@5@:2wk6mU6'S GV1c|g膥U*[< +c@y—ͥTk \ N!?b +軰n-=J3 &d 8'Ƅ;Q8.Yŧxi gcJ4!]jvr0df<ƾ:S\}/Oz[>0(4OD@"l5T_G?& P_v75.aY?-U9T$]4y9 IktOE$p @W h PgAS<x; MP@ T|Pq0SW) .3i"lʩ@xGԐ+7.yr? BmEYV[窈&"5Z{CN* /M8zt?Ț WAfM4`fM % nöS׍d{ܔ{ߧS="jy9]xZ7Гe =XېKimm(:Ma[G:KRH-:gOR~I|2+]jq?OGJ'.o>z/?$YT>qU(ZKXuT!~X.6q$5xz:F$s7=%?B-{%u/=gOb?$GYT>RNä@vV|B:O`\T]*vB)Evj>v3幋(ߋ7Q'sTȶKEEb"r#y̬ F @#JSԿOVrS<פD2Iwy4[239L% A@^zV¦Yğd<(zy2߄isB{3Ns(͉f?rY\)R_4s3y PQFNs(f4#Cf||v([P*p'@8 ΃K  `c D4!=! CP%AC+ P!TC'tuA>hz}Q0: l`6G Tx 2 > 7k]~ bQV(J@ըT) UjAunzQèOh,f t4^^.B>Xb0L&)Tc01w1X,5ź`4 lvۊcGq8& qqٸ|.Q9-# x1~= ?F n0PL8Hh! ƈ*DS1F\G,#/ߒH$+i>IDZK*#']&>Ud_r"YNL>Dn%'P(&oJ%RC@yBDSV((U(5*RzLP6Vf+/RU.U>|CyJP}\jj:BSS ST)R9rEePjWS=zA|i<AEK7siB1z'}DMUQ-FmZ^a02Ōnguu@}z-45w5>k2555j6i>BkYhZW,,YYNz k[hGh>}]{TGW'PGKΰ.C[7MwY!=Ho9L5&,c3G; L <6$ S 5ZiTk`240`bjkѤdTÔckZkȌbeĬ9֜enld!a [:[,Xvv-]5NJlŶʱfXZn~ecd`զ曭mAۇvvvZ[+8P84;vt8uDsѩ髳Թy%eK g.b\}\׸vvw+t#sLaLzm~6g_H}|>mCuG? 0H  t \ 85`U!Ȑ򐧡Ж>g'8aY<-F9&1&C_lIloMܪkZ\BLBu; $:%'v/4]lEZ2Yd&)6Hn;Iޝ<{TmCB/apX+*N JL~(}<#6>yJ*Ngf-XJ%KܖX2" V BYs6r.Yzr2-oZ<7 m+W[ٷjjhu5k \{xq]_ۮ/YnC솖Ѿ{7VT9`Y?TkUV=$>{8p{KM#ŵpvhћ5Yg_Sm'Y'~6ywj\8$lmo:|Ž_?]qFLYټrύJZϧo[B܅;;/\|)҅vǹO_qr*j5kם7kCsg 7]ot:{~/ܹvwݮ{==g õ0 S>~Roνg?|w_Q>{^3h?xz(`/^J^ C_?č SVwFG|?ßX:>~~> KW-B=pQ(CȜ&S@S & ''VdzA4 J祝p,~* MJ"S=cM_b s~BGWm].S3z &*p_ IwZ$VeXIfMM*iD ASCIIScreenshotAeiTXtXML:com.adobe.xmp 1013 Screenshot 267 =@IDATx|G_Bpwww-RH "8R "Hі/R(Nqw7τ96}efر7O|7YK_~]vIȑFh˖-:T"GN:I$Id޼y2sLy왔+WNWN-N׸C/^<3g@t; ~… 6mZr劎/H"R^=֭sʔ)#-tIf$z?tR^N`%0>13fL[֭J}14ҥK˲eaÆz``<#cœ o8q.1uYEݻwK~lusHHzs'dаQxi)W3@yQW_d <{|:*^.]z!Sj~ & A/e(Hñ(ޫW1eCY ek׮Zy2dV_9tVtB A"ߥK)X;vL骼;w*TS2z)˗JǏMӂ;Yd(uV[;,X haP}UjU\CiEHtEKuҤI:LJ.[믿ٳg%_w}H 7oԊ!k16ڃAч@ ^lYɖ-ܻwOnݺ Hb:< e͜Q+0PHHHJfڵkl#-8 ǔ }O ޻w³5kVM(8P2Um޼IF>LW8ݖ,Y؊;wn)\L7nX+3^hUV;kNtE0:xl!w̙S{݅<0(hMƗuo= H9MR_dI=  ] B:o%?$WJio29QF;?(Q"Kd0Ҫ? *S*|(wI9q{FOd2ڳOU  cx4C op r(V1¹ JJ#Z5G(mC0Va8E7BG57xQ?IAX<^ ` c;sǔ(X4 t:,`w/_>߹s`<,grc>SEd7 BfɋC2eʤ>@0 s0 "}bP|&|4''F( 8'7q}$AxҺyc9OI!XDUPvH1wdHZg[^W|9|tS;3 B C ҲeK[= E>-$q/*têfzf5ҘE r%Jc qU}?9]j6d L}&t 1 #s!P;&ڵӑX@s1Q: !X`ȓ'.kСz6mD9X|O ubo(0L`/cb㼷%ts|;jGy1zw 8&FjZ5~)KLsq4n^['",Z*~]{Nj+iRkK^eɴ ~dϾҧ_%NXEZ={`uċG4#/km IHCX^^."JUIA=zE:X FEsQy|h{Wh~-XS 5,{DЁH7n N ~Ӷ1߰$'#k2IHH<@)T*.O||ue(- Gpa[ϥ+੯8)U"~.aUw'm{ ?,}c!$@$@oUdʕ*4]tRT)_gɓ'ҡC/Vļy̙3Ҿ}{߿˗Oʔ)c;΍Jp>ׯKh$B{̘1r-w;$@$q`ҭltYtPW)1#yIa$Vd/IdJR2nY}챌KT._Jy$%:*cK~-+a1|d'ҩT4ckC}NZRHH<y(+J~*?} =8!aϘ0d߶mU9s*U΃9sHu<;w  jՒoŋ\rZ61BOuذa,YRM&FEFH8p@ǵW?|xP7o.>رcqo7 Z .OjLÆ u9ڀ*'Ovɸq1?{8̘1ٳGS\pٵ{ :TsxDO>zgϞYٺuV5jq͜]/7e_3 &_Çp} ҽ{w+*n @r&5|BE}ڼjNU|,ы>%zK %N \y֭>9kɕ'E?lNEP!nưU   `"%FCL~Kd$VXZQ%$zϟ?"}| o*>x$@BrZD¡dsLժU Jʀ EQ{(P ܛnx2mÚ h+2 #,Yha֨B r1am.1e;kʍ9̘1C0c!PLg͚֯___k(7n ,KNGX7HH $mU+/^.q2N,5G>_*}7',]Dqw~c=pTް1Bz<& o>{( ?= ז-[y(c͚5A-Z/x̃KA5k?2'Bᅒ&MI VDT5()^VE]P7l /@}.V(Ў8ځyEs:ԣP^a@;`^@sM*ڄrƲP^nL6~ bF`Vw؛ޯ#EraEͼ~ہP+W.0UA}.wkxJA;]/û +0Bhqa(p cB$@$9^T)8Bxn#þ=E#iѢE.@ZhJ(VҵkWk% ûPlҍpwGL0`=°aāWs:"?b(,np:VXe}ǎZ!ļrW(C0:۴ݾ gmJ)B|`(c=z"y|8?So1L  3.&4)Se@D¸Pa0@^w`qYGGƆ9xN,DzAaG˺~D@x3N7իf?$@$(oi—9:OZTu5iЫm/|5/+RM_U:HHH 5/k9 yʕWo }Hxy1GJ^H .; Kwnxrs 6P1^(v3UCf#@ pP$(mBO((mwkxaSLG |& z7j0\}_KʸYK2vX- 6(퇑* HP9S& \.G%]G$@$6@a-<mg+Wm˪L9c(Pj93SrPꆗ?(F"Wk}M8ߪ`C7 9*U*zc=Uz\7쯥)Yi0:a/dq 6;WѹiAd>IHMԿI<HHx]O|ކ>Zjq9 ¢X,K+!%51 PnIPHHy 0cBZ [fxÀyijIH! t=! vǶ c.J$@$@o^qF!   T=:$@$@$V_   !U6lp'x ާZH3 {b Zn]gO6>wHHH 0'N, 6lXDXh:аy[iT.Q,HU3c$OƅIH SI,HHH>L/^p8=$@$@$2dŰ     x &;~}D9{ԨVY9*uZ._& )3dΔ?d%irmɔ!' IeѲ3RH-wɯ#R2ҫ{g [֬(GOH2%F> xY $/WJv=]Vƍ"Ϝm;J1,_Vf̚+-6ˠFIe #zt>{jߕ=w]ee2ٶ&.wt^F iv9w|{RLsDY#y       O%kI\9^Q׎me6^b?kxI0ҷ)YL޻'1bD[ovΑML>Kͥ7n |.(._HYmyul'V       d!_|UҦNec0A|66&NzlNj[f8?QHqHKT)64j̹ĉڶ#F(}Uoҥ x2 Gbƌ!uu66Ej۟|Ya>X%M"g̖}J7r IRyqĎGKR:b˃v;  wNV#06SSrdR`~7N`YyHHH .X\6n&?S‡/]Ql)%ݝeHHHHM=nr6& ܙԭ[! @xw5uЯ<ྮ!P͛5b`l}G}rBI]Ft~!   @}f{߇β$@$@Ν;Bw#2d(Ff4gLNn!]_ez!ռs+$\8o˰QWnX0o|&uJYf,3UEj$@$@$@$ٳ7{~HH Rνz"2'  &`-v >P=tJZ4N֬آ09l֖nsce9qNE;jÌ$~v[kn./\FFʊ]۠z'Ǿ]/% -%Vvc>3|zЧ@z;&Ч3mp)}~P$@$@y]*  Xz]RL~Y>ϟ0EI ՛x2U& ߎd=ڶO?'+n5GV;N ~pmԨS^貸Iݚ<}r+?$ū[زU >A$@$n nIH>0P뤽]bcS v%;}xqܾu"[6"%j]j׸L5SJ[%vRS)I>.˂:OҪC}-ƼYe}wT1ߎ2irYɔ5*HD~< j̤I\:}L)д͗u//?I'W+74ObIHђut:gr}p%YltV=K+=OtȠ 3uy[Hhб{SY|,F{FY $% Ǔe hut{y\Ma)=:i_Ҋ6Bzy  ЮʟF~;ms$g:FgۗǰDU[%2 ʠs~ɤd5xz1yŹF̐S'K55hlN5 gȲVT"o7HH<&=`IH;ib%&9kO1möIO;-ݺU$l %cDiYSӊ{qx|{A{\Cj8 :<kWmx&Px7%=TQY)PG,WǾM8yàТ]])WV[-_#YD4h,]%M[@ܸj-?^*q%2FJT^PiX_8EZ]_:z*%? }h;tuUJֵ5۝**6c}AoWĈtѢGy1D<Yu"J1sǹش~.b/ 9~Ҿydտu>Gbx0u)J)O0/O>M$@$U3V R󇋕DIkzzAj?YD LC3J)&_tMwXF:꯳8Z͜C6 NJ29$)UH>:? 0$x9;{!bx?*Ϙv,ER0Q6֭;ZG3mi3'/Hd ͩAg.ιw,Ts6;k[z#2T`}ɗkB&:1_y ds1>o 76P"PP˄gӤKӖ90R`ǍKzk`:Aۇ[ mv.k7y]gLeH4^WF9>>XTF#4*ҧԑx*Μ .ޠr1-S,Hzϝۈ5mh_8Yin`@& xz=r$@$@n^m,}s4Q"% ֘;mvP*U+OPWA XϺmQ>aMGk [n9Cl31G<6QFXWoD0}~#i hrqD^V؏D=@-HHB' fHH>CPY`vGkJo8>H{\T+*'q$@s$xE<%PJ9GbZYҤO"JAeJ>`>|Xm= ԛ6Յ=O9\Sm8go~XE|$@$@@ݻwȑ#-# xS?V'a —b4*~A^:cp0Rh@Mόԋ{.o:HH<=$@$ކRdj   ~4^M$@$@$@$@$@$@~e 5-bCHHHHHHH"6lX22 nD,HHH>D/I  PK Oǫ͇a$@$@$@$0>] 6HHHHHHB@^+(HHHHHHH RJ.A$@$@$@$@$@$@A%ŋ$@$@$\oZD_< $έoܸ)||$a8g@IDATng[G4q1 {CͥdGHH !zCRv#YlҺc7ٵ{TSiFf\z}keب|@st=omB]hАo_f鳜~޻_ڨ1mߡl 0a¼y<HHB-{Kreiú>c+ϟ?y򕫶xl*QC;=A/>,T4?qPxi ٲ}gh;A$@$@CסsHHB5jj#(,Z*|']iM[MrQiڪ4L= k?1x޻g;fWOvEgԻrz[޿ۂczwc3fϕN}/ e^]X*׬D{8ǣǏA:O F{~{E$\|EuAVԍg,202I "  "ʢA`SIHHFY/Wh%9gG<ߴo-Q"Gu>r̙2Ȃl/[FGJ޵gܸySƍ"jU}}kץi믥F2~Py8D5SF['q})QN%ĉVurWXN6XH*/+|_z 0mBУ] (E Ic'UhUN>#U)_5i(CF[oc U6=۷U5m[j&uj~ VGˠFIgufO 1Gqǜ=i(I|kdqU[={fN7 xo{5cIH\xlܲMʗ)e=Z43F ;G6!V'J(ʖ0|xa9sL?JFM/([Dׯ-ʔy{)u(\ T>C(acӧJe,f͝/Er)d>=QŞ)#xÔMP"+ b4EFKʔ,qQ|P]зU+ <~y'L ƍ"B$@$@Ez^l, ||N;ZXql_oVL(%-F:K3,s,i_q$O'K"S iBoM6wPO߶?)S9НQcEv(kUvI<޼zN$9$0B+|\Z^?V.!)_*THua?́|8c51cDBPs(w xY?2OGhߴe CEB?#R8j9f=nf7|lX@M320bN0N1J?StU6#  PKsCaHH&|]2i8L0w!.\pE N/0gg/֗.Q߹2C>}W ̗WN> җɀ)[,pdʘNџB]j5(O;S+ >jDI,Ny#n#ϩ3~g=.ž3gL#>D;qJ/WPjǺu ,Y+>(fBNeԿN; Q :B$@$@Hxf @DXS7O?/\aеtlBԩC?.UB?x ƱpӶ2J-@]m[J_t̷ީةW7acP=vVE[XϰcwKhw܁ :Qr:{̙ts> 3 ˪9O\Aon@X*:Z ʠ1G2-B>F:dbPHHHS ٳgo=l7 عs~´hD6мȀW]|Uvt/)qz5J,dz(QHx;+Mvc7,g/X?z(u/Ypξ-E&6682 ;palY2Hd90X-Ƕ+> t|ZZCΧwI$@$@AҼd6HHC#2oKZϚ+Ԣjռ\:o9V/SVh =ՑBcX]?\W !MU?^m_٧c? 7T1{R ($@$@$+v uR &_M[wHB%zŝx*u焛r: `Muբz  DT=$@$ކR|   p%z:HHHHHHH%5zHHHHHH^y?HHHM B$@$@$@$z{˳HHH=t/#s  RHHHHHHH<J!      <{5cIH#G '@O# eםQߺyW]z@r ]1z9x2jzEoW._P1~g\sg<7x M i,YǁxyWB]g$@$@0 ((G5+:~6L=&-8ޠz'?y.UJ7 ڟ4{RY`|ݢܼqNHK UN~6>nۼW-&{ߔ9|+X&Mk+I1mа p8~_yJJCqL@)nuξqP3~ /"#\_By0HH]B_^'흷o{Rmx{yxVi^YSKhe^p\goݤTT޲u///޻oJǃIԨeC:4gL#f4y9v~0Ej֭ EK~?#I'z_Tugۦ=2~L|隤I\tj(_?̖; :޺cGaqŒ/[גEs<?zFΟ,nޱw_:3D\zK9e-PJ\M?J1}[dO/ݿER$K1yQGgaǣ#>Џ >sT.-_â+* SK )%Z(RaeʛYjTZA2JRH3u]RI*{,JbqsI]_Gc-RQ}TS#GהEÛ@6ح -[C;KQipi١4DeR)WmH)'2|$Yz=*_Ǝny-Y=S/|W$ܗq &m{GO66P%ר 1aex7 j  RU$ax Sƒ}J1M׿; ɥ #Zy:|Ѽ6:K#})YoRU:zI#u\o3ޞIH93w.ǩIQl6pt xpÄy uB?]gk髕aRFvm;8sȏ"D U~7JZFyl/ʳː>cJ RޕsJ81VJ:\>*]"I)P8_ აxj_Sg1Gmr9r$}ܾMx<`~}bI,C)]̡9s9r(} zAdǾb;SfҡkcYl3{ӶѣGvhj՗Զ_NԘ0m@?'vGWc SH<ї[>ϟEkϷ%RWPru1؇a;Ϸ#=]ӧϔaV xB-ӆKٴa_<۷3ʺ\7eYJ[C&s:}$UF/L` (٤ &h_>7 0䍘26EV(?]ҧpda 6kP+y]"$*됊XQ1$  Y= =uw~:I j,bV`coY0CCJe??~ ߸oHH u¸HFe=0 c笔/.:?B >cɞ36TY d_1a|߯qD=3p΋%ӱunۂczfY :^?WW{||{=WaPHHH 4H+4YCDikMz)m (>Qxdx`AC[M)oK݅g؟1R;P=6Pf5sPO1dKȝQ+pCvd0^އ竍R2RB{Ag qԦ2o @y+d5kHD ސ/[^Gx`GVZ )苳:͹vn7oS[J}PU[`8 r5G?Ǐ~$/OG=*71.7:]yĠHkґ-Bs+1cEW~)AJ(KVK鴧Xst}gJƸe}IcIZ <"#um{۵6xܿP+~oZKZȅJlF?g/>x$Y<ۉm0@avu$Ktڈ ֙,݈Xq˅LdZ=f3> £GVU >~(W)S'/S׾.48*a5 苯5P ynKGk^VnUzM\lC 7o[=gmD^,[/7ၬ G ilp`pܸ~zXOFA )=s`c_Av;>x0=xb?V7RJ*qԵS!*,cɸG7U8?"O v]?ƒ}ژ~D "t0l,-_QpFDp[-4AڱesH )67ؐ $O<-Pݑ+ʛR#RRLZhGkP:xf &P<#xp>oU\70^3],ʺhoɃox(#0 &_`@0>| eHh ӉOPOq{,1uj00)kj,!r~L2kH8\wwzv1Q!ROHrIHH-@()V_z~x Zx*|n<.ƋUk5g^3S"_u Ns`Ӑ$GW@G'™ao]`J-" QR-|glZ%Z(XO`J\\ܢnj#JFr}C0;y`epVP0^P#ƚ7<$uFsc?;9ٶE'gk豽nxेwC&PEא <&1 gHƘQn!0.A6+OكdAEA¸ 5?zښՋ|FIo5Y[ yL kk/m S~07~`})9qtvFUc;ņ;MnOߘaw)a̓* P13tZ==+2ma&HHBX҅knՃxvf\ -(%˗`^ŅS =<b x0%sժG0%Ã* ':67ҜkvզǏ0M((e( (#P",٪Yن MxIbl S}.Bk <z.}JRBUo2ph](Kh)ƂFP P07PVȚ[=ӭLͩWc>o` }OچP]57XưUjoDx8"/=>엉:k\$M亼l930rL:X=L-z+[AD 5~F,UsW&xS1p_UAԍWmuXWtb0<1bD=jAL(&0Db<#~`ŵ+*/qX a{A? 0>}ylWfLYˇrH{ʌ{ z';oA5F-6Z58=V݆8tU}tC:cH%y]b\my9L\gw&p:_?b P$տdNYBBEGmbYRB e_~gݹs}׌2}};yyuhט]f8,x+^$K3#;M*pr^4e G-X x.6jҒ!q'ՇCfc'N-86G'DB@$'1~2fQ<+C>snbP$}mqVEz]9H˗ +eպ_w$QꌿowhLNU۲un}uk,ZRD(ҎIK#\Z=Y2/wp]yX<8Xk.#Ү-d|P,`{i X^([R)!W˭BѾ۱78ERFtK1`ƽ07#5AYA6g4NEu) gVNcM[}UG ,M_mux$cM,ڂPaG@@;#>^11cK3y *jkP?ePQ=J{ N복`7Txú7ѧUߌ]CWqQ/!Cs,5Չӱ+NcXѹM9hfNPs#&at\|p(n#Nb6cǘCPM|rX͝h|$raD/[{t rSw`,‹8/n oJIqz6w}X2vx}aRWw" \CQ&|bqw؊?t }@Q{ n7iBs8OϮx`v>AF㬂|'P(0` 3V}xK)ތ%|bKk1XB[IQ;Ǜɏn-zRN_kBI%&SqEB@ܯFz[\s1kHB~{S[?Ý[;}6P38Cqi0|S[wEJ%!&Ez]ޚy5T.УNzvla2\aݼ+79vcizv?z dkԜiߍӵv)SUAw69b\9խsRx} ku>Ϫ'XfHև*6앍S7msPnƕNϟݽ0.r@vuH7[ԡ UuU^^$p_ﭼa̒SKߥinn7cie؍]}>=_-2,p}XJؒb׮]wwKʤ)/Nc͗2B^x,2$%)^UZ/K߶ -ۛaeR}ϟ~;q |ص)Dpԧԫbm~HKXrΈA@I)v}hQ]'6B KB@D{PS! B@! ? ! B@! IݮHz] ! Hݏ͒6 ! B IK}OH! @%Pd{mcB@! RJB@! B@! !YSJB@! B@! %Q%])[! B@! =$ - 'pEi:Y=;?ղ>}NQ$W~?Ng&nݢCЕW-A?8YBN !=S B@{Jm߳'ݰ)-_oץع4-?[J'S&vמ{p$ZzeO9.YFM:miѶ̉˛:Zqŗ뙳7oޤ/T۷t嚴qw4tX#ӎu^xѸ~ߨ˝sP!^S -jWZS?H֮s:mO#e{1t$Trwsbܢ Z|?c|ީ4rGtu:~$[~\c]lNDw"#B@!d ܵjTD-4>@Q^e ?q,U]l>'8|<74PoM s<"z0g1cocinOgΡfalf37kw\kz1m~jF˼`$ k}E>_%5msMY[ܯ7=q%o6Ў]{B|IISgR]h;u/>1r=ߋmz:6*! QHB@$]P;8ܑJ1noZzֹ]ߣ hۙ ͰrK4,f )v6Y۪}x7i݁}k84>g<۵g8uQNBvN̘Eܾ}&OE^iuM>˵h{9ߴ5>7d ^I2)~xBOٱ>w<_sQP&fz{'L0yk6YgS}?_FH[ng(p|c>auy!6cq}}M7*>RdzS̙ERe+T ?8IU]E =&x 3m24nP/ V:/ZNH;wQ:52bENGs~}lmPV1RTӻhۨrrd}+ëׯVjWZCgϣ>=ጡV])m@Z*d1N5a"M4:cTteCk~=HYOI7mL۱NYوyР^ Llv+ Qno\Q,Q>D~< )0 {x 4Xqɛ' p]ѐ^R0P;kF akT\g!UT|Г)va*{}4v`59w|WX=wkӼ F Ԓ]dO]TG  ǢӆM} +5k5G^R_c)sϔU#Y2*9|!  ^D! @r!p56Vѥt>$O/Jb?ثW@)k7,nf1y:vJ!J+?/E)_Sг%KPҥ8OX˷k=K29Nib[(ŏim߱GBVk <#XoinbehλrFboLhִ Rm@ڴ4qp޷~5~Xk%g~(rWgz'ֵ$ìy YG駊J=<"?N)ONfoӂ>UPzX"k0n>­@bYBgϝc4_t #[A7͘!w봻E tԳ t1Ϭzp]sx0*Q1jH Rc{ 3vnvҾ'V8u7spr/< ?PWKM B|ƍ$zoL APs8! l ?fg%Q! @o?atG!GjB:xU8wHܽFIeFO*a(KLG|6^Y2g2!OY,wNjġ0UY?NqNp5ٳ> ,:[00%M4tK'RMba GZ~v|y-X![! ?/M̒&B mMƫi8g#s5Z"_FXgN6+X{]Yܴ:}|WW߬[.?644=[::>i-q2ޅ,Cb/_4\iXeL`B tJBP3}{ցJq̠kx=}-Ƽ3} 4>"ԩ3\۷ab쮼0V>yڴfZ ,]}?73SƯh`xO}}ͅXO`SZ2} "# Lߛ%ݜGBGFUKe2Gs tl";B@~/8$B_!Zc omG9 4vyaEXP4v k+/"G7֐#w}+Y/Syl!ڿ` n9 u{5 Smk:k)TztHm:gX1 2/[,Ǯ7`Gpi*$ىX~Y=/<,z UxSk͛4)Fh;ݎ9|!  عsŋ۟T! @TTGodɿC;Uu9r, l)KsщP.=0j| h{R^gݢސt!& cnezM녵NmEAxݍ[ʵ!ב {Bȩ=P.[#Ka'`Yjmּ&|-a+YEbHXZ7S}xFe0Xz`5 al5gX+tj5}_[t>e2Mٕgz/vα{O4M nX_'[! ]IB@!oV̛+]^5uv 6[Y쏨hpXW l4zuZ#"I7_g·}X?]^Xa2e}.Ϊ4kvn =\]gbDEx`@AF<'qJ9k1ibl%db^0#`NAҝOt] 2Oc}lNj۶Y8a:](B@! ٺ+wV^7k +W\fjT"T#zwv|VN''QkMN}Cy(zys|>|QYfMp> 1c(o>ķJ60|cSgΥKQoӟNdl8l9sNsV`\z,͛7 +פ[#yv]/ŋFE^̜zG 2lfmJ=oWuAZvi#~,;] #餚-%m+\gOuՏt떭B;t! HvK5VMsܹsǰ!q3% 禲Pv$Ns; s<"z0g1[^t}:s5 g3 ߍ0k~;?oceivטӶ~Z5e^0>"~O/ҒP6⦬- כq|>ni ; c_;\O䠁u7seG!@/E$Y! @$߱{Zak=Pi K+W`GYD2o bw`mӱ5wawhߌOpY Ls~]~ ykp<'M1}M6Ґ<}kp7zsZik>}U2RZ/Dbmn?a]X;9tюISgd,W^c 6&Ou-&6K!s{~ rWШinݾ}]~czQ?n2n\mxtc2{U-8ij/ȅ .ZkϑѼEK:wtM`xZn#Mqqn6Cqcn ! 8-iB@! X60h':zRF 虧d7e+VQ*<܊ ̯^c+]!4yHګ\ J9NY2gK}wBӸS4\U`ۣ+jʘ1- C*pmQpp+~T-LsNQ~|ȱݏ^Ul ڠc TRQw{oQ ZmLVW_#ԮXϞG}zCR]p6 HK%,ɰf?XIFg3v2=\.]l(b_a͏D~< )0 {x 4Xqɛ' p]ѐ^R0P;kF akT\g!UT|Г)d7FwGcG Vsg[}s6͛Pxn@-iiUK$E5~p9}6l>Kw?LUX^Yɴ>Bjy}6n)֨c]>M̾IB@!vmnUT0]p0 SŋRjX@q+G l`@ ۰fL/pݻRR KQ:bה-#lTt)-ZR;o+9m7kx,r$d@|yh||ȟ//=%3گ*yr #[DX\5Ѱ>X65mmd;H)y6-M3fegc>Z|nYމum50kBVyRHSS۴Ogaީ\O,%blv z/ J/e/\{9zD|yrӁ3n34c *y~-ToR.&B0)CRϑcb$Y!LB@!pOt~pQ?o~h?O !:I leɔIFX犵Y2g2!OY,w 0UY?Nt9f(+yA09o0NKᘏ?jP`& jըj3sLNO-ʭ=N1>:z,!ٳ1Qi-q2ޅ,Cb/_4\iXeL`B tJBP3}{ցJq̠kx=}-Ƽ3} 4>"ԩ3\۷ab쮼0V>yڴfZ ,]}?73SƯh`xO}}ͅXO`SZ2} \wwsp7K9-sT?O1RIKP: HVkLaH9':\FG;eB5XlkϊXCߥ =ӴR;xZߞR#(2$`\H8ȅ;-Z]tXow}~+l!e+3G#Y>|yW 86asLLQn 񅵮m,ŽZK*Ӵّ\vaܪ6{E,Vnb%q.֭>(p=ϖ5+I_rCb;.ո7'9r4?XR2|^b=,3cI ~.1ƷL&RWX=F =sƻXFXtlSVbc! ojF- n՗IBv:wCs2_By^u-PVlJL4zjV?ıvK0jԬ:WI)+齮e`(:k9qG ꧔\vTpsheIJ #B?ܯF(刐KEZEbgLZ| yԭ#?SL@`_#^4uSPޞXJWk#k(׮]'7an$e.|:ZkNw+7Ybߜĭ>lZT5låܷ%2*LϖQ &p햃h{7OR 0֚u!/RґtzV,>'0PFzEFL8MD0>;뱬4Btnǜ[>RܹNJB@{@ **VdI5\yvϙG'NAݻx_#<ܪ!XS캎aF7} f/)-uy- Io9&/ ߺy\,TdwX(JV*꺥\1o ,xЈ'EU8BHt fqb ekkb(o2ũ\I,䎥%xせ8Շg^X8Lh3Xc2al5gX+tj5jIrΧL5hKz&1!cL蝿pۉ6;y2_OûJy:'$u!t H{B@ʼ7W.`1UAk9 lQk'  '%.iNF7ER+PpיaOnEs'uiu*33p3wB2|)WיX,`P4IRuZ,t[| ؁ +8{nFtU 2O{)WCࡰdqL>]! Bl];Z؂ą+.LQ;Sq~ > Td"Uqc;=  [~(zys|>T rQYfM"3Rƌ'Lho/l xs&z&&Ҩ(Oɭ! @]v)VX,% ! p C1,B@@EH< B@! B >+iB@! B@8C㜕! \EB@! ?^""B"+IB@!  Ὡ"B@! B@! @# ! B@! I*"B@! B@$=bOzLZ,xjQNVO,uO>C'Nu~(+c3g%!ӡGիI;4Bt\+OǾ-yx0 k! @$pǻ%c{D6嫾~҃vMuT-ukϏԽO}8azDzΜK ,ߦ?O4phsDoMr Y7oR* JWI|GCG58_\煋}퇯ܙ9wen,z<0{"j?zu/9j:F42YvNCGI5)w7[K!-ڠP'O;5_}| ݸq8֫X-[! QHB@$Y;wU+Q&PTܹcXƐ8̒[FsÇsS)hx~ySV9N{v]@-/v)tj։-f:;ajkw\kz1m~jF˼`$ k}E>_%5msMY[ܯ7=q~r}v7hǮ=F!DŽ/4Zq M3r=ߋmz:6*! qw#B@!t @yw,pG7*ŤiF~Xw}~/Ԣmg4C6F>XQ,Ѱ̛-FXptfMv<{n]ݤu7#o\{'_ׯ_~s3r('!;'OISf"n߾j4&OZ4܍ޜoOD TK:)Ѷ @[~<~xBF۱>w<_sQΤY^ (eO<յ\\,7y)ڏqohevvoݾ}]~#m%]SL?z\mOnL}~e[xve?!rb(sgA4oR0[?=֮HS\dFRJ/w ڐ)OǺݲB@@yvX$M! 2X;l`8OsujG&3O=nVU*yJ>}M7*>RdzS̙ER]P4n?d$ Wu)XJ=002f`qU~Ax4wDڹ{ 5ש׏OZܱUuگϖ 9f@J*uzmvUPu6̔hex5JUkhyѧ=1۪/SZp6 HK%,ɰf?XIFg3v2=\.]l(b_a͏D~< )0 {x 4Xqɛ' p]ѐ^R0P;kF akT\g!UT|Г)x޽w1XMΝn=V&4oB=zFW-ǓxrvaӷAD_aBrJt@Me}WeP"g瞍iBlj㘫S!`K@z[,(IkhwۨZ F~>U(Vw`^}ݰҺ iq۽+(%`+VMٲ>Bϖ,AJ"N95_ Ӌec\1ZFJi1ynVv;ѾM tVzX"];qanD˲ ={=]3,B'p0~Q??O !:I leɔI^aϒ9|b{>^0RqG;? <{GYחϛɉ~qnBp/(ht VjT5ΙùMFl'W'Vc'֘=X2~lLy%O}&F EکSg۷o9d]y`P7-}i2zX~VofE;_q`8}-a9 >< v0~N5,tR.2~oXwsR1ѤU{z,$eX_'[! ?DIB@!oH[1ŷ# p?픅S*Wpk Jkװbq>+tc 9"~3OJ\#j}{Jt?$`\H8ȅ;-Z]tXow}~+l!e+3G#Y>|yW 86aM0I2EC k]? 7˗Y{뗪Ui#힭RUm(Xc˭d K5]֭[}Pz-kV^l* 2-}w \ q3Nrh4[R2|^b=,3cI ~.1ƷL&RWX=F =sF 5Rˊu^*6C<c$B@~nd! Z(eV} i-a7)s>4gg .U*f-۾e*+ޢիq!5kURJzk8o>ΚkNr:)r<3- ](c?"Cjj]R|ToDF=;e 'k۠nmVrUeN  r)3M|TJM^ XCYv:a}YgNj2[zzX5o뻕,1oNV Pj-MV6XQ[x ^gW`nvA۫oXk֑^WNHԣKGj?X=?Ho2/[,Ǯ7`Gpi*$ىXF1 Ua?dDL1qŻPV du 4k6{I)n ]G˳nQoHxP1y`\7nܤnezMEvNmEAxݍ[ʵ!ב {Bȩ=P.[#Ka'`Yjmּ&|-a+YEbHXZ7S}xFeH6c過Nk"1ΰVjBՒZ[t~wuk-AĄY0wy[`n'|?*zzh!@%o"L] ! @r%27W.`1UAk9 lQk'::)ּwsTNkps=P$2 |9akVt=w{}b]ɘM_82 03Ӭ 7sxw)(×ruၑ4Oo:Y4l12/X0Vv'qj ^giܠvN߫*peORɇCoY8a&JOB~!pqʝUMlZVlNݩ]8??oEv~Lhڔ;qLPI}NXo墲-6mD>g CyOԙ:^( &7L*JMLqO|RB@x B@! X*KL0t\! B@! @%7H4[! B@! H@EB@[/"B@!pwUB@!8y]$! B K}#! B@! x0H>K/B@! B K}2%! B@! x0R`gB 8բ4jY~>}NQ$W~?NgΞK2ݻu:|\dKC/x:l+I@.B@$Mwn{=~ U_qv]zЎJ2t?anw'L5_XԙsieԸӶ?>m{Μm\!W|9M*BE:޾}JWI|GCG58_\煋}퇯ܙ9wen,z<0{"j?zu/9j:F42YvNCGI5)w7[K!-ڠ`9)T7O=8~$[~\c]lN=Nd$]! ,REF`XmCOg3Kb`<7)-co~yS>+e=Y}"}2S9,[,t~7vFԎzt.>]cN kՌʗyHv,;u?}KKjC|?犛~4_oz"%o6Ў]{B|?wvv6} Q,_|r=ߋmz:6*! QHB@$]P;8ܑJ1noZzֹ]ߣ hۙ ͰrK4,f )v6Y۪}x7i݁}k8bשy \ 8yO2c6s󫵮J>}M7*>RdzS̙ER]P4n?d$ Wu)XJ=002f`ɼӸA]zϏ*XX%`ԉs:jկS/9#cѿ;ѫꜵ_-[As̰*U*n/6\Y4nk(Oz8c(U|lJJ>Ya~XH:flez8#y:]tPÚm9x0ad^Q}==o5rb>$}~5{+-Al-V6 M5zݹܭۣY\}pcxR` *n 1h7O. % s6<^1aԠn-n&FH˕}RN_?ܸ=x޽w1X)g[}s0(=zFW-E5~p9}6l>Kw?LUX^Yɴ>Bjy}ߩmk_;9b㘫S!`K@z[,(IkhwۨZ F~>U(Vw`^}ݰҺ iq۽+(%`+VMٲ>Bϖ,AJԤx;d! ':ݺpWGtJ/]˞T †q@^qQ̙ ERu S?\~!NXC=룬KQ*pɉ~e(:K-,s1\|J^Hȩ?kə#M2\90ښ[>zXcfoZ5X\cK9{6&<Ғ'wN˻Sr 6q Iˤ)vkmϕ]9iҤVI\t& Ka^^`CW ,-XS0>NiN[LiI6 RI,gƬO&'ISXX[8"!< =B@$E9 R@t+`# sw kdkݗj~a:& M ^qC*,+p,[Y]N3&'{; i-P!Hk\˕).5Egm~9Hb/c2f3apZ%NjrցJq̠kx=}-Ƽ3}4>"ԩ3\۷ab쮼0V>yڴfRۖw {+67C!7C4_ćb=p[NjɄ~N5pG:e,#Vshdž@[8o tːB[  $M! ?A m*ᜓ].Gp:\an'++?bq>+tc 9"~3OJ\#j}{Jt{8C0.oP|mfBUV-.^[>At?T•룡`||c+ }c@F<7$K_Xf_R+XT2MEhl]o#@/\[n&&Kx5NVdg˚פ/[ȡ1T ghv%+//2e&Sw5>5%6WZ"^dP= .i屲֢m yĞ9X9?3\R'6xs̙C!@'0PFzEFzw@" u^#AƮ;Eh|V>R޽NѢEJB@{@ **VdI5\yvϙG'NAݻx_#<ܪ!x?u:57@K' 7Yoݼu~gk*;P`%vj+ MnuR7XlhBN큢.&x=,Vךd>P`e6SȓX,KK7q( f,=u[xM$ֲ ZM(ZR_krΧL5k-AꙴƇ„ 9nh#s'+ Oyn@?l^x:NH9B@$ObO7Vz%HNUuEs,E {T0M _;uNP8)v}Ms\5B(ZcΜ~5^w+>dL˦\Y2_lfYõnpSQ/z%, 29S}Pʱ^Nc˜/!;aewuv hT~bc,'T@?,@?!ʾBCaaagiw ! HǏu Yz%.\qa< Qݝڵnjm O-^ךfӭPI}NXo墲-6mD>g CyOԙ:^( &7L*JMLqO|RB@x B@! X(q! B@! HTI~! B@! $Q.B@7/"B@!pw*! o^ioMB@! NMB@! B@! diB@! B@!~~/A! B@! @$$i\Z-S-Jeg?/@r%pt$=>BW^M2m k})[ !`k! @$pW޳'ݰ)-_oץع4-?[J'S&vמ{p$ZzeO9.YFM:miѶ̉˛:Zqŗ뙳7oޤ/T۷t嚴qw4tX#ӎu^xѸ~ߨ˝sP!^S -jWZS?H֮s:mO#e{1t$Trwsbܢ Z|寯bjj։8֫WǺ ! ᏭB@D`箽Tj%jѤ1w ʝ;w '~:Yo }Npyn* %i'_inaDzSD,ƾ]vˋ]fvy>9"X,t~7vFԎzt.>]cN kՌʗyHv,;u?}KKjC|?犛~4_oz"%o6Ў]{B| ?~ b&r=ߋmz:6*! w#B@!t @yw,PJ1noZzֹ]ߣ hۙ ͰK4,f ΣMz=U.lnҺ?qMRP{'_ׯ_~s3r('!;'OISf"n߾C͢j4&OZ4܍ޜoOL"e ^I2)~xBF۱>w<_sQΤY^ (l&LZ.Ml臿<K?ڏqohevvoݾ}]~#m%]SL?z\mOnL}~e[xve?!rb(sgA4oR0ݸq>=֮HS\d{EAg Kgٔ6 -|'Úb&uڱpFrut鲡 |5?ra e=&={{޴k:}H:f&jf#V-ZB{,Z09ۭ4l@8Gjs[G9D,@Tbw/c&`%o\4 4wjFCzIy@ܮ WL5[ۇ&RrelTS7.COߧva*{}4v`59w|WX=wkӼ F Ԓ]dO]TG nciæo郈tGÄ^镚5耚L#T˜Ws|ږ[˘@\Po'0<E!'lư/B@! k׮]mj*LCBxTZ)Pz vH6TIDATlǩ oRίX5e=[+]|-}֣)Ҝ6Mr 5n9ma2@<}N>L>ϗ͒_jWAg<9spPu"M yWhX^, ך6$Uʔ H&p1k~DeURĺd5o!+zXcnGE7ov?{6&<Ғ'wN˻Sr 6q Iˤ);CŊWT4G-jҦ=!-ܭ`) l3KsYCr{xp/&` No䞖4i-՟J0hM-[@Gx?`k^^MO7%nJB )Y궦[5i8g#s5Z"_FXgN6+X{ SϲWb4cXznjw-pEŸ\R\YStx[,_4\iXeL`B tJBP3}{ցJq̠kx=}-Ƽ3} 4>"ԩ3\۷ab쮼0V>yڴfZ ,]}?73SƯh`xO}}a N> v UK&s=I˄YbI w&wKpܮKw.m9Dx:L!p 'z2,*SpX|k;pINY8=rvP4v k+/"G7֐#wFC[%y(PݤМq6WPןm] -۾e*+ޢիq!5kURJzk8tn>ΚkNr:)r<3->=AlҪ=G1ѫu9J9"RVz)SG<oD_Cukϔj,p*qXHW'MTm:+{^E񓐠 M 5 ( K b!@0AJ' ) EDl#F'eMޝșSFбȑ53g ͢~ҷ#ƨMov5V6Kl 8иMGA6Xҿ{>tJÖ^6:#C+޿U8v8 ߵȨro΂8Z5ɈX0Kl8ܭIbqx|lLHJd|VJ1֫Ͽtv[/ &p1,,̺$@$@$:{;,m.GHYFk˶htHbkot3íc'iu$ r${3i]i_+ .i96/ oOu$3Kz(J yJzXk96BrR`qdS'\[ f0klܦ~m~a+YEoD&wgb7~Feplb= Bߠ6-i7+eRܴfs)~&!clL>9;|v|wCN=u<=sus$@wǙ uC]eR94T/_%UR/:Q%[,{Ur4Z VNIjiݸ13f7EP&3gX?qWV'{9lseT p톛9;)Ó~1 In<(刻7(-s+؁ H+YZ`Vv @#@>3ΘHHHHHH4HH|@    x HֺS$@$@$5i \q>+HHHHHHH ϓ'O5gL$@$@$@$@$@$@$ 8#B$@$@$@$@$@$@$Ys$@$@ #}zJ_GɹTd ڳ xBŋk^)YɣlK$@$@^%@KWq3  w[S>tcG/&U!Y-:lzw=Af{E.~Ų##G]ekѡ!{:]kj[wˡTZY^3C+7#ʧi%2eR:VG,sgNPƍ.}!q0ɛ7 6Jz(%ٺĔk 4<&yJʨxb%عKJ)%[r[f~sUmwJjyٷ<1b /U Q͎1+I$Nߙ0;5SϻbHHIzod_$@$@9N̙3Zm۲c. [k 7Պ;vˋ|I_7_}A'c➔BR]):ѧKH^mҤQ S>00@׎XIoP!6!{헅IAj*Wc|R3jKHG~V͛HmP%+ebrҢic}nX`mҼ,;Gh+9Vc['lRJ%YtSxX>;,m?SujI(Q;;`•}f|x|2:.Nj?FXO$@$@"@[$ "''GEFodG|_cSVs% f1|W~mwկ=U@zW&dpdlݶCߧ|E ">Uk֓ @61RD+vj~r_Qu\  2$n|\[2ߺySdpO?,խlX+qt9zrp}_?ny`.O(>7  |7ٵ޾[O|&MTn⺍zumsc=T<;c3#n@]rHc=}xbēpoG~cvx8$Z4ixfiyS۞HHH6lpN:} S:Yԇ;mʔ%st^\Eo/CzD"btb{ڴԏc''(w긳 /,8z?SbsRU4;2ٳBp&3qQ>02uhg 0:m㊈ȳF1ǜ= =Ova ʼ1ʡURV 1uFueѲwyeӴ3+Rp"}G}УsX״l-Iݺ} s}Z^~siB>gqq+n i2hA  We didn't have to specify any resolver for the Book's fields, this is because Strawberry adds a default for each field, returning the value of that field. ## Step 5: Create our schema and run it We have defined our data and query, now what we need to do is create a GraphQL schema and start the server. To create the schema add the following code: ```python schema = strawberry.Schema(query=Query) ``` Then run the following command ```shell strawberry dev schema ``` This will start a dev server, you should see the following output: ```text Running strawberry on http://0.0.0.0:8000/graphql 🍓 ``` ## Step 6: execute your first query We can now execute GraphQL queries. Strawberry comes with a tool called **GraphiQL**. To open it go to [http://0.0.0.0:8000/graphql](http://0.0.0.0:8000/graphql) You should see something like this: ![A view of the GraphiQL interface](./images/index-server.png) The GraphiQL UI includes: - A text area (to the left) for writing queries - A Play button (the triangle button in the middle) for executing queries - A text area (to the right) for viewing query results Views for schema inspection and generated documentation (via tabs on the right side) Our server supports a single query named books. Let's execute it! Paste the following string into the left area and then click the play button: ```graphql { books { title author } } ``` You should see the hardcoded data appear on the right side: ![A view of the GraphiQL interface after running a GraphQL query](./images/index-query-example.png) GraphQL allows clients to query only the fields they need, go ahead and remove `author` from the query and run it again. The response should now only show the title for each book. ## Next steps Well done! You just created your first GraphQL API using Strawberry 🙌! Check out the following resources to learn more about GraphQL and Strawberry. - [Schema Basics](./general/schema-basics.md) - [Resolvers](./types/resolvers.md) - [Deployment](./operations/deployment.md) strawberry-graphql-0.287.0/docs/integrations/000077500000000000000000000000001511033167500211705ustar00rootroot00000000000000strawberry-graphql-0.287.0/docs/integrations/aiohttp.md000066400000000000000000000200151511033167500231600ustar00rootroot00000000000000--- title: AIOHTTP --- # AIOHTTP Strawberry comes with a basic AIOHTTP integration. It provides a view that you can use to serve your GraphQL schema: ```python import strawberry import asyncio from aiohttp import web from strawberry.aiohttp.views import GraphQLView from typing import AsyncGenerator @strawberry.type class Query: @strawberry.field def hello(self, name: str = "World") -> str: return f"Hello, {name}!" @strawberry.type class Subscription: @strawberry.subscription async def count(self, to: int = 100) -> AsyncGenerator[int, None]: for i in range(to): yield i await asyncio.sleep(1) schema = strawberry.Schema(query=Query, subscription=Subscription) app = web.Application() app.router.add_route("*", "/graphql", GraphQLView(schema=schema)) web.run_app(app) ``` ## Options The `GraphQLView` accepts the following options at the moment: - `schema`: mandatory, the schema created by `strawberry.Schema`. - `graphql_ide`: optional, defaults to `"graphiql"`, allows to choose the GraphQL IDE interface (one of `graphiql`, `apollo-sandbox` or `pathfinder`) or to disable it by passing `None`. - `allow_queries_via_get`: optional, defaults to `True`, whether to enable queries via `GET` requests - `multipart_uploads_enabled`: optional, defaults to `False`, controls whether to enable multipart uploads. Please make sure to consider the [security implications mentioned in the GraphQL Multipart Request Specification](https://github.com/jaydenseric/graphql-multipart-request-spec/blob/master/readme.md#security) when enabling this feature. ## Extending the view The base `GraphQLView` class can be extended by overriding any of the following methods: - `async def get_context(self, request: Request, response: Union[Response, WebSocketResponse]) -> Context` - `async def get_root_value(self, request: Request) -> Optional[RootValue]` - `async def process_result(self, request: Request, result: ExecutionResult) -> GraphQLHTTPResponse` - `def decode_json(self, data: Union[str, bytes]) -> object` - `def encode_json(self, data: object) -> str | bytes` - `async def render_graphql_ide(self, request: Request) -> Response` - `async def on_ws_connect(self, context: Context) -> Union[UnsetType, None, Dict[str, object]]` ### get_context By overriding `GraphQLView.get_context` you can provide a custom context object for your resolvers. You can return anything here; by default GraphQLView returns a dictionary with the request. ```python import strawberry from typing import Union from strawberry.types import Info from strawberry.aiohttp.views import GraphQLView from aiohttp.web import Request, Response, WebSocketResponse class MyGraphQLView(GraphQLView): async def get_context( self, request: Request, response: Union[Response, WebSocketResponse] ): return {"request": request, "response": response, "example": 1} @strawberry.type class Query: @strawberry.field def example(self, info: strawberry.Info) -> str: return str(info.context["example"]) ``` Here we are returning a custom context dictionary that contains only one item called `"example"`. Then we can use the context in a resolver. In this case the resolver will return `1`. ### get_root_value By overriding `GraphQLView.get_root_value` you can provide a custom root value for your schema. This is probably not used a lot but it might be useful in certain situations. Here's an example: ```python import strawberry from aiohttp.web import Request from strawberry.aiohttp.views import GraphQLView class MyGraphQLView(GraphQLView): async def get_root_value(self, request: Request): return Query(name="Patrick") @strawberry.type class Query: name: str ``` Here we configure a Query where requesting the `name` field will return `"Patrick"` through the custom root value. ### process_result By overriding `GraphQLView.process_result` you can customize and/or process results before they are sent to a client. This can be useful for logging errors, or even hiding them (for example to hide internal exceptions). It needs to return an object of `GraphQLHTTPResponse` and accepts the request and execution result. ```python from aiohttp.web import Request from strawberry.aiohttp.views import GraphQLView from strawberry.http import GraphQLHTTPResponse from strawberry.types import ExecutionResult class MyGraphQLView(GraphQLView): async def process_result( self, request: Request, result: ExecutionResult ) -> GraphQLHTTPResponse: data: GraphQLHTTPResponse = {"data": result.data} if result.errors: data["errors"] = [err.formatted for err in result.errors] return data ``` In this case we are doing the default processing of the result, but it can be tweaked based on your needs. ### decode_json `decode_json` allows to customize the decoding of HTTP and WebSocket JSON requests. By default we use `json.loads` but you can override this method to use a different decoder. ```python from strawberry.aiohttp.views import GraphQLView from typing import Union import orjson class MyGraphQLView(GraphQLView): def decode_json(self, data: Union[str, bytes]) -> object: return orjson.loads(data) ``` Make sure your code raises `json.JSONDecodeError` or a subclass of it if the JSON cannot be decoded. The library shown in the example above, `orjson`, does this by default. ### encode_json `encode_json` allows to customize the encoding of HTTP and WebSocket JSON responses. By default we use `json.dumps` but you can override this method to use a different encoder. ```python import json from strawberry.aiohttp.views import GraphQLView class MyGraphQLView(GraphQLView): def encode_json(self, data: object) -> str | bytes: return json.dumps(data, indent=2) ``` ### render_graphql_ide In case you need more control over the rendering of the GraphQL IDE than the `graphql_ide` option provides, you can override the `render_graphql_ide` method. ```python from aiohttp.web import Request, Response from strawberry.aiohttp.views import GraphQLView class MyGraphQLView(GraphQLView): async def render_graphql_ide(self, request: Request) -> Response: custom_html = """

Custom GraphQL IDE

""" return Response(text=custom_html, content_type="text/html") ``` ### on_ws_connect By overriding `on_ws_connect` you can customize the behavior when a `graphql-ws` or `graphql-transport-ws` connection is established. This is particularly useful for authentication and authorization. By default, all connections are accepted. To manually accept a connection, return `strawberry.UNSET` or a connection acknowledgment payload. The acknowledgment payload will be sent to the client. Note that the legacy protocol does not support `None`/`null` acknowledgment payloads, while the new protocol does. Our implementation will treat `None`/`null` payloads the same as `strawberry.UNSET` in the context of the legacy protocol. To reject a connection, raise a `ConnectionRejectionError`. You can optionally provide a custom error payload that will be sent to the client when the legacy GraphQL over WebSocket protocol is used. ```python from typing import Dict from strawberry.exceptions import ConnectionRejectionError from strawberry.aiohttp.views import GraphQLView class MyGraphQLView(GraphQLView): async def on_ws_connect(self, context: Dict[str, object]): connection_params = context["connection_params"] if not isinstance(connection_params, dict): # Reject without a custom graphql-ws error payload raise ConnectionRejectionError() if connection_params.get("password") != "secret": # Reject with a custom graphql-ws error payload raise ConnectionRejectionError({"reason": "Invalid password"}) if username := connection_params.get("username"): # Accept with a custom acknowledgment payload return {"message": f"Hello, {username}!"} # Accept without a acknowledgment payload return await super().on_ws_connect(context) ``` strawberry-graphql-0.287.0/docs/integrations/asgi.md000066400000000000000000000214201511033167500224340ustar00rootroot00000000000000--- title: ASGI --- # ASGI Strawberry comes with a basic ASGI integration. It provides an app that you can use to serve your GraphQL schema. Before using Strawberry's ASGI support make sure you install all the required dependencies by running: ```shell pip install 'strawberry-graphql[asgi]' ``` Once that's done you can use Strawberry with ASGI like so: ```python # server.py from strawberry.asgi import GraphQL from api.schema import schema app = GraphQL(schema) ``` Every ASGI server will accept this `app` instance to start the server. For example if you're using [uvicorn](https://pypi.org/project/uvicorn/) you run the app with `uvicorn server:app` ## Options The `GraphQL` app accepts the following options at the moment: - `schema`: mandatory, the schema created by `strawberry.Schema`. - `graphql_ide`: optional, defaults to `"graphiql"`, allows to choose the GraphQL IDE interface (one of `graphiql`, `apollo-sandbox` or `pathfinder`) or to disable it by passing `None`. - `allow_queries_via_get`: optional, defaults to `True`, whether to enable queries via `GET` requests - `multipart_uploads_enabled`: optional, defaults to `False`, controls whether to enable multipart uploads. Please make sure to consider the [security implications mentioned in the GraphQL Multipart Request Specification](https://github.com/jaydenseric/graphql-multipart-request-spec/blob/master/readme.md#security) when enabling this feature. ## Extending the view The base `GraphQL` class can be extended by overriding any of the following methods: - `async def get_context(self, request: Union[Request, WebSocket], response: Union[Response, WebSocket]) -> Context` - `async def get_root_value(self, request: Union[Request, WebSocket]) -> Optional[RootValue]` - `async def process_result(self, request: Request, result: ExecutionResult) -> GraphQLHTTPResponse` - `def decode_json(self, data: Union[str, bytes]) -> object` - `def encode_json(self, data: object) -> str | bytes` - `async def render_graphql_ide(self, request: Request) -> Response` - `async def on_ws_connect(self, context: Context) -> Union[UnsetType, None, Dict[str, object]]` ### get_context `get_context` allows to provide a custom context object that can be used in your resolver. You can return anything here, by default we return a dictionary with the request and the response. ```python import strawberry from typing import Union from strawberry.asgi import GraphQL from starlette.requests import Request from starlette.responses import Response class MyGraphQL(GraphQL): async def get_context( self, request: Union[Request, WebSocket], response: Optional[Response] = None ): return {"example": 1} @strawberry.type class Query: @strawberry.field def example(self, info: strawberry.Info) -> str: return str(info.context["example"]) ``` Here we are returning a custom context dictionary that contains only one item called "example". Then we use the context in a resolver, the resolver will return "1" in this case. #### Setting response headers It is possible to use `get_context` to set response headers. A common use case might be cookie-based user authentication, where your login mutation resolver needs to set a cookie on the response. This is possible by updating the response object contained inside the context of the `Info` object. ```python import strawberry @strawberry.type class Mutation: @strawberry.mutation def login(self, info: strawberry.Info) -> bool: token = do_login() info.context["response"].set_cookie(key="token", value=token) return True ``` #### Setting background tasks Similarly, [background tasks](https://www.starlette.io/background/) can be set on the response via the context: ```python import strawberry from starlette.background import BackgroundTask async def notify_new_flavour(name: str): ... @strawberry.type class Mutation: @strawberry.mutation def create_flavour(self, name: str, info: strawberry.Info) -> bool: info.context["response"].background = BackgroundTask(notify_new_flavour, name) ``` ### get_root_value `get_root_value` allows to provide a custom root value for your schema, this is probably not used a lot but it might be useful in certain situations. Here's an example: ```python import strawberry from typing import Union from strawberry.asgi import GraphQL from starlette.requests import Request from starlette.websockets import WebSocket class MyGraphQL(GraphQL): async def get_root_value(self, request: Union[Request, WebSocket]): return Query(name="Patrick") @strawberry.type class Query: name: str ``` Here we are returning a Query where the name is "Patrick", so we when requesting the field name we'll return "Patrick" in this case. ### process_result `process_result` allows to customize and/or process results before they are sent to the clients. This can be useful logging errors or hiding them (for example to hide internal exceptions). It needs to return an object of `GraphQLHTTPResponse` and accepts the request and the execution results. ```python from strawberry.asgi import GraphQL from strawberry.http import GraphQLHTTPResponse from strawberry.types import ExecutionResult from starlette.requests import Request class MyGraphQL(GraphQL): async def process_result( self, request: Request, result: ExecutionResult ) -> GraphQLHTTPResponse: data: GraphQLHTTPResponse = {"data": result.data} if result.errors: data["errors"] = [err.formatted for err in result.errors] return data ``` In this case we are doing the default processing of the result, but it can be tweaked based on your needs. ### decode_json `decode_json` allows to customize the decoding of HTTP JSON requests. By default we use `json.loads` but you can override this method to use a different decoder. ```python from strawberry.asgi import GraphQL from typing import Union import orjson class MyGraphQLView(GraphQL): def decode_json(self, data: Union[str, bytes]) -> object: return orjson.loads(data) ``` Make sure your code raises `json.JSONDecodeError` or a subclass of it if the JSON cannot be decoded. The library shown in the example above, `orjson`, does this by default. ### encode_json `encode_json` allows to customize the encoding of HTTP and WebSocket JSON responses. By default we use `json.dumps` but you can override this method to use a different encoder. ```python import json from strawberry.asgi import GraphQL class MyGraphQLView(GraphQL): def encode_json(self, data: object) -> str | bytes: return json.dumps(data, indent=2) ``` ### render_graphql_ide In case you need more control over the rendering of the GraphQL IDE than the `graphql_ide` option provides, you can override the `render_graphql_ide` method. ```python from strawberry.asgi import GraphQL from starlette.responses import HTMLResponse, Response class MyGraphQL(GraphQL): async def render_graphql_ide(self, request: Request) -> Response: custom_html = """

Custom GraphQL IDE

""" return HTMLResponse(custom_html) ``` ### on_ws_connect By overriding `on_ws_connect` you can customize the behavior when a `graphql-ws` or `graphql-transport-ws` connection is established. This is particularly useful for authentication and authorization. By default, all connections are accepted. To manually accept a connection, return `strawberry.UNSET` or a connection acknowledgment payload. The acknowledgment payload will be sent to the client. Note that the legacy protocol does not support `None`/`null` acknowledgment payloads, while the new protocol does. Our implementation will treat `None`/`null` payloads the same as `strawberry.UNSET` in the context of the legacy protocol. To reject a connection, raise a `ConnectionRejectionError`. You can optionally provide a custom error payload that will be sent to the client when the legacy GraphQL over WebSocket protocol is used. ```python from typing import Dict from strawberry.exceptions import ConnectionRejectionError from strawberry.asgi import GraphQL class MyGraphQL(GraphQL): async def on_ws_connect(self, context: Dict[str, object]): connection_params = context["connection_params"] if not isinstance(connection_params, dict): # Reject without a custom graphql-ws error payload raise ConnectionRejectionError() if connection_params.get("password") != "secret": # Reject with a custom graphql-ws error payload raise ConnectionRejectionError({"reason": "Invalid password"}) if username := connection_params.get("username"): # Accept with a custom acknowledgment payload return {"message": f"Hello, {username}!"} # Accept without a acknowledgment payload return await super().on_ws_connect(context) ``` strawberry-graphql-0.287.0/docs/integrations/chalice.md000066400000000000000000000134201511033167500231020ustar00rootroot00000000000000--- title: Chalice --- # Chalice Strawberry comes with an AWS Chalice integration. It provides a view that you can use to serve your GraphQL schema: Use the Chalice CLI to create a new project ```shell chalice new-project badger-project cd badger-project ``` Replace the contents of app.py with the following: ```python from chalice import Chalice from chalice.app import Request, Response import strawberry from strawberry.chalice.views import GraphQLView app = Chalice(app_name="BadgerProject") @strawberry.type class Query: @strawberry.field def greetings(self) -> str: return "hello from the illustrious stack badger" @strawberry.type class Mutation: @strawberry.mutation def echo(self, string_to_echo: str) -> str: return string_to_echo schema = strawberry.Schema(query=Query, mutation=Mutation) view = GraphQLView(schema=schema) @app.route("/graphql", methods=["GET", "POST"], content_types=["application/json"]) def handle_graphql() -> Response: request: Request = app.current_request result = view.execute_request(request) return result ``` And then run `chalice local` to start the localhost ```shell chalice local ``` The GraphiQL interface can then be opened in your browser on http://localhost:8000/graphql ## Options The `GraphQLView` accepts two options at the moment: - `schema`: mandatory, the schema created by `strawberry.Schema`. - `graphiql`: optional, defaults to `True`, whether to enable the GraphiQL interface. ## Extending the view The base `GraphQLView` class can be extended by overriding any of the following methods: - `def get_context(self, request: Request, response: TemporalResponse) -> Context` - `def get_root_value(self, request: Request) -> Optional[RootValue]` - `def process_result(self, request: Request, result: ExecutionResult) -> GraphQLHTTPResponse` - `def decode_json(self, data: Union[str, bytes]) -> object` - `def encode_json(self, data: object) -> str | bytes` - `def render_graphql_ide(self, request: Request) -> Response` ### get_context `get_context` allows to provide a custom context object that can be used in your resolver. You can return anything here, by default we return a dictionary with the request. By default; the `Response` object from `flask` is injected via the parameters. ```python import strawberry from strawberry.chalice.views import GraphQLView from strawberry.http.temporal import TemporalResponse from chalice.app import Request class MyGraphQLView(GraphQLView): def get_context(self, request: Request, response: TemporalResponse): return {"example": 1} @strawberry.type class Query: @strawberry.field def example(self, info: strawberry.Info) -> str: return str(info.context["example"]) ``` Here we are returning a custom context dictionary that contains only one item called "example". Then we use the context in a resolver, the resolver will return "1" in this case. ### get_root_value `get_root_value` allows to provide a custom root value for your schema, this is probably not used a lot but it might be useful in certain situations. Here's an example: ```python import strawberry from strawberry.chalice.views import GraphQLView class MyGraphQLView(GraphQLView): def get_root_value(self): return Query(name="Patrick") @strawberry.type class Query: name: str ``` Here we are returning a Query where the name is "Patrick", so we when requesting the field name we'll return "Patrick" in this case. ### process_result `process_result` allows to customize and/or process results before they are sent to the clients. This can be useful logging errors or hiding them (for example to hide internal exceptions). It needs to return an object of `GraphQLHTTPResponse` and accepts the execution result. ```python from strawberry.http import GraphQLHTTPResponse from strawberry.types import ExecutionResult from strawberry.chalice.views import GraphQLView class MyGraphQLView(GraphQLView): def process_result(self, result: ExecutionResult) -> GraphQLHTTPResponse: data: GraphQLHTTPResponse = {"data": result.data} if result.errors: data["errors"] = [err.formatted for err in result.errors] return data ``` In this case we are doing the default processing of the result, but it can be tweaked based on your needs. ### decode_json `decode_json` allows to customize the decoding of HTTP JSON requests. By default we use `json.loads` but you can override this method to use a different decoder. ```python from strawberry.chalice.views import GraphQLView from typing import Union import orjson class MyGraphQLView(GraphQLView): def decode_json(self, data: Union[str, bytes]) -> object: return orjson.loads(data) ``` Make sure your code raises `json.JSONDecodeError` or a subclass of it if the JSON cannot be decoded. The library shown in the example above, `orjson`, does this by default. ### encode_json `encode_json` allows to customize the encoding of HTTP and WebSocket JSON responses. By default we use `json.dumps` but you can override this method to use a different encoder. ```python import json from strawberry.chalice.views import GraphQLView class MyGraphQLView(GraphQLView): def encode_json(self, data: object) -> str | bytes: return json.dumps(data, indent=2) ``` ### render_graphql_ide In case you need more control over the rendering of the GraphQL IDE than the `graphql_ide` option provides, you can override the `render_graphql_ide` method. ```python from strawberry.chalice.views import GraphQLView from chalice.app import Request, Response class MyGraphQLView(GraphQLView): def render_graphql_ide(self, request: Request) -> Response: custom_html = """

Custom GraphQL IDE

""" return Response(custom_html, headers={"Content-Type": "text/html"}) ``` strawberry-graphql-0.287.0/docs/integrations/channels.md000066400000000000000000000625361511033167500233210ustar00rootroot00000000000000--- title: Channels --- # Channels Strawberry provides support for [Channels](https://channels.readthedocs.io/) with [Consumers](https://channels.readthedocs.io/en/stable/topics/consumers.html) to provide GraphQL support over WebSockets and HTTP. ## Introduction While Channels does require Django to be installed as a dependency, you can actually run this integration without using Django's request handler. However, the most common use case will be to run a normal Django project with GraphQL subscriptions support, typically taking advantage of the Channel Layers functionality which is exposed through the Strawberry integration. --- ## Getting Started ### Pre-requisites Make sure you have read the following Channels documentation: - [Introduction](https://channels.readthedocs.io/en/stable/introduction.html#) - [Tutorial](https://channels.readthedocs.io/en/stable/tutorial/index.html) - [Consumers](https://channels.readthedocs.io/en/stable/topics/consumers.html) - And our [Subscriptions](../general/subscriptions) documentation. If you have read the Channels documentation, You should know by now that: 1. ASGI application is a callable that can handle multiple send / receive operations without the need of a new application instance. 2. Channels is all about making ASGI applications instances (whether in another processes or in another machine) talk to each other seamlessly. 3. A `scope` is a single connection represented by a dict, whether it would be a websocket or an HTTP request or another protocol. 4. A `Consumer` is an ASGI application abstraction that helps to handle a single scope. ### Installation Before using Strawberry's Channels support, make sure you install all the required dependencies by running: ```shell pip install 'strawberry-graphql[channels]' ``` --- ## Tutorial _The following example will pick up where the Channels tutorials left off._ By the end of this tutorial, You will have a graphql chat subscription that will be able to talk with the channels chat consumer from the tutorial. ### Types setup First, let's create some Strawberry-types for the chat. ```python # mysite/gqlchat/subscription.py import strawberry @strawberry.input class ChatRoom: room_name: str @strawberry.type class ChatRoomMessage: room_name: str current_user: str message: str ``` ### Channel Layers The Context for Channels integration includes the consumer, which has an instance of the channel layer and the consumer's channel name. This tutorial is an example of how this can be used in the schema to provide subscriptions to events generated by background tasks or the web server. Even if these are executed in other threads, processes, or even other servers, if you are using a Layer backend like Redis or RabbitMQ you should receive the events. To set this up, you'll need to make sure Channel Layers is configured as per the [documentation](https://channels.readthedocs.io/en/stable/topics/channel_layers.html). Then you'll want to add a subscription that accesses the channel layer and joins one or more broadcast groups. Since listening for events and passing them along to the client is a common use case, the base consumer provides a high level API for that using a generator pattern, as we will see below. ### The chat subscription Now we will create the chat [subscription](../general/subscriptions.md). ```python # mysite/gqlchat/subscription.py import os import threading from typing import AsyncGenerator, List @strawberry.type class Subscription: @strawberry.subscription async def join_chat_rooms( self, info: strawberry.Info, rooms: List[ChatRoom], user: str, ) -> AsyncGenerator[ChatRoomMessage, None]: """Join and subscribe to message sent to the given rooms.""" ws = info.context["ws"] channel_layer = ws.channel_layer room_ids = [f"chat_{room.room_name}" for room in rooms] for room in room_ids: # Join room group await channel_layer.group_add(room, ws.channel_name) for room in room_ids: await channel_layer.group_send( room, { "type": "chat.message", "room_id": room, "message": f"process: {os.getpid()} thread: {threading.current_thread().name}" f" -> Hello my name is {user}!", }, ) async with ws.listen_to_channel("chat.message", groups=room_ids) as cm: async for message in cm: if message["room_id"] in room_ids: yield ChatRoomMessage( room_name=message["room_id"], message=message["message"], current_user=user, ) ``` Explanation: `Info.context["ws"]` or `Info.context["request"]` is a pointer to the [`ChannelsConsumer`](#channelsconsumer) instance. Here we have first sent a message to all the channel_layer groups (specified in the subscription argument `rooms`) that we have joined the chat. The `ChannelsConsumer` instance is shared between all subscriptions created in a single websocket connection. The `ws.listen_to_channel` context manager will return a function to yield all messages sent using the given message `type` (`chat.message` in the above example) but does not ensure that the message was sent to the same group or groups that it was called with - if another subscription using the same `ChannelsConsumer` also uses `ws.listen_to_channel` with some other group names, those will be returned as well. In the example we ensure `message["room_id"] in room_ids` before passing messages on to the subscription client to ensure subscriptions only receive messages for the chat rooms requested in that subscription. We do not need to call `await channel_layer.group_add(room, ws.channel_name)` if we don't want to send an initial message while instantiating the subscription. It is handled by `ws.listen_to_channel`. ### Chat message mutation If you noticed, the subscription client can't send a message willingly. You will have to create a mutation for sending messages via the `channel_layer` ```python # mysite/gqlchat/subscription.py from channels.layers import get_channel_layer @strawberry.type class Mutation: @strawberry.mutation async def send_chat_message( self, info: strawberry.Info, room: ChatRoom, message: str, ) -> None: channel_layer = get_channel_layer() await channel_layer.group_send( f"chat_{room.room_name}", { "type": "chat.message", "room_id": f"chat_{room.room_name}", "message": message, }, ) ``` ### Creating the consumers All we did so far is useless without creating an asgi consumer for our schema. The easiest way to do that is to use the [`GraphQLProtocolTypeRouter`](#graphqlprotocoltyperouter) which will wrap your Django application, and route **HTTP and websockets** for `"/graphql"` to Strawberry, while sending all other requests to Django. You'll need to modify the `myproject.asgi.py` file from the Channels instructions to look something like this: ```python import os from django.core.asgi import get_asgi_application from strawberry.channels import GraphQLProtocolTypeRouter os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") django_asgi_app = get_asgi_application() # Import your Strawberry schema after creating the django ASGI application # This ensures django.setup() has been called before any ORM models are imported # for the schema. from mysite.graphql import schema application = GraphQLProtocolTypeRouter( schema, django_application=django_asgi_app, ) ``` This approach is not very flexible, taking away some useful capabilities of Channels. For more complex deployments, i.e you want to integrate several ASGI applications on different URLs and protocols, like what is described in the [Channels documentation](https://channels.readthedocs.io/en/stable/topics/protocols.html). You will probably craft your own [`ProtocolTypeRouter`](https://channels.readthedocs.io/en/stable/topics/routing.html#protocoltyperouter). An example of this (continuing from channels tutorial) would be: ```python import os from channels.auth import AuthMiddlewareStack from channels.routing import ProtocolTypeRouter, URLRouter from django.core.asgi import get_asgi_application from django.urls import re_path from strawberry.channels import GraphQLHTTPConsumer, GraphQLWSConsumer os.environ.setdefault("DJANGO_SETTINGS_MODULE", "berry.settings") django_asgi_app = get_asgi_application() # Import your Strawberry schema after creating the django ASGI application # This ensures django.setup() has been called before any ORM models are imported # for the schema. from chat import routing from mysite.graphql import schema gql_http_consumer = AuthMiddlewareStack(GraphQLHTTPConsumer.as_asgi(schema=schema)) gql_ws_consumer = GraphQLWSConsumer.as_asgi(schema=schema) websocket_urlpatterns = routing.websocket_urlpatterns + [ re_path(r"graphql", gql_ws_consumer), ] application = ProtocolTypeRouter( { "http": URLRouter( [ re_path("^graphql", gql_http_consumer), re_path( "^", django_asgi_app ), # This might be another endpoint in your app ] ), "websocket": AuthMiddlewareStack(URLRouter(websocket_urlpatterns)), } ) ``` This example demonstrates some ways that Channels can be set up to handle routing. A very common scenario will be that you want user and session information inside the GraphQL context, which the AuthMiddlewareStack wrapper above will provide. It might be apparent by now, there's no reason at all why you couldn't run a Channels server without any Django ASGI application at all. However, take care to ensure you run `django.setup()` instead of `get_asgi_application()`, if you need any Django ORM or other Django features in Strawberry. ### Running our example First run your asgi application _(The ProtocolTypeRouter)_ using your asgi server. If you are coming from the channels tutorial, there is no difference. Then open three different tabs on your browser and go to the following URLs: 1. `localhost:8000/graphql` 2. `localhost:8000/graphql` 3. `localhost:8000/chat` If you want, you can run 3 different instances of your application with different ports it should work the same! On tab #1 start the subscription: ```graphql subscription SubscribeToChatRooms { joinChatRooms( rooms: [{ roomName: "room1" }, { roomName: "room2" }] user: "foo" ) { roomName message currentUser } } ``` On tab #2 we will run `sendChatMessage` mutation: ```graphql mutation echo { sendChatMessage(message: "hello room 1", room: { roomName: "room1" }) } ``` On tab #3 we will join the room you subscribed to ("room1") and start chatting. Before we do that there is a slight change we need to make in the `ChatConsumer` you created with channels in order to make it compatible with our `ChatRoomMessage` type. ```python # Send message to room group await self.channel_layer.group_send( self.room_group_name, { "type": "chat.message", "room_id": self.room_group_name, # <<< here is the change "message": f"process is {os.getpid()}, Thread is {threading.current_thread().name}" f" -> {message}", }, ) ``` Look here for some more complete examples: 1. The [Strawberry Examples repo](https://github.com/strawberry-graphql/examples) contains a basic example app demonstrating subscriptions with Channels. --- ### Confirming GraphQL Subscriptions By default no confirmation message is sent to the GraphQL client once the subscription has started. However, this is useful to be able to synchronize actions and detect communication errors. The code below shows how the above example can be adapted to send a null from the server to the client to confirm that the subscription has successfully started. This includes confirming that the Channels layer subscription has started. ```python # mysite/gqlchat/subscription.py @strawberry.type class Subscription: @strawberry.subscription async def join_chat_rooms( self, info: strawberry.Info, rooms: List[ChatRoom], user: str, ) -> AsyncGenerator[ChatRoomMessage | None, None]: ... async with ws.listen_to_channel("chat.message", groups=room_ids) as cm: yield None async for message in cm: if message["room_id"] in room_ids: yield ChatRoomMessage( room_name=message["room_id"], message=message["message"], current_user=user, ) ``` Note the change in return signature for `join_chat_rooms` and the `yield None` after entering the `listen_to_channel` context manger. ## Testing We provide a minimal application communicator (`GraphQLWebsocketCommunicator`) for subscribing. Here is an example based on the tutorial above: _Make sure you have pytest-async installed_ ```python from channels.testing import WebsocketCommunicator import pytest from myapp.asgi import application # your channels asgi from strawberry.channels.testing import GraphQLWebsocketCommunicator @pytest.fixture async def gql_communicator() -> GraphQLWebsocketCommunicator: client = GraphQLWebsocketCommunicator(application=application, path="/graphql") await client.gql_init() yield client await client.disconnect() chat_subscription_query = """ subscription fooChat { joinChatRooms( rooms: [{ roomName: "room1" }, { roomName: "room2" }] user: "foo"){ roomName message currentUser } } """ @pytest.mark.asyncio async def test_joinChatRooms_sends_welcome_message(gql_communicator): async for result in gql_communicator.subscribe(query=chat_subscription_query): data = result.data assert data["currentUser"] == "foo" assert "room1" in data["roomName"] assert "hello" in data["message"] ``` In order to test a real server connection we can use python [gql client](https://github.com/graphql-python/gql) and channels [`ChannelsLiveServerTestCase`](https://channels.readthedocs.io/en/latest/topics/testing.html#channelsliveservertestcase). This example is based on the extended `ChannelsLiveServerTestCase` class from channels tutorial part 4. **You cannot run this test with a pytest session.** Add this test in your `ChannelsLiveServerTestCase` extended class: ```python from gql import Client, gql from gql.transport.websockets import WebsocketsTransport def test_send_message_via_channels_chat_joinChatRooms_recieves(self): transport = WebsocketsTransport(url=self.live_server_ws_url + "/graphql") client = Client( transport=transport, fetch_schema_from_transport=False, ) query = gql(chat_subscription_query) for index, result in enumerate(client.subscribe(query)): if index == 0 or 1: print(result) # because we subscribed to 2 rooms we received two welcome messages. elif index == 2: print(result) assert "hello from web browser" in result["joinChatRooms"]["message"] break try: self._enter_chat_room("room1") self._post_message("hello from web browser") finally: self._close_all_new_windows() ``` --- The HTTP and WebSockets protocol are handled by different base classes. HTTP uses `GraphQLHTTPConsumer` and WebSockets uses `GraphQLWSConsumer`. Both of them can be extended: ### Passing connection params Connection parameters can be passed using the `connection_params` parameter of the `GraphQLWebsocketCommunicator` class. This is particularily useful to test websocket authentication. ```python GraphQLWebsocketCommunicator( application=application, path="/graphql", connection_params={"token": "strawberry"}, ) ``` ## GraphQLHTTPConsumer (HTTP) ### Options `GraphQLHTTPConsumer` supports the same options as all other integrations: - `schema`: mandatory, the schema created by `strawberry.Schema`. - `graphql_ide`: optional, defaults to `"graphiql"`, allows to choose the GraphQL IDE interface (one of `graphiql`, `apollo-sandbox` or `pathfinder`) or to disable it by passing `None`. - `allow_queries_via_get`: optional, defaults to `True`, whether to enable queries via `GET` requests - `multipart_uploads_enabled`: optional, defaults to `False`, controls whether to enable multipart uploads. Please make sure to consider the [security implications mentioned in the GraphQL Multipart Request Specification](https://github.com/jaydenseric/graphql-multipart-request-spec/blob/master/readme.md#security) when enabling this feature. ### Extending the consumer The base `GraphQLHTTPConsumer` class can be extended by overriding any of the following methods: - `async def get_context(self, request: ChannelsRequest, response: TemporalResponse) -> Context` - `async def get_root_value(self, request: ChannelsRequest) -> Optional[RootValue]` - `async def process_result(self, request: Request, result: ExecutionResult) -> GraphQLHTTPResponse` - `def decode_json(self, data: Union[str, bytes]) -> object` - `def encode_json(self, data: object) -> str | bytes` - `async def render_graphql_ide(self, request: ChannelsRequest) -> ChannelsResponse` #### Context The default context returned by `get_context()` is a `dict` that includes the following keys by default: - `request`: A `ChannelsRequest` object with the following fields and methods: - `consumer`: The `GraphQLHTTPConsumer` instance for this connection - `body`: The request body - `headers`: A dict containing the headers of the request - `method`: The HTTP method of the request - `content_type`: The content type of the request - `response` A `TemporalResponse` object, that can be used to influence the HTTP response: - `status_code`: The status code of the response, if there are no execution errors (defaults to `200`) - `headers`: Any additional headers that should be send with the response #### render_graphql_ide In case you need more control over the rendering of the GraphQL IDE than the `graphql_ide` option provides, you can override the `render_graphql_ide` method. ```python from strawberry.channels import GraphQLHTTPConsumer, ChannelsRequest, ChannelsResponse class MyGraphQLHTTPConsumer(GraphQLHTTPConsumer): async def render_graphql_ide(self, request: ChannelsRequest) -> ChannelsResponse: custom_html = """

Custom GraphQL IDE

""" return ChannelsResponse(content=custom_html, content_type="text/html") ``` ## GraphQLWSConsumer (WebSockets / Subscriptions) ### Options - `schema`: mandatory, the schema created by `strawberry.Schema`. - `debug`: optional, defaults to `False`, whether to enable debug mode. - `keep_alive`: optional, defaults to `False`, whether to enable keep alive mode for websockets. - `keep_alive_interval`: optional, defaults to `1`, the interval in seconds for keep alive messages. ### Extending the consumer The base `GraphQLWSConsumer` class can be extended by overriding any of the following methods: - `async def get_context(self, request: GraphQLWSConsumer, response: GraphQLWSConsumer) -> Context` - `async def get_root_value(self, request: GraphQLWSConsumer) -> Optional[RootValue]` - `def decode_json(self, data: Union[str, bytes]) -> object` - `def encode_json(self, data: object) -> str | bytes` - `async def on_ws_connect(self, context: Context) -> Union[UnsetType, None, Dict[str, object]]` ### on_ws_connect By overriding `on_ws_connect` you can customize the behavior when a `graphql-ws` or `graphql-transport-ws` connection is established. This is particularly useful for authentication and authorization. By default, all connections are accepted. To manually accept a connection, return `strawberry.UNSET` or a connection acknowledgment payload. The acknowledgment payload will be sent to the client. Note that the legacy protocol does not support `None`/`null` acknowledgment payloads, while the new protocol does. Our implementation will treat `None`/`null` payloads the same as `strawberry.UNSET` in the context of the legacy protocol. To reject a connection, raise a `ConnectionRejectionError`. You can optionally provide a custom error payload that will be sent to the client when the legacy GraphQL over WebSocket protocol is used. ```python from typing import Dict from strawberry.exceptions import ConnectionRejectionError from strawberry.channels import GraphQLWSConsumer class MyGraphQLWSConsumer(GraphQLWSConsumer): async def on_ws_connect(self, context: Dict[str, object]): connection_params = context["connection_params"] if not isinstance(connection_params, dict): # Reject without a custom graphql-ws error payload raise ConnectionRejectionError() if connection_params.get("password") != "secret": # Reject with a custom graphql-ws error payload raise ConnectionRejectionError({"reason": "Invalid password"}) if username := connection_params.get("username"): # Accept with a custom acknowledgment payload return {"message": f"Hello, {username}!"} # Accept without a acknowledgment payload return await super().on_ws_connect(context) ``` ### Context The default context returned by `get_context()` is a `dict` and it includes the following keys by default: - `request`: The `GraphQLWSConsumer` instance of the current connection. It can be used to access the connection scope, e.g. `info.context["ws"].headers` allows access to any headers. - `ws`: The same as `request` - `connection_params`: Any `connection_params`, see [Authenticating Subscriptions](/docs/general/subscriptions#authenticating-subscriptions) ## Example for defining a custom context Here is an example for extending the base classes to offer a different context object in your resolvers. For the HTTP integration, you can also have properties to access the current user and the session. Both properties depend on the `AuthMiddlewareStack` wrapper. ```python from django.contrib.auth.models import AnonymousUser from strawberry.channels import ChannelsConsumer, ChannelsRequest from strawberry.channels import GraphQLHTTPConsumer as BaseGraphQLHTTPConsumer from strawberry.channels import GraphQLWSConsumer as BaseGraphQLWSConsumer from strawberry.http.temporal_response import TemporalResponse @dataclass class ChannelsContext: request: ChannelsRequest response: TemporalResponse @property def user(self): # Depends on Channels' AuthMiddlewareStack if "user" in self.request.consumer.scope: return self.request.consumer.scope["user"] return AnonymousUser() @property def session(self): # Depends on Channels' SessionMiddleware / AuthMiddlewareStack if "session" in self.request.consumer.scope: return self.request.consumer.scope["session"] return None @dataclass class ChannelsWSContext: request: ChannelsConsumer connection_params: Optional[Dict[str, Any]] = None @property def ws(self) -> ChannelsConsumer: return self.request class GraphQLHTTPConsumer(BaseGraphQLHTTPConsumer): @override async def get_context( self, request: ChannelsRequest, response: TemporalResponse ) -> ChannelsContext: return ChannelsContext( request=request, response=response, ) class GraphQLWSConsumer(BaseGraphQLWSConsumer): @override async def get_context( self, request: ChannelsConsumer, connection_params: Any ) -> ChannelsWSContext: return ChannelsWSContext( request=request, connection_params=connection_params, ) ``` You can import and use the extended `GraphQLHTTPConsumer` and `GraphQLWSConsumer` classes in your `myproject.asgi.py` file as shown before. --- ## API ### GraphQLProtocolTypeRouter A helper for creating a common strawberry-django [`ProtocolTypeRouter`](https://channels.readthedocs.io/en/stable/topics/routing.html#protocoltyperouter) Implementation. Example usage: ```python from strawberry.channels import GraphQLProtocolTypeRouter from django.core.asgi import get_asgi_application django_asgi = get_asgi_application() from myapi import schema application = GraphQLProtocolTypeRouter( schema, django_application=django_asgi, ) ``` This will route all requests to /graphql on either HTTP or websockets to us, and everything else to the Django application. ### ChannelsConsumer Strawberries extended [`AsyncConsumer`](https://channels.readthedocs.io/en/stable/topics/consumers.html#consumers). Every graphql session will have an instance of this class inside `info.context["ws"]` (WebSockets) or `info.context["request"].consumer` (HTTP). #### properties ```python @contextlib.asynccontextmanager async def listen_to_channel( self, type: str, *, timeout: float | None = None, groups: Sequence[str] | None = None ) -> AsyncGenerator[Any, None]: ... ``` - `type` - The type of the message to wait for, equivalent to `scope['type']` - `timeout` - An optional timeout to wait for each subsequent message. - `groups` - list of groups to yield messages from threw channel layer. strawberry-graphql-0.287.0/docs/integrations/creating-an-integration.md000066400000000000000000000070451511033167500262310ustar00rootroot00000000000000--- title: Adding an integration --- # Adding an integration Strawberry provides a set of integrations with other libraries, such as Django, FastAPI, Flask, and more, but you can also add your own integration. This guide will show you how to do that. ## Base views Strawberry includes two base views that you can use to create your own integrations: - `SyncBaseHTTPView`: a base view for synchronous integrations - `AsyncBaseHTTPView`: a base view for asynchronous integrations Both views are provides the same API, with the main difference being that the `AsyncBaseHTTPView`'s methods are async. ## Creating a view To create a view, you need to create a class that inherits from either `SyncBaseHTTPView` or `AsyncBaseHTTPView` and implement the `get_root_value` method. Here is an example of a view that inherits from `AsyncBaseHTTPView`: ```python from strawberry.http.async_base_view import AsyncBaseHTTPView from strawberry.http.temporal_response import TemporalResponse from strawberry.http.typevars import Context, RootValue class MyView( AsyncBaseHTTPView[ MyRequest, MyResponse, TemporalResponse, Context, RootValue, ] ): @property def allow_queries_via_get(self) -> bool: # this will usually be a setting on the view return True async def get_sub_response(self, request: MyRequest) -> TemporalResponse: return TemporalResponse(status_code=200) async def get_context(self, request: Request, response: SubResponse) -> Context: return {"request": request, "response": response} async def get_root_value(self, request: Request) -> Optional[RootValue]: return None def render_graphql_ide(self, request: Request) -> Response: ... def create_response( self, response_data: GraphQLHTTPResponse, sub_response: SubResponse ) -> Response: ... ``` The methods above are the bare minimum that you need to implement to create a view. They are all required, but you can also override other methods to change the behaviour of the view. On top of that we also need a request adapter, here's the base class for the async version: ```python from strawberry.http.types import HTTPMethod, QueryParams, FormData class AsyncHTTPRequestAdapter: @property def query_params(self) -> Mapping[str, Optional[Union[str, List[str]]]]: ... @property def method(self) -> HTTPMethod: ... @property def headers(self) -> Mapping[str, str]: ... @property def content_type(self) -> Optional[str]: ... async def get_body(self) -> Union[bytes, str]: ... async def get_form_data(self) -> FormData: ... ``` This request adapter will be used to get the request data from the request object. You can specify the request adapter to use by setting the `request_adapter_class` attribute on the view. ```python class MyView( AsyncBaseHTTPView[ MyRequest, MyResponse, TemporalResponse, Context, RootValue, ] ): request_adapter_class = MyRequestAdapter ``` Finally you need to execute the operation, the base view provides a `run` method that you can use to do that: ```python from strawberry.http.exceptions import HTTPException class MyView( AsyncBaseHTTPView[ MyRequest, MyResponse, TemporalResponse, Context, RootValue, ] ): ... async def get(self, request: MyRequest) -> MyResponse: try: return await self.run(request) except HTTPException as e: response = Response(e.reason, status_code=e.status_code) ``` strawberry-graphql-0.287.0/docs/integrations/django.md000066400000000000000000000364741511033167500227720ustar00rootroot00000000000000--- title: Django --- # Django Strawberry comes with a basic [Django integration](https://github.com/strawberry-graphql/strawberry-graphql-django). It provides a view that you can use to serve your GraphQL schema: ```python from django.urls import path from django.views.decorators.csrf import csrf_exempt from strawberry.django.views import GraphQLView from api.schema import schema urlpatterns = [ path("graphql/", csrf_exempt(GraphQLView.as_view(schema=schema))), ] ``` Strawberry only provides a GraphQL view for Django, [Strawberry GraphQL Django](https://github.com/strawberry-graphql/strawberry-graphql-django) provides integration with the models. `import strawberry_django` should do the same as `import strawberry.django` if both libraries are installed. You'd also need to add `strawberry_django` to the `INSTALLED_APPS` of your project, this is needed to provide the template for the GraphiQL interface. ## Options The `GraphQLView` accepts the following arguments: - `schema`: mandatory, the schema created by `strawberry.Schema`. - `graphql_ide`: optional, defaults to `"graphiql"`, allows to choose the GraphQL IDE interface (one of `graphiql`, `apollo-sandbox` or `pathfinder`) or to disable it by passing `None`. - `allow_queries_via_get`: optional, defaults to `True`, whether to enable queries via `GET` requests - `multipart_uploads_enabled`: optional, defaults to `False`, controls whether to enable multipart uploads. Please make sure to consider the [security implications mentioned in the GraphQL Multipart Request Specification](https://github.com/jaydenseric/graphql-multipart-request-spec/blob/master/readme.md#security) when enabling this feature. ## Deprecated options The following options are deprecated and will be removed in a future release: - `json_encoder`: optional JSON encoder, defaults to `DjangoJSONEncoder`, will be used to serialize the data. - `json_dumps_params`: optional dictionary of keyword arguments to pass to the `json.dumps` call used to generate the response. To get the most compact JSON representation, you should specify `{"separators": (",", ":")}`, defaults to `None`. You can extend the view and override `encode_json` to customize the JSON encoding process. ## Extending the view We allow to extend the base `GraphQLView`, by overriding the following methods: - `def get_context(self, request: HttpRequest, response: HttpResponse) -> Context` - `def get_root_value(self, request: HttpRequest) -> Optional[RootValue]` - `def process_result(self, request: Request, result: ExecutionResult) -> GraphQLHTTPResponse` - `def decode_json(self, data: Union[str, bytes]) -> object` - `def encode_json(self, data: object) -> str | bytes` - `def render_graphql_ide(self, request: HttpRequest) -> HttpResponse` ### get_context `get_context` allows to provide a custom context object that can be used in your resolver. You can return anything here, by default we return a `StrawberryDjangoContext` object. ```python import strawberry @strawberry.type class Query: @strawberry.field def user(self, info: strawberry.Info) -> str: return str(info.context.request.user) ``` or in case of a custom context: ```python import strawberry from strawberry.django.views import GraphQLView from django.http import HttpRequest, HttpResponse class MyGraphQLView(GraphQLView): def get_context(self, request: HttpRequest, response: HttpResponse): return {"example": 1} @strawberry.type class Query: @strawberry.field def example(self, info: strawberry.Info) -> str: return str(info.context["example"]) ``` Here we are returning a custom context dictionary that contains only one item called "example". Then we use the context in a resolver, the resolver will return "1" in this case. ### get_root_value `get_root_value` allows to provide a custom root value for your schema, this is probably not used a lot but it might be useful in certain situations. Here's an example: ```python import strawberry from strawberry.django.views import GraphQLView from django.http import HttpRequest class MyGraphQLView(GraphQLView): def get_root_value(self, request: HttpRequest): return Query(name="Patrick") @strawberry.type class Query: name: str ``` Here we are returning a Query where the name is "Patrick", so we when requesting the field name we'll return "Patrick" in this case. ### process_result `process_result` allows to customize and/or process results before they are sent to the clients. This can be useful logging errors or hiding them (for example to hide internal exceptions). It needs to return an object of `GraphQLHTTPResponse` and accepts the request and the execution results. ```python from strawberry.http import GraphQLHTTPResponse from strawberry.types import ExecutionResult from strawberry.django.views import GraphQLView from django.http import HttpRequest class MyGraphQLView(GraphQLView): def process_result( self, request: HttpRequest, result: ExecutionResult ) -> GraphQLHTTPResponse: data: GraphQLHTTPResponse = {"data": result.data} if result.errors: data["errors"] = [err.formatted for err in result.errors] return data ``` In this case we are doing the default processing of the result, but it can be tweaked based on your needs. ### decode_json `decode_json` allows to customize the decoding of HTTP and WebSocket JSON requests. By default we use `json.loads` but you can override this method to use a different decoder. ```python from strawberry.django.views import GraphQLView from typing import Union import orjson class MyGraphQLView(GraphQLView): def decode_json(self, data: Union[str, bytes]) -> object: return orjson.loads(data) ``` Make sure your code raises `json.JSONDecodeError` or a subclass of it if the JSON cannot be decoded. The library shown in the example above, `orjson`, does this by default. ### encode_json `encode_json` allows to customize the encoding of HTTP and WebSocket JSON responses. By default we use `json.dumps` but you can override this method to use a different encoder. ```python import json from strawberry.django.views import GraphQLView class MyGraphQLView(GraphQLView): def encode_json(self, data: object) -> str | bytes: return json.dumps(data, indent=2) ``` ### render_graphql_ide In case you need more control over the rendering of the GraphQL IDE than the `graphql_ide` option provides, you can provide the `graphql/graphiql.html` template, which will be used instead of the configured IDE. Alternatively, you can override the `render_graphql_ide` method: ```python from strawberry.django.views import GraphQLView from django.http import HttpResponse, HttpRequest from django.template.loader import render_to_string class MyGraphQLView(GraphQLView): def render_graphql_ide(self, request: HttpRequest) -> HttpResponse: content = render_to_string("myapp/my_graphql_ide_template.html") return HttpResponse(content) ``` # Async Django Strawberry also provides an async view that you can use with Django 3.1+ ```python from django.urls import path from strawberry.django.views import AsyncGraphQLView from api.schema import schema urlpatterns = [ path("graphql/", AsyncGraphQLView.as_view(schema=schema)), ] ``` You'd also need to add `strawberry_django` to the `INSTALLED_APPS` of your project, this is needed to provide the template for the GraphiQL interface. ## Important Note: Django ORM and Async Context When using `AsyncGraphQLView`, you may encounter a `SynchronousOnlyOperation` error if your resolvers access Django's ORM directly: ```text django.core.exceptions.SynchronousOnlyOperation: You cannot call this from an async context - use a thread or sync_to_async. ``` This occurs because Django's ORM is synchronous by default and cannot be called directly from async contexts like the `AsyncGraphQLView`. Here are two solutions: ### Solution 1: Use the async version of the ORM methods Instead of using the standard version of the ORM methods, you can usually use an async version, for example, in addition to `get` Django also provides `aget` than can be used in an async context: ```python import strawberry from django.contrib.auth.models import User @strawberry.type class Query: @strawberry.field @staticmethod async def user_name(id: strawberry.ID) -> str: # Note: this is a simple example, you'd normally just return # a full user instead of making a resolver to get a user's name # by id. This is just to explain the async issues with Django :) user = await User.objects.aget(id) # This would cause SynchronousOnlyOperation error: # user = User.objects.get(id) return user.name ``` You can find all the supported methods in the [Asynchronous support guide](https://docs.djangoproject.com/en/5.2/topics/async/) on Django's website. ### Solution 2: Use `sync_to_async` While some ORM methods have an async equivalent, not all of them do, in that case you can wrap your ORM operations with Django's `sync_to_async`: ```python import strawberry from django.contrib.auth.models import User from asgiref.sync import sync_to_async @strawberry.type class Query: @strawberry.field async def users(self) -> list[str]: # This would cause SynchronousOnlyOperation error: # return [user.username for user in User.objects.all()] # Correct way using sync_to_async: users = await sync_to_async(list)(User.objects.all()) return [user.username for user in users] ``` ### Solution 3: Use `strawberry_django` (Recommended) The `strawberry_django` package automatically handles async/sync compatibility. Use `strawberry_django.field` instead of `strawberry.field`: ```python import strawberry_django from django.contrib.auth.models import User @strawberry_django.type(User) class UserType: username: str email: str @strawberry.type class Query: # This automatically works with both sync and async views users: list[UserType] = strawberry_django.field() ``` We recommend using the `strawberry_django` package for Django ORM integration as it provides automatic async/sync compatibility and additional Django-specific features. ## Options The `AsyncGraphQLView` accepts the following arguments: - `schema`: mandatory, the schema created by `strawberry.Schema`. - `graphql_ide`: optional, defaults to `"graphiql"`, allows to choose the GraphQL IDE interface (one of `graphiql`, `apollo-sandbox` or `pathfinder`) or to disable it by passing `None`. - `allow_queries_via_get`: optional, defaults to `True`, whether to enable queries via `GET` requests ## Extending the view The base `AsyncGraphQLView` class can be extended by overriding any of the following methods: - `async def get_context(self, request: HttpRequest, response: HttpResponse) -> Context` - `async def get_root_value(self, request: HttpRequest) -> Optional[RootValue]` - `async def process_result(self, request: Request, result: ExecutionResult) -> GraphQLHTTPResponse` - `def decode_json(self, data: Union[str, bytes]) -> object` - `def encode_json(self, data: object) -> str | bytes` - `async def render_graphql_ide(self, request: HttpRequest) -> HttpResponse` ### get_context `get_context` allows to provide a custom context object that can be used in your resolver. You can return anything here, by default we return a dictionary with the request. ```python import strawberry from strawberry.django.views import AsyncGraphQLView from django.http import HttpRequest, HttpResponse class MyGraphQLView(AsyncGraphQLView): async def get_context(self, request: HttpRequest, response: HttpResponse): return {"example": 1} @strawberry.type class Query: @strawberry.field def example(self, info: strawberry.Info) -> str: return str(info.context["example"]) ``` Here we are returning a custom context dictionary that contains only one item called "example". Then we use the context in a resolver, the resolver will return "1" in this case. ### get_root_value `get_root_value` allows to provide a custom root value for your schema, this is probably not used a lot but it might be useful in certain situations. Here's an example: ```python import strawberry from strawberry.django.views import AsyncGraphQLView from django.http import HttpRequest class MyGraphQLView(AsyncGraphQLView): async def get_root_value(self, request: HttpRequest): return Query(name="Patrick") @strawberry.type class Query: name: str ``` Here we are returning a Query where the name is "Patrick", so we when requesting the field name we'll return "Patrick" in this case. ### process_result `process_result` allows to customize and/or process results before they are sent to the clients. This can be useful logging errors or hiding them (for example to hide internal exceptions). It needs to return an object of `GraphQLHTTPResponse` and accepts the request and the execution results. ```python from strawberry.http import GraphQLHTTPResponse from strawberry.types import ExecutionResult from strawberry.django.views import AsyncGraphQLView from django.http import HttpRequest class MyGraphQLView(AsyncGraphQLView): async def process_result( self, request: HttpRequest, result: ExecutionResult ) -> GraphQLHTTPResponse: data: GraphQLHTTPResponse = {"data": result.data} if result.errors: data["errors"] = [err.formatted for err in result.errors] return data ``` In this case we are doing the default processing of the result, but it can be tweaked based on your needs. ### decode_json `decode_json` allows to customize the decoding of HTTP and WebSocket JSON requests. By default we use `json.loads` but you can override this method to use a different decoder. ```python from strawberry.django.views import AsyncGraphQLView from typing import Union import orjson class MyGraphQLView(AsyncGraphQLView): def decode_json(self, data: Union[str, bytes]) -> object: return orjson.loads(data) ``` Make sure your code raises `json.JSONDecodeError` or a subclass of it if the JSON cannot be decoded. The library shown in the example above, `orjson`, does this by default. ### encode_json `encode_json` allows to customize the encoding of HTTP and WebSocket JSON responses. By default we use `json.dumps` but you can override this method to use a different encoder. ```python import json from strawberry.django.views import AsyncGraphQLView class MyGraphQLView(AsyncGraphQLView): def encode_json(self, data: object) -> str | bytes: return json.dumps(data, indent=2) ``` ### render_graphql_ide In case you need more control over the rendering of the GraphQL IDE than the `graphql_ide` option provides, you can provide the `graphql/graphiql.html` template, which will be used instead of the configured IDE. Alternatively, you can override the `render_graphql_ide` method: ```python from strawberry.django.views import AsyncGraphQLView from django.http import HttpResponse from django.template.loader import render_to_string class MyGraphQLView(AsyncGraphQLView): async def render_graphql_ide(self, request: HttpRequest) -> HttpResponse: content = render_to_string("myapp/my_graphql_ide_template.html") return HttpResponse(content) ``` ## Subscriptions Subscriptions run over websockets and thus depend on [channels](https://channels.readthedocs.io/). Take a look at our [channels integration](./channels.md) page for more information regarding it. strawberry-graphql-0.287.0/docs/integrations/fastapi.md000066400000000000000000000300701511033167500231410ustar00rootroot00000000000000--- title: FastAPI --- # FastAPI Strawberry provides support for [FastAPI](https://fastapi.tiangolo.com/) with a custom [APIRouter](https://fastapi.tiangolo.com/tutorial/bigger-applications/#apirouter) called `GraphQLRouter`. Before using Strawberry's FastAPI support make sure you install all the required dependencies by running: ```shell pip install 'strawberry-graphql[fastapi]' ``` See the example below for integrating FastAPI with Strawberry: ```python import strawberry from fastapi import FastAPI from strawberry.fastapi import GraphQLRouter @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "Hello World" schema = strawberry.Schema(Query) graphql_app = GraphQLRouter(schema) app = FastAPI() app.include_router(graphql_app, prefix="/graphql") ``` Both FastAPI and Strawberry support sync and async functions, but their behavior is different. FastAPI processes sync endpoints in a threadpool and async endpoints using the event loop. However, Strawberry processes sync and async fields using the event loop, which means that using a sync `def` will block the entire worker. It is recommended to use `async def` for all of your fields if you want to be able to handle concurrent request on a single worker. If you can't use async, make sure you wrap blocking code in a suspending thread, for example using `starlette.concurrency.run_in_threadpool`. ## Options The `GraphQLRouter` accepts the following options: - `schema`: mandatory, the schema created by `strawberry.Schema`. - `graphql_ide`: optional, defaults to `"graphiql"`, allows to choose the GraphQL IDE interface (one of `graphiql`, `apollo-sandbox` or `pathfinder`) or to disable it by passing `None`. - `allow_queries_via_get`: optional, defaults to `True`, whether to enable queries via `GET` requests - `context_getter`: optional FastAPI dependency for providing custom context value. - `root_value_getter`: optional FastAPI dependency for providing custom root value. - `multipart_uploads_enabled`: optional, defaults to `False`, controls whether to enable multipart uploads. Please make sure to consider the [security implications mentioned in the GraphQL Multipart Request Specification](https://github.com/jaydenseric/graphql-multipart-request-spec/blob/master/readme.md#security) when enabling this feature. ### context_getter The `context_getter` option allows you to provide a custom context object that can be used in your resolver. `context_getter` is a [FastAPI dependency](https://fastapi.tiangolo.com/tutorial/dependencies/) and can inject other dependencies if you so wish. There are two options at your disposal here: 1. Define your custom context as a dictionary, 2. Define your custom context as a class. If no context is supplied, then the default context returned is a dictionary containing the request, the response, and any background tasks. However, you can define a class-based custom context inline with [FastAPI practice](https://fastapi.tiangolo.com/tutorial/dependencies/classes-as-dependencies/). If you choose to do this, you must ensure that your custom context class inherits from `BaseContext` or an `InvalidCustomContext` exception is raised. For dictionary-based custom contexts, an example might look like the following. ```python import strawberry from fastapi import FastAPI, Depends, Request, WebSocket, BackgroundTasks from strawberry.fastapi import GraphQLRouter def custom_context_dependency() -> str: return "John" async def get_context( custom_value=Depends(custom_context_dependency), ): return { "custom_value": custom_value, } @strawberry.type class Query: @strawberry.field def example(self, info: strawberry.Info) -> str: return f"Hello {info.context['custom_value']}" schema = strawberry.Schema(Query) graphql_app = GraphQLRouter( schema, context_getter=get_context, ) app = FastAPI() app.include_router(graphql_app, prefix="/graphql") ``` Here we are returning a custom context dictionary that contains one extra item called "custom*value", which is injected from `custom_context_dependency`. This value exists alongside `request`, `response`, and `background_tasks` in the `info.context` \_dictionary* and so it requires `['request']` indexing. Then we use the context in a resolver. The resolver will return "Hello John" in this case. For class-based custom contexts, an example might look like the following. ```python import strawberry from fastapi import FastAPI, Depends, Request, WebSocket, BackgroundTasks from strawberry.fastapi import BaseContext, GraphQLRouter class CustomContext(BaseContext): def __init__(self, greeting: str, name: str): self.greeting = greeting self.name = name def custom_context_dependency() -> CustomContext: return CustomContext(greeting="you rock!", name="John") async def get_context( custom_context=Depends(custom_context_dependency), ): return custom_context @strawberry.type class Query: @strawberry.field def example(self, info: strawberry.Info) -> str: return f"Hello {info.context.name}, {info.context.greeting}" schema = strawberry.Schema(Query) graphql_app = GraphQLRouter( schema, context_getter=get_context, ) app = FastAPI() app.include_router(graphql_app, prefix="/graphql") ``` In this case, we are returning a custom context class that inherits from BaseContext with fields `name` and `greeting`, which is also injected by `custom_context_dependency`. These custom values exist alongside `request`, `response`, and `background_tasks` in the `info.context` _class_ and so it requires `.request` indexing. Then we use the context in a resolver. The resolver will return “Hello John, you rock!” in this case. #### Setting background tasks Similarly, [background tasks](https://fastapi.tiangolo.com/tutorial/background-tasks/?h=background) can be added via the context: ```python import strawberry from fastapi import FastAPI, BackgroundTasks from strawberry.fastapi import GraphQLRouter async def notify_new_flavour(name: str): print(name) @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "Hello World" @strawberry.type class Mutation: @strawberry.mutation def create_flavour(self, name: str, info: strawberry.Info) -> bool: info.context["background_tasks"].add_task(notify_new_flavour, name) return True schema = strawberry.Schema(Query, Mutation) graphql_app = GraphQLRouter(schema) app = FastAPI() app.include_router(graphql_app, prefix="/graphql") ``` If using a custom context class, then background tasks should be stored within the class object as `.background_tasks`. ### root_value_getter The `root_value_getter` option allows you to provide a custom root value for your schema. This is most likely a rare usecase but might be useful in certain situations. Here's an example: ```python import strawberry from fastapi import FastAPI from strawberry.fastapi import GraphQLRouter async def get_root_value(): return Query(name="Patrick") @strawberry.type class Query: name: str schema = strawberry.Schema(Query) graphql_app = GraphQLRouter( schema, root_value_getter=get_root_value, ) app = FastAPI() app.include_router(graphql_app, prefix="/graphql") ``` Here we are returning a Query where the name is "Patrick", so when we request the field name we'll return "Patrick". ## Extending the router The base `GraphQLRouter` class can be extended by overriding any of the following methods: - `async def process_result(self, request: Request, result: ExecutionResult) -> GraphQLHTTPResponse` - `def decode_json(self, data: Union[str, bytes]) -> object` - `def encode_json(self, data: object) -> str | bytes` - `async def render_graphql_ide(self, request: Request) -> HTMLResponse` - `async def on_ws_connect(self, context: Context) -> Union[UnsetType, None, Dict[str, object]]` ### process_result The `process_result` option allows you to customize and/or process results before they are sent to the clients. This can be useful for logging errors or hiding them (for example to hide internal exceptions). It needs to return a `GraphQLHTTPResponse` object and accepts the request and execution results. ```python from starlette.requests import Request from strawberry.fastapi import GraphQLRouter from strawberry.http import GraphQLHTTPResponse from strawberry.types import ExecutionResult class MyGraphQLRouter(GraphQLRouter): async def process_result( self, request: Request, result: ExecutionResult ) -> GraphQLHTTPResponse: data: GraphQLHTTPResponse = {"data": result.data} if result.errors: data["errors"] = [err.formatted for err in result.errors] return data ``` In this case we are doing the default processing of the result, but it can be tweaked based on your needs. ### decode_json `decode_json` allows to customize the decoding of HTTP and WebSocket JSON requests. By default we use `json.loads` but you can override this method to use a different decoder. ```python from strawberry.fastapi import GraphQLRouter from typing import Union import orjson class MyGraphQLRouter(GraphQLRouter): def decode_json(self, data: Union[str, bytes]) -> object: return orjson.loads(data) ``` Make sure your code raises `json.JSONDecodeError` or a subclass of it if the JSON cannot be decoded. The library shown in the example above, `orjson`, does this by default. ### encode_json `encode_json` allows to customize the encoding of HTTP and WebSocket JSON responses. By default we use `json.dumps` but you can override this method to use a different encoder. ```python from strawberry.fastapi import GraphQLRouter import json class MyGraphQLRouter(GraphQLRouter): def encode_json(self, data: object) -> str | bytes: return json.dumps(data, indent=2) ``` ### render_graphql_ide In case you need more control over the rendering of the GraphQL IDE than the `graphql_ide` option provides, you can override the `render_graphql_ide` method. ```python from strawberry.fastapi import GraphQLRouter from starlette.responses import HTMLResponse, Response from starlette.requests import Request class MyGraphQLRouter(GraphQLRouter): async def render_graphql_ide(self, request: Request) -> HTMLResponse: custom_html = """

Custom GraphQL IDE

""" return HTMLResponse(custom_html) ``` ### on_ws_connect By overriding `on_ws_connect` you can customize the behavior when a `graphql-ws` or `graphql-transport-ws` connection is established. This is particularly useful for authentication and authorization. By default, all connections are accepted. To manually accept a connection, return `strawberry.UNSET` or a connection acknowledgment payload. The acknowledgment payload will be sent to the client. Note that the legacy protocol does not support `None`/`null` acknowledgment payloads, while the new protocol does. Our implementation will treat `None`/`null` payloads the same as `strawberry.UNSET` in the context of the legacy protocol. To reject a connection, raise a `ConnectionRejectionError`. You can optionally provide a custom error payload that will be sent to the client when the legacy GraphQL over WebSocket protocol is used. ```python from typing import Dict from strawberry.exceptions import ConnectionRejectionError from strawberry.fastapi import GraphQLRouter class MyGraphQLRouter(GraphQLRouter): async def on_ws_connect(self, context: Dict[str, object]): connection_params = context["connection_params"] if not isinstance(connection_params, dict): # Reject without a custom graphql-ws error payload raise ConnectionRejectionError() if connection_params.get("password") != "secret": # Reject with a custom graphql-ws error payload raise ConnectionRejectionError({"reason": "Invalid password"}) if username := connection_params.get("username"): # Accept with a custom acknowledgment payload return {"message": f"Hello, {username}!"} # Accept without a acknowledgment payload return await super().on_ws_connect(context) ``` strawberry-graphql-0.287.0/docs/integrations/flask.md000066400000000000000000000136001511033167500226120ustar00rootroot00000000000000--- title: Flask --- # Flask Strawberry comes with a basic Flask integration. It provides a view that you can use to serve your GraphQL schema: ```python from flask import Flask from strawberry.flask.views import GraphQLView from api.schema import schema app = Flask(__name__) app.add_url_rule( "/graphql", view_func=GraphQLView.as_view("graphql_view", schema=schema), ) if __name__ == "__main__": app.run() ``` If you'd prefer to use an asynchronous view you can instead use the following import which has the same interface as `GraphQLView`. This is helpful if using a dataloader. ```python from strawberry.flask.views import AsyncGraphQLView ``` ## Options The `GraphQLView` accepts the following options at the moment: - `schema`: mandatory, the schema created by `strawberry.Schema`. - `graphql_ide`: optional, defaults to `"graphiql"`, allows to choose the GraphQL IDE interface (one of `graphiql`, `apollo-sandbox` or `pathfinder`) or to disable it by passing `None`. - `allow_queries_via_get`: optional, defaults to `True`, whether to enable queries via `GET` requests - `multipart_uploads_enabled`: optional, defaults to `False`, controls whether to enable multipart uploads. Please make sure to consider the [security implications mentioned in the GraphQL Multipart Request Specification](https://github.com/jaydenseric/graphql-multipart-request-spec/blob/master/readme.md#security) when enabling this feature. ## Extending the view The base `GraphQLView` class can be extended by overriding any of the following methods: - `def get_context(self, request: Request, response: Response) -> Context` - `def get_root_value(self, request: Request) -> Optional[RootValue]` - `def process_result(self, request: Request, result: ExecutionResult) -> GraphQLHTTPResponse` - `def decode_json(self, data: Union[str, bytes]) -> object` - `def encode_json(self, data: object) -> str | bytes` - `def render_graphql_ide(self, request: Request) -> Response` Note that the `AsyncGraphQLView` can also be extended in the same way, but the `get_context`, `get_root_value`, `process_result`, and `render_graphql_ide` methods are asynchronous. ### get_context `get_context` allows to provide a custom context object that can be used in your resolver. You can return anything here, by default we return a dictionary with the request. By default; the `Response` object from `flask` is injected via the parameters. ```python import strawberry from strawberry.flask.views import GraphQLView from flask import Request, Response class MyGraphQLView(GraphQLView): def get_context(self, request: Request, response: Response): return {"example": 1} @strawberry.type class Query: @strawberry.field def example(self, info: strawberry.Info) -> str: return str(info.context["example"]) ``` Here we are returning a custom context dictionary that contains only one item called "example". Then we use the context in a resolver, the resolver will return "1" in this case. ### get_root_value `get_root_value` allows to provide a custom root value for your schema, this is probably not used a lot but it might be useful in certain situations. Here's an example: ```python import strawberry from strawberry.flask.views import GraphQLView from flask import Request class MyGraphQLView(GraphQLView): def get_root_value(self, request: Request): return Query(name="Patrick") @strawberry.type class Query: name: str ``` Here we are returning a Query where the name is "Patrick", so we when requesting the field name we'll return "Patrick" in this case. ### process_result `process_result` allows to customize and/or process results before they are sent to the clients. This can be useful logging errors or hiding them (for example to hide internal exceptions). It needs to return an object of `GraphQLHTTPResponse` and accepts the execution result. ```python from strawberry.flask.views import GraphQLView from strawberry.http import GraphQLHTTPResponse from strawberry.types import ExecutionResult class MyGraphQLView(GraphQLView): def process_result(self, result: ExecutionResult) -> GraphQLHTTPResponse: data: GraphQLHTTPResponse = {"data": result.data} if result.errors: data["errors"] = [err.formatted for err in result.errors] return data ``` In this case we are doing the default processing of the result, but it can be tweaked based on your needs. ### decode_json `decode_json` allows to customize the decoding of HTTP JSON requests. By default we use `json.loads` but you can override this method to use a different decoder. ```python from strawberry.flask.views import GraphQLView from typing import Union import orjson class MyGraphQLView(GraphQLView): def decode_json(self, data: Union[str, bytes]) -> object: return orjson.loads(data) ``` Make sure your code raises `json.JSONDecodeError` or a subclass of it if the JSON cannot be decoded. The library shown in the example above, `orjson`, does this by default. ### encode_json `encode_json` allows to customize the encoding of HTTP and WebSocket JSON responses. By default we use `json.dumps` but you can override this method to use a different encoder. ```python import json from strawberry.flask.views import GraphQLView class MyGraphQLView(GraphQLView): def encode_json(self, data: object) -> str | bytes: return json.dumps(data, indent=2) ``` ### render_graphql_ide In case you need more control over the rendering of the GraphQL IDE than the `graphql_ide` option provides, you can override the `render_graphql_ide` method. ```python from strawberry.flask.views import GraphQLView from flask import Request, Response class MyGraphQLView(GraphQLView): def render_graphql_ide(self, request: Request) -> Response: custom_html = """

Custom GraphQL IDE

""" return Response(custom_html, status=200, content_type="text/html") ``` strawberry-graphql-0.287.0/docs/integrations/index.md000066400000000000000000000036301511033167500226230ustar00rootroot00000000000000--- title: Integrations --- # Integration Strawberry can be used with a variety of web frameworks and libraries. Here is a list of the integrations along with the features they support. Note, this table is not up to date, and this page shouldn't be linked anywhere (yet). | name | Supports sync | Supports async | Supports subscriptions via websockets | Supports subscriptions via multipart HTTP | Supports file uploads | Supports batch queries | | --------------------------- | ------------- | -------------------- | ------------------------------------- | ----------------------------------------- | --------------------- | ---------------------- | | [django](./django.md) | ✅ | ✅ (with Async view) | ❌ (use Channels for websockets) | ✅ (From Django 4.2) | ✅ | ❌ | | [starlette](./starlette.md) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | [aiohttp](./aiohttp.md) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | [flask](./flask.md) | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ | | [channels](./channels.md) | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | | [fastapi](./fastapi.md) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | strawberry-graphql-0.287.0/docs/integrations/litestar.md000066400000000000000000000355401511033167500233500ustar00rootroot00000000000000--- title: Litestar --- # Litestar Strawberry comes with an integration for [Litestar](https://litestar.dev/) by providing a `make_graphql_controller` function that can be used to create a GraphQL controller. See the example below for integrating Litestar with Strawberry: ```python import strawberry from litestar import Litestar from strawberry.litestar import make_graphql_controller @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "Hello World" schema = strawberry.Schema(Query) GraphQLController = make_graphql_controller( schema, path="/graphql", ) app = Litestar(route_handlers=[GraphQLController]) ``` ## Options The `make_graphql_controller` function accepts the following options: - `schema`: mandatory, the schema created by `strawberry.Schema`. - `path`: optional, defaults to ``, the path where the GraphQL endpoint will be mounted. - `graphql_ide`: optional, defaults to `"graphiql"`, allows to choose the GraphQL IDE interface (one of `graphiql`, `apollo-sandbox` or `pathfinder`) or to disable it by passing `None`. - `allow_queries_via_get`: optional, defaults to `True`, whether to enable queries via `GET` requests - `context_getter`: optional Litestar dependency for providing custom context value. - `root_value_getter`: optional Litestar dependency for providing custom root value. - `debug`: optional, defaults to `False`, whether to enable debug mode. - `keep_alive`: optional, defaults to `False`, whether to enable keep alive mode for websockets. - `keep_alive_interval`: optional, defaults to `1`, the interval in seconds for keep alive messages. - `subscription_protocols` optional, defaults to `(GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL)`, the allowed subscription protocols - `connection_init_wait_timeout` optional, default to `timedelta(minutes=1)`, the maximum time to wait for the connection initialization message when using `graphql-transport-ws` [protocol](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md#connectioninit) - `multipart_uploads_enabled`: optional, defaults to `False`, controls whether to enable multipart uploads. Please make sure to consider the [security implications mentioned in the GraphQL Multipart Request Specification](https://github.com/jaydenseric/graphql-multipart-request-spec/blob/master/readme.md#security) when enabling this feature. ### context_getter The `context_getter` option allows you to provide a Litestar dependency that return a custom context object that can be used in your resolver. ```python import strawberry from litestar import Request, Litestar from strawberry.litestar import make_graphql_controller from strawberry.types.info import Info async def custom_context_getter(): return {"custom": "context"} @strawberry.type class Query: @strawberry.field def hello(self, info: strawberry.Info[dict, None]) -> str: return info.context["custom"] schema = strawberry.Schema(Query) GraphQLController = make_graphql_controller( schema, path="/graphql", context_getter=custom_context_getter, ) app = Litestar(route_handlers=[GraphQLController]) ``` The `context_getter` is a standard Litestar dependency and can receive any existing dependency: ```python import strawberry from litestar import Request, Litestar from strawberry.litestar import make_graphql_controller from strawberry.types.info import Info from sqlalchemy.ext.asyncio import AsyncSession from app.models import User from sqlalchemy import select async def custom_context_getter(request: Request, db_session: AsyncSession): return {"user": request.user, "session": db_session} @strawberry.type class Query: @strawberry.field async def hello(self, info: strawberry.Info[dict, None]) -> str: session: AsyncSession = info.context["session"] user: User = info.context["user"] query = select(User).where(User.id == user.id) user = (await session.execute((query))).scalar_one() return f"Hello {user.first_name}" schema = strawberry.Schema(Query) GraphQLController = make_graphql_controller( schema, path="/graphql", context_getter=custom_context_getter, ) app = Litestar(route_handlers=[GraphQLController]) ``` You can also use a class-based custom context. To do this, you must inherit from `BaseContext` [msgspec Struct](https://jcristharif.com/msgspec/structs.html) or an `InvalidCustomContext` exception will be raised. ```python import strawberry from litestar import Request, Litestar from strawberry.litestar import make_graphql_controller, BaseContext from strawberry.types.info import Info from sqlalchemy.ext.asyncio import AsyncSession from app.models import User from sqlalchemy import select class CustomContext(BaseContext): user: User session: AsyncSession async def custom_context_getter( request: Request, db_session: AsyncSession ) -> CustomContext: return CustomContext(user=request.user, session=db_session) @strawberry.type class Query: @strawberry.field async def hello(self, info: strawberry.Info[CustomContext, None]) -> str: session: AsyncSession = info.context.session user: User = info.context.user query = select(User).where(User.id == user.id) user = (await session.execute((query))).scalar_one() return f"Hello {user.first_name}" schema = strawberry.Schema(Query) GraphQLController = make_graphql_controller( schema, path="/graphql", context_getter=custom_context_getter, ) app = Litestar(route_handlers=[GraphQLController]) ``` #### Context typing In our previous example using class based context, the actual runtime context a `CustomContext` type. Because it inherits from `BaseContext`, the `request`, `socket` and `response` attributes are typed as optional. When inside a query/mutation resolver, `request` and `response` are always set and `socket` is only set in subscriptions. To distinguish theses cases typing wise, the integration provides two classes that will help you to enforce strong typing: ```python from strawberry.litestar import HTTPContextType, WebSocketContextType ``` These classes does not actually exists at runtime, they are intended to be used to define a custom `Info` type with proper context typing. Taking over our previous example with class based custom context, here it how we can define two `Info` types for both queries/mutations and subscriptions: ```python import strawberry from typing import Any from litestar import Request, Litestar from litestar.datastructures import State from strawberry.litestar import ( make_graphql_controller, BaseContext, HTTPContextType, WebSocketContextType, ) from strawberry.types.info import Info from sqlalchemy.ext.asyncio import AsyncSession from app.models import User from sqlalchemy import select class CustomContext(BaseContext, kw_only=True): user: User session: AsyncSession class CustomHTTPContextType(HTTPContextType, CustomContext): request: Request[User, Any, State] class CustomWSContextType(WebSocketContextType, CustomContext): socket: WebSocket[User, Token, State] async def custom_context_getter( request: Request, db_session: AsyncSession ) -> CustomContext: return CustomContext(user=request.user, session=db_session) @strawberry.type class Query: @strawberry.field async def hello(self, info: strawberry.Info[CustomHTTPContextType, None]) -> str: session: AsyncSession = info.context.session user: User = info.context.user query = select(User).where(User.id == user.id) user = (await session.execute((query))).scalar_one() return f"Hello {user.first_name}" @strawberry.type class Subscription: @strawberry.subscription async def count( self, info: strawberry.Info[CustomWSContextType, None], target: int = 100 ) -> AsyncGenerator[int, None]: import devtools devtools.debug(info.context) devtools.debug(info.context.socket) for i in range(target): yield i await asyncio.sleep(0.5) schema = strawberry.Schema(Query, subscription=Subscription) GraphQLController = make_graphql_controller( schema, path="/graphql", context_getter=custom_context_getter, ) app = Litestar(route_handlers=[GraphQLController]) ``` ### root_value_getter The `root_value_getter` option allows you to provide a custom root value that can be used in your resolver ```python import strawberry from litestar import Request, Litestar from strawberry.litestar import make_graphql_controller @strawberry.type class Query: example: str = "Hello World" @strawberry.field def hello(self) -> str: return self.example def custom_get_root_value(): return Query() schema = strawberry.Schema(Query) GraphQLController = make_graphql_controller( schema, path="/graphql", root_value_getter=custom_get_root_value, ) app = Litestar(route_handlers=[GraphQLController]) ``` ## Extending the controller The base `GraphQLController` class returned by `make_graphql_controller` can be extended by overriding any of the following methods: - `async def process_result(self, request: Request, result: ExecutionResult) -> GraphQLHTTPResponse` - `def decode_json(self, data: Union[str, bytes]) -> object` - `def encode_json(self, data: object) -> str | bytes` - `async def render_graphql_ide(self, request: Request) -> Response` - `async def on_ws_connect(self, context: Context) -> Union[UnsetType, None, Dict[str, object]]` ### process_result The `process_result` option allows you to customize and/or process results before they are sent to the clients. This can be useful for logging errors or hiding them (for example to hide internal exceptions). It needs to return a `GraphQLHTTPResponse` object and accepts the request and execution results. ```python import strawberry from strawberry.http import GraphQLHTTPResponse from strawberry.types import ExecutionResult from strawberry.litestar import make_graphql_controller from litestar import Request @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "world" schema = strawberry.Schema(Query) GraphQLController = make_graphql_controller( schema, path="/graphql", ) class MyGraphQLController(GraphQLController): async def process_result( self, request: Request, result: ExecutionResult ) -> GraphQLHTTPResponse: data: GraphQLHTTPResponse = {"data": result.data} if result.errors: data["errors"] = [err.formatted for err in result.errors] return data ``` In this case we are doing the default processing of the result, but it can be tweaked based on your needs. ### decode_json `decode_json` allows to customize the decoding of HTTP and WebSocket JSON requests. By default we use `json.loads` but you can override this method to use a different decoder. ```python import strawberry import orjson from strawberry.litestar import make_graphql_controller from typing import Union @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "world" schema = strawberry.Schema(Query) GraphQLController = make_graphql_controller( schema, path="/graphql", ) class MyGraphQLController(GraphQLController): def decode_json(self, data: Union[str, bytes]) -> object: return orjson.loads(data) ``` Make sure your code raises `json.JSONDecodeError` or a subclass of it if the JSON cannot be decoded. The library shown in the example above, `orjson`, does this by default. ### encode_json `encode_json` allows to customize the encoding of HTTP and WebSocket JSON responses. By default we use `json.dumps` but you can override this method to use a different encoder. ```python import json import strawberry from strawberry.litestar import make_graphql_controller @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "world" schema = strawberry.Schema(Query) GraphQLController = make_graphql_controller( schema, path="/graphql", ) class MyGraphQLController(GraphQLController): def encode_json(self, data: object) -> bytes: return json.dumps(data, indent=2) ``` ### render_graphql_ide In case you need more control over the rendering of the GraphQL IDE than the `graphql_ide` option provides, you can override the `render_graphql_ide` method. ```python import strawberry from strawberry.litestar import make_graphql_controller from litestar import MediaType, Request, Response @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "world" schema = strawberry.Schema(Query) GraphQLController = make_graphql_controller( schema, path="/graphql", ) class MyGraphQLController(GraphQLController): async def render_graphql_ide(self, request: Request) -> Response: custom_html = """

Custom GraphQL IDE

""" return Response(custom_html, media_type=MediaType.HTML) ``` ### on_ws_connect By overriding `on_ws_connect` you can customize the behavior when a `graphql-ws` or `graphql-transport-ws` connection is established. This is particularly useful for authentication and authorization. By default, all connections are accepted. To manually accept a connection, return `strawberry.UNSET` or a connection acknowledgment payload. The acknowledgment payload will be sent to the client. Note that the legacy protocol does not support `None`/`null` acknowledgment payloads, while the new protocol does. Our implementation will treat `None`/`null` payloads the same as `strawberry.UNSET` in the context of the legacy protocol. To reject a connection, raise a `ConnectionRejectionError`. You can optionally provide a custom error payload that will be sent to the client when the legacy GraphQL over WebSocket protocol is used. ```python import strawberry from typing import Dict from strawberry.exceptions import ConnectionRejectionError from strawberry.litestar import make_graphql_controller @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "world" schema = strawberry.Schema(Query) GraphQLController = make_graphql_controller( schema, path="/graphql", ) class MyGraphQLController(GraphQLController): async def on_ws_connect(self, context: Dict[str, object]): connection_params = context["connection_params"] if not isinstance(connection_params, dict): # Reject without a custom graphql-ws error payload raise ConnectionRejectionError() if connection_params.get("password") != "secret": # Reject with a custom graphql-ws error payload raise ConnectionRejectionError({"reason": "Invalid password"}) if username := connection_params.get("username"): # Accept with a custom acknowledgment payload return {"message": f"Hello, {username}!"} # Accept without a acknowledgment payload return await super().on_ws_connect(context) ``` strawberry-graphql-0.287.0/docs/integrations/pydantic.md000066400000000000000000000304171511033167500233320ustar00rootroot00000000000000--- title: Pydantic support experimental: true --- # Pydantic support Strawberry comes with support for [Pydantic](https://pydantic-docs.helpmanual.io/). This allows for the creation of Strawberry types from pydantic models without having to write code twice. Here's a basic example of how this works, let's say we have a pydantic Model for a user, like this: ```python from datetime import datetime from typing import List, Optional from pydantic import BaseModel class User(BaseModel): id: int name: str signup_ts: Optional[datetime] = None friends: List[int] = [] ``` We can create a Strawberry type by using the `strawberry.experimental.pydantic.type` decorator: ```python import strawberry from .models import User @strawberry.experimental.pydantic.type(model=User) class UserType: id: strawberry.auto name: strawberry.auto friends: strawberry.auto ``` The `strawberry.experimental.pydantic.type` decorator accepts a Pydantic model and wraps a class that contains dataclass style fields with `strawberry.auto` as the type annotation. The fields marked with `strawberry.auto` will inherit their types from the Pydantic model. If you want to include all of the fields from your Pydantic model, you can instead pass `all_fields=True` to the decorator. -> **Note** Care should be taken to avoid accidentally exposing fields that -> weren't meant to be exposed on an API using this feature. ```python import strawberry from .models import User @strawberry.experimental.pydantic.type(model=User, all_fields=True) class UserType: pass ``` By default, computed fields are excluded. To also include all computed fields pass `include_computed=True` to the decorator. ```python import strawberry from .models import User @strawberry.experimental.pydantic.type( model=User, all_fields=True, include_computed=True ) class UserType: pass ``` ## Input types Input types are similar to types; we can create one by using the `strawberry.experimental.pydantic.input` decorator: ```python import strawberry from .models import User @strawberry.experimental.pydantic.input(model=User) class UserInput: id: strawberry.auto name: strawberry.auto friends: strawberry.auto ``` ## Interface types Interface types are similar to normal types; we can create one by using the `strawberry.experimental.pydantic.interface` decorator: ```python import strawberry from pydantic import BaseModel from typing import List # pydantic types class User(BaseModel): id: int name: str class NormalUser(User): friends: List[int] = [] class AdminUser(User): role: int # strawberry types @strawberry.experimental.pydantic.interface(model=User) class UserType: id: strawberry.auto name: strawberry.auto @strawberry.experimental.pydantic.type(model=NormalUser) class NormalUserType(UserType): # note the base class friends: strawberry.auto @strawberry.experimental.pydantic.type(model=AdminUser) class AdminUserType(UserType): role: strawberry.auto ``` ## Error Types In addition to object types and input types, Strawberry allows you to create "error types". You can use these error types to have a typed representation of Pydantic errors in GraphQL. Let's see an example: ```python from pydantic import BaseModel, constr import strawberry class User(BaseModel): id: int name: constr(min_length=2) signup_ts: Optional[datetime] = None friends: List[int] = [] @strawberry.experimental.pydantic.error_type(model=User) class UserError: id: strawberry.auto name: strawberry.auto friends: strawberry.auto ``` ```graphql type UserError { id: [String!] name: [String!] friends: [[String!]] } ``` where each field will hold a list of error messages ## Extending types You can use the usual Strawberry syntax to add additional new fields to the GraphQL type that aren't defined in the pydantic model ```python import strawberry from pydantic import BaseModel from .models import User class User(BaseModel): id: int name: str @strawberry.experimental.pydantic.type(model=User) class User: id: strawberry.auto name: strawberry.auto age: int ``` ```graphql type User { id: Int! name: String! age: Int! } ``` ## Converting types The generated types won't run any pydantic validation. This is to prevent confusion when extending types and also to be able to run validation exactly where it is needed. To convert a Pydantic instance to a Strawberry instance you can use `from_pydantic` on the Strawberry type: ```python import strawberry from typing import List, Optional from pydantic import BaseModel class User(BaseModel): id: int name: str @strawberry.experimental.pydantic.type(model=User) class UserType: id: strawberry.auto name: strawberry.auto instance = User(id="123", name="Jake") data = UserType.from_pydantic(instance) ``` If your Strawberry type includes additional fields that aren't defined in the pydantic model, you will need to use the `extra` parameter of `from_pydantic` to specify the values to assign to them. ```python import strawberry from typing import List, Optional from pydantic import BaseModel class User(BaseModel): id: int name: str @strawberry.experimental.pydantic.type(model=User) class UserType: id: strawberry.auto name: strawberry.auto age: int instance = User(id="123", name="Jake") data = UserType.from_pydantic(instance, extra={"age": 10}) ``` The data dictionary structure follows the structure of your data -- if you have a list of `User`, you should send an `extra` that is the list of `User` with the missing data (in this case, `age`). You don't need to send all fields; data from the model is used first and then the `extra` parameter is used to fill in any additional missing data. To convert a Strawberry instance to a pydantic instance and trigger validation, you can use `to_pydantic` on the Strawberry instance: ```python import strawberry from typing import List, Optional from pydantic import BaseModel class User(BaseModel): id: int name: str @strawberry.experimental.pydantic.input(model=User) class UserInput: id: strawberry.auto name: strawberry.auto input_data = UserInput(id="abc", name="Jake") # this will run pydantic's validation instance = input_data.to_pydantic() ``` ## Constrained types Strawberry supports [pydantic constrained types](https://pydantic-docs.helpmanual.io/usage/types/#constrained-types). Note that constraint is not enforced in the graphql type. Thus, we recommend always working on the pydantic type such that the validation is enforced. ```python from pydantic import BaseModel, conlist import strawberry class Example(BaseModel): friends: conlist(str, min_items=1) @strawberry.experimental.pydantic.input(model=Example, all_fields=True) class ExampleGQL: ... @strawberry.type class Query: @strawberry.field() def test(self, example: ExampleGQL) -> None: # friends may be an empty list here print(example.friends) # calling to_pydantic() runs the validation and raises # an error if friends is empty print(example.to_pydantic().friends) schema = strawberry.Schema(query=Query) ``` ```graphql input ExampleGQL { friends: [String!]! } type Query { test(example: ExampleGQL!): Void } ``` ## Classes with `__get_validators__` Pydantic BaseModels may define a custom type with [`__get_validators__`](https://pydantic-docs.helpmanual.io/usage/types/#classes-with-__get_validators__) logic. You will need to add a scalar type and add the mapping to the `scalar_overrides` argument in the Schema class. ```python import strawberry from pydantic import BaseModel class MyCustomType: @classmethod def __get_validators__(cls): yield cls.validate @classmethod def validate(cls, v): return MyCustomType() class Example(BaseModel): custom: MyCustomType @strawberry.experimental.pydantic.type(model=Example, all_fields=True) class ExampleGQL: ... MyScalarType = strawberry.scalar( MyCustomType, # or another function describing how to represent MyCustomType in the response serialize=str, parse_value=lambda v: MyCustomType(), ) @strawberry.type class Query: @strawberry.field() def test(self) -> ExampleGQL: return Example(custom=MyCustomType()) # Tells strawberry to convert MyCustomType into MyScalarType schema = strawberry.Schema(query=Query, scalar_overrides={MyCustomType: MyScalarType}) ``` ## Custom Conversion Logic Sometimes you might not want to translate your Pydantic model into Strawberry using the logic provided in the library. Sometimes types in Pydantic are unrepresentable in GraphQL (such as unions of scalar values) or structural changes are needed before the data is exposed in the schema. In these cases, there are two methods you can use to control the conversion logic more directly. First, you can use a different type annotation in your Strawberry model for a field type instead of using `strawberry.auto` to choose an equivalent type. This allows you to do things like converting values to custom scalar types or converting between basic types. Strawberry will call the constructor of the new type annotation with the field value as input, so this only works when conversion is possible through a constructor. ```python import base64 import strawberry from pydantic import BaseModel from typing import Union, NewType class User(BaseModel): id: Union[int, str] # Not representable in GraphQL hash: bytes Base64 = strawberry.scalar( NewType("Base64", bytes), serialize=lambda v: base64.b64encode(v).decode("utf-8"), parse_value=lambda v: base64.b64decode(v.encode("utf-8")), ) @strawberry.experimental.pydantic.type(model=User) class UserType: id: str # Serialize int values to strings hash: Base64 # Use a custom scalar to serialize values @strawberry.type class Query: @strawberry.field def test() -> UserType: return UserType.from_pydantic(User(id=123, hash=b"abcd")) schema = strawberry.Schema(query=Query) print(schema.execute_sync("query { test { id, hash } }").data) # {"test": {"id": "123", "hash": "YWJjZA=="}} ``` The other, more comprehensive, method for modifying the conversion logic is to provide custom implementations of `from_pydantic` and `to_pydantic`. This allows you full control over the conversion process and bypasses Strawberry's built in conversion rules completely, while still registering the new type as a Pydantic conversion type so it can be referenced in other models. This is useful when you need to represent structures that are very different from GraphQL standards, without changing the underlying Pydantic model. An example would be a use case that uses a `dict` field to store some semi-structured content, which is difficult to represent in GraphQL's strict type system. ```python import enum import dataclasses import strawberry from pydantic import BaseModel from typing import Any, Dict, Optional class ContentType(enum.Enum): NAME = "name" DESCRIPTION = "description" class User(BaseModel): id: str content: Dict[ContentType, str] @strawberry.experimental.pydantic.type(model=User) class UserType: id: strawberry.auto # Flatten the content dict into specific fields in the query content_name: Optional[str] = None content_description: Optional[str] = None @staticmethod def from_pydantic(instance: User, extra: Dict[str, Any] = None) -> "UserType": data = instance.dict() content = data.pop("content") data.update({f"content_{k.value}": v for k, v in content.items()}) return UserType(**data) def to_pydantic(self) -> User: data = dataclasses.asdict(self) # Pull out the content_* fields into a dict content = {} for enum_member in ContentType: key = f"content_{enum_member.value}" if data.get(key) is not None: content[enum_member.value] = data.pop(key) return User(content=content, **data) user = User(id="abc", content={ContentType.NAME: "Bob"}) print(UserType.from_pydantic(user)) # UserType(id='abc', content_name='Bob', content_description=None) user_type = UserType(id="abc", content_name="Bob", content_description=None) print(user_type.to_pydantic()) # id='abc' content={: 'Bob'} ``` strawberry-graphql-0.287.0/docs/integrations/quart.md000066400000000000000000000166661511033167500226650ustar00rootroot00000000000000--- title: Quart --- # Quart Strawberry comes with a basic Quart integration. It provides a view that you can use to serve your GraphQL schema: ```python from quart import Quart from strawberry.quart.views import GraphQLView from api.schema import schema view = GraphQLView.as_view("graphql_view", schema=schema) app = Quart(__name__) app.add_url_rule("/graphql", view_func=view) app.add_url_rule("/graphql", view_func=view, methods=["GET"], websocket=True) if __name__ == "__main__": app.run() ``` ## Options The `GraphQLView` accepts the following options at the moment: - `schema`: mandatory, the schema created by `strawberry.Schema`. - `graphql_ide`: optional, defaults to `"graphiql"`, allows to choose the GraphQL IDE interface (one of `graphiql`, `apollo-sandbox` or `pathfinder`) or to disable it by passing `None`. - `allow_queries_via_get`: optional, defaults to `True`, whether to enable queries via `GET` requests - `multipart_uploads_enabled`: optional, defaults to `False`, controls whether to enable multipart uploads. Please make sure to consider the [security implications mentioned in the GraphQL Multipart Request Specification](https://github.com/jaydenseric/graphql-multipart-request-spec/blob/master/readme.md#security) when enabling this feature. ## Extending the view The base `GraphQLView` class can be extended by overriding any of the following methods: - `async def get_context(self, request: Request, response: Response) -> Context` - `async def get_root_value(self, request: Request) -> Optional[RootValue]` - `async def process_result(self, request: Request, result: ExecutionResult) -> GraphQLHTTPResponse` - `def decode_json(self, data: Union[str, bytes]) -> object` - `def encode_json(self, data: object) -> str | bytes` - `async def render_graphql_ide(self, request: Request) -> Response` - `async def on_ws_connect(self, context: Context) -> Union[UnsetType, None, Dict[str, object]]` ### get_context `get_context` allows to provide a custom context object that can be used in your resolver. You can return anything here, by default we return a dictionary with the request. By default; the `Response` object from Quart is injected via the parameters. ```python import strawberry from strawberry.quart.views import GraphQLView from quart import Request, Response class MyGraphQLView(GraphQLView): async def get_context(self, request: Request, response: Response): return {"example": 1} @strawberry.type class Query: @strawberry.field def example(self, info: strawberry.Info) -> str: return str(info.context["example"]) ``` Here we are returning a custom context dictionary that contains only one item called "example". Then we use the context in a resolver, the resolver will return "1" in this case. ### get_root_value `get_root_value` allows to provide a custom root value for your schema, this is probably not used a lot but it might be useful in certain situations. Here's an example: ```python import strawberry from strawberry.quart.views import GraphQLView from quart import Request class MyGraphQLView(GraphQLView): async def get_root_value(self, request: Request): return Query(name="Patrick") @strawberry.type class Query: name: str ``` Here we are returning a Query where the name is "Patrick", so we when requesting the field name we'll return "Patrick" in this case. ### process_result `process_result` allows to customize and/or process results before they are sent to the clients. This can be useful logging errors or hiding them (for example to hide internal exceptions). It needs to return an object of `GraphQLHTTPResponse` and accepts the execution result. ```python from strawberry.quart.views import GraphQLView from strawberry.http import GraphQLHTTPResponse from strawberry.types import ExecutionResult from quart import Request class MyGraphQLView(GraphQLView): async def process_result( self, request: Request, result: ExecutionResult ) -> GraphQLHTTPResponse: data: GraphQLHTTPResponse = {"data": result.data} if result.errors: data["errors"] = [err.formatted for err in result.errors] return data ``` In this case we are doing the default processing of the result, but it can be tweaked based on your needs. ### decode_json `decode_json` allows to customize the decoding of HTTP JSON requests. By default we use `json.loads` but you can override this method to use a different decoder. ```python from strawberry.quart.views import GraphQLView from typing import Union import orjson class MyGraphQLView(GraphQLView): def decode_json(self, data: Union[str, bytes]) -> object: return orjson.loads(data) ``` Make sure your code raises `json.JSONDecodeError` or a subclass of it if the JSON cannot be decoded. The library shown in the example above, `orjson`, does this by default. ### encode_json `encode_json` allows to customize the encoding of HTTP and WebSocket JSON responses. By default we use `json.dumps` but you can override this method to use a different encoder. ```python import json from strawberry.quart.views import GraphQLView class MyGraphQLView(GraphQLView): def encode_json(self, data: object) -> str | bytes: return json.dumps(data, indent=2) ``` ### render_graphql_ide In case you need more control over the rendering of the GraphQL IDE than the `graphql_ide` option provides, you can override the `render_graphql_ide` method. ```python from strawberry.quart.views import GraphQLView from quart import Request, Response class MyGraphQLView(GraphQLView): async def render_graphql_ide(self, request: Request) -> Response: custom_html = """

Custom GraphQL IDE

""" return Response(custom_html) ``` ### on_ws_connect By overriding `on_ws_connect` you can customize the behavior when a `graphql-ws` or `graphql-transport-ws` connection is established. This is particularly useful for authentication and authorization. By default, all connections are accepted. To manually accept a connection, return `strawberry.UNSET` or a connection acknowledgment payload. The acknowledgment payload will be sent to the client. Note that the legacy protocol does not support `None`/`null` acknowledgment payloads, while the new protocol does. Our implementation will treat `None`/`null` payloads the same as `strawberry.UNSET` in the context of the legacy protocol. To reject a connection, raise a `ConnectionRejectionError`. You can optionally provide a custom error payload that will be sent to the client when the legacy GraphQL over WebSocket protocol is used. ```python from typing import Dict from strawberry.exceptions import ConnectionRejectionError from strawberry.quart.views import GraphQLView class MyGraphQLView(GraphQLView): async def on_ws_connect(self, context: Dict[str, object]): connection_params = context["connection_params"] if not isinstance(connection_params, dict): # Reject without a custom graphql-ws error payload raise ConnectionRejectionError() if connection_params.get("password") != "secret": # Reject with a custom graphql-ws error payload raise ConnectionRejectionError({"reason": "Invalid password"}) if username := connection_params.get("username"): # Accept with a custom acknowledgment payload return {"message": f"Hello, {username}!"} # Accept without an acknowledgment payload return await super().on_ws_connect(context) ``` strawberry-graphql-0.287.0/docs/integrations/sanic.md000066400000000000000000000131041511033167500226060ustar00rootroot00000000000000--- title: Sanic --- # Sanic Strawberry comes with a basic [Sanic](https://github.com/sanic-org/sanic) integration. It provides a view that you can use to serve your GraphQL schema: ```python from strawberry.sanic.views import GraphQLView from api.schema import Schema app = Sanic(__name__) app.add_route( GraphQLView.as_view(schema=schema, graphiql=True), "/graphql", ) ``` ## Options The `GraphQLView` accepts the following options at the moment: - `schema`: mandatory, the schema created by `strawberry.Schema`. - `graphql_ide`: optional, defaults to `"graphiql"`, allows to choose the GraphQL IDE interface (one of `graphiql`, `apollo-sandbox` or `pathfinder`) or to disable it by passing `None`. - `allow_queries_via_get`: optional, defaults to `True`, whether to enable queries via `GET` requests - `multipart_uploads_enabled`: optional, defaults to `False`, controls whether to enable multipart uploads. Please make sure to consider the [security implications mentioned in the GraphQL Multipart Request Specification](https://github.com/jaydenseric/graphql-multipart-request-spec/blob/master/readme.md#security) when enabling this feature. ## Extending the view The base `GraphQLView` class can be extended by overriding any of the following methods: - `async def get_context(self, request: Request, response: TemporalResponse) -> Context` - `async def get_root_value(self, request: Request) -> Optional[RootValue]` - `async def process_result(self, request: Request, result: ExecutionResult) -> GraphQLHTTPResponse` - `def decode_json(self, data: Union[str, bytes]) -> object` - `def encode_json(self, data: object) -> str | bytes` - `async def render_graphql_ide(self, request: Request) -> HTTPResponse` ### get_context By overriding `GraphQLView.get_context` you can provide a custom context object for your resolvers. You can return anything here; by default GraphQLView returns a dictionary with the request. ```python import strawberry from strawberry.sanic.views import GraphQLView from strawberry.http.temporal_response import TemporalResponse from sanic.request import Request class MyGraphQLView(GraphQLView): async def get_context(self, request: Request, response: TemporalResponse): return {"example": 1} @strawberry.type class Query: @strawberry.field def example(self, info: strawberry.Info) -> str: return str(info.context["example"]) ``` Here we are returning a custom context dictionary that contains only one item called `"example"`. Then we can use the context in a resolver. In this case the resolver will return `1`. ### get_root_value By overriding `GraphQLView.get_root_value` you can provide a custom root value for your schema. This is probably not used a lot but it might be useful in certain situations. Here's an example: ```python import strawberry from strawberry.sanic.views import GraphQLView from sanic.request import Request class MyGraphQLView(GraphQLView): async def get_root_value(self, request: Request): return Query(name="Patrick") @strawberry.type class Query: name: str ``` Here we configure a Query where requesting the `name` field will return `"Patrick"` through the custom root value. ### process_result By overriding `GraphQLView.process_result` you can customize and/or process results before they are sent to a client. This can be useful for logging errors, or even hiding them (for example to hide internal exceptions). It needs to return an object of `GraphQLHTTPResponse` and accepts the execution result. ```python from strawberry.sanic.views import GraphQLView from strawberry.http import GraphQLHTTPResponse, process_result from strawberry.types import ExecutionResult from sanic.request import Request class MyGraphQLView(GraphQLView): async def process_result( self, request: Request, result: ExecutionResult ) -> GraphQLHTTPResponse: if result.errors: result.errors = [err.formatted for err in result.errors] return process_result(data) ``` In this case we are doing the default processing of the result, but it can be tweaked based on your needs. ### decode_json `decode_json` allows to customize the decoding of HTTP JSON requests. By default we use `json.loads` but you can override this method to use a different decoder. ```python from strawberry.sanic.views import GraphQLView from typing import Union import orjson class MyGraphQLView(GraphQLView): def decode_json(self, data: Union[str, bytes]) -> object: return orjson.loads(data) ``` Make sure your code raises `json.JSONDecodeError` or a subclass of it if the JSON cannot be decoded. The library shown in the example above, `orjson`, does this by default. ### encode_json `encode_json` allows to customize the encoding of HTTP and WebSocket JSON responses. By default we use `json.dumps` but you can override this method to use a different encoder. ```python import json from strawberry.sanic.views import GraphQLView class MyGraphQLView(GraphQLView): def encode_json(self, data: object) -> str | bytes: return json.dumps(data, indent=2) ``` ### render_graphql_ide In case you need more control over the rendering of the GraphQL IDE than the `graphql_ide` option provides, you can override the `render_graphql_ide` method. ```python from strawberry.sanic.views import GraphQLView from sanic.request import Request from sanic.response import HTTPResponse, html class MyGraphQLView(GraphQLView): async def render_graphql_ide(self, request: Request) -> HTTPResponse: custom_html = """

Custom GraphQL IDE

""" return html(custom_html) ``` strawberry-graphql-0.287.0/docs/integrations/starlette.md000066400000000000000000000010541511033167500235210ustar00rootroot00000000000000--- title: Starlette --- # Starlette Strawberry provides support for [Starlette](https://www.starlette.io/) with the ASGI integration. See below example for integrating Starlette with Strawberry: ```python from starlette.applications import Starlette from strawberry.asgi import GraphQL from api.schema import schema graphql_app = GraphQL(schema) app = Starlette() app.add_route("/graphql", graphql_app) app.add_websocket_route("/graphql", graphql_app) ``` For more information about Strawberry ASGI refer to [the documentation on ASGI](./asgi.md) strawberry-graphql-0.287.0/docs/operations/000077500000000000000000000000001511033167500206455ustar00rootroot00000000000000strawberry-graphql-0.287.0/docs/operations/deployment.md000066400000000000000000000052421511033167500233520ustar00rootroot00000000000000--- title: Deployment --- # Deployment Before deploying your GraphQL app to production you should disable `GraphiQL` and `Introspection`. ## Why are they a problem? 1. They can reveal sensitive information (e.g. internal business logic) 2. They make it easier for malicious parties to reverse engineer your GraphQL API [See more on this topic](https://www.apollographql.com/blog/graphql/security/why-you-should-disable-graphql-introspection-in-production/) ## How to disable them ### GraphiQL GraphiQL is useful during testing and development but should be disabled in production by default. It can be turned off by setting the `graphql_ide` option to `None` See the Strawberry Options documentation for the integration you are using for more information on how to disable it: - [AIOHTTP](../integrations/aiohttp.md#options) - [ASGI](../integrations/asgi.md#options) - [Django](../integrations/django.md#options) - [FastAPI](../integrations/fastapi.md#options) - [Flask](../integrations/flask.md#options) - [Quart](../integrations/quart.md#options) - [Sanic](../integrations/sanic.md#options) - [Chalice](../integrations/chalice.md#options) - [Starlette](../integrations/starlette.md#options) ### Introspection Introspection should primarily be used as a discovery and diagnostic tool for testing and development, and should be disabled in production by default. You can disable introspection by [adding a validation rule extension](../extensions/add-validation-rules.md#more-examples). ## Security extensions Strawberry provides some security extensions to limit various aspects of the GraphQL document. These are recommended in production. - [query depth](../extensions/query-depth-limiter.md) - [max number of aliases](../extensions/max-aliases-limiter.md) - [max number of tokens](../extensions/max-tokens-limiter.md) # More resources See the documentation for the integration you are using for more information on deployment: - [AIOHTTP](https://docs.aiohttp.org/en/stable/deployment.html) - [Chalice](https://aws.github.io/chalice/quickstart.html#deploying) - [Django](https://docs.djangoproject.com/en/4.0/howto/deployment/) - [FastAPI](https://fastapi.tiangolo.com/deployment/) - [Flask](https://flask.palletsprojects.com/en/2.0.x/deploying/) - [Litestar](https://docs.litestar.dev/latest/topics/deployment/index.html) - [Sanic](https://sanic.dev/en/guide/deployment/configuration.html) The docs for [ASGI](https://asgi.readthedocs.io/en/latest/index.html) and [Starlette](https://www.starlette.io/) do not provide an official deployment guide, but you may find the documentation for other frameworks that use ASGI servers useful (e.g. [FastAPI](https://fastapi.tiangolo.com/deployment/)) strawberry-graphql-0.287.0/docs/operations/testing.md000066400000000000000000000072361511033167500226540ustar00rootroot00000000000000--- title: Testing --- # Testing The GraphiQL playground integrated with Strawberry available at [http://localhost:8000/graphql](http://localhost:8000/graphql) (if you run the schema with `strawberry dev`) can be a good place to start testing your queries and mutations. However, at some point, while you are developing your application (or even before if you are practising TDD), you may want to create some automated tests. We can use the Strawberry `schema` object we defined in the [Getting Started tutorial](../index.md#step-5-create-our-schema-and-run-it) to run our first test: ```python def test_query(): query = """ query TestQuery($title: String!) { books(title: $title) { title author } } """ result = schema.execute_sync( query, variable_values={"title": "The Great Gatsby"}, ) assert result.errors is None assert result.data["books"] == [ { "title": "The Great Gatsby", "author": "F. Scott Fitzgerald", } ] ``` This `test_query` example: 1. defines the query we will test against; it accepts one argument, `title`, as input 2. executes the query and assigns the result to a `result` variable 3. asserts that the result is what we are expecting: nothing in `errors` and our desired book in `data` As you may have noticed, we explicitly defined the query variable `title`, and we passed it separately with the `variable_values` argument, but we could have directly hardcoded the `title` in the query string instead. We did this on purpose because usually the query's arguments will be dynamic and, as we want to test our application as close to production as possible, it wouldn't make much sense to hardcode the variables in the query. ## Testing Async Since Strawberry supports async, tests can also be written to be async: ```python @pytest.mark.asyncio async def test_query_async(): ... resp = await schema.execute(query, variable_values={"title": "The Great Gatsby"}) ... ``` ## Testing Mutations We can also write a test for our [`addBook` Mutation](../general/mutations.md) example: ```python @pytest.mark.asyncio async def test_mutation(): mutation = """ mutation TestMutation($title: String!, $author: String!) { addBook(title: $title, author: $author) { title } } """ resp = await schema.execute( mutation, variable_values={ "title": "The Little Prince", "author": "Antoine de Saint-Exupéry", }, ) assert resp.errors is None assert resp.data["addBook"] == { "title": "The Little Prince", } ``` ## Testing Subscriptions And finally, a test for our [`count` Subscription](../general/subscriptions.md): ```python import asyncio import pytest import strawberry @strawberry.type class Subscription: @strawberry.subscription async def count(self, target: int = 100) -> int: for i in range(target): yield i await asyncio.sleep(0.5) @strawberry.type class Query: @strawberry.field def hello() -> str: return "world" schema = strawberry.Schema(query=Query, subscription=Subscription) @pytest.mark.asyncio async def test_subscription(): query = """ subscription { count(target: 3) } """ sub = await schema.subscribe(query) index = 0 async for result in sub: assert not result.errors assert result.data == {"count": index} index += 1 ``` As you can see testing Subscriptions is a bit more complicated because we want to check the result of each individual result. strawberry-graphql-0.287.0/docs/operations/tracing.md000066400000000000000000000153211511033167500226200ustar00rootroot00000000000000--- title: Tracing --- # Tracing ## Apollo To enable [Apollo tracing](https://github.com/apollographql/apollo-tracing) you can use the ApolloTracingExtension provided: ```python from strawberry.extensions.tracing import ApolloTracingExtension schema = strawberry.Schema(query=Query, extensions=[ApolloTracingExtension]) ``` Note that if you're not running under ASGI you'd need to use the sync version of ApolloTracingExtension: ```python from strawberry.extensions.tracing import ApolloTracingExtensionSync schema = strawberry.Schema(query=Query, extensions=[ApolloTracingExtensionSync]) ``` ## Datadog In addition to Apollo Tracing we also support tracing with [Datadog](https://www.datadoghq.com/). using the DatadogTracingExtension. ```python from strawberry.extensions.tracing import DatadogTracingExtension schema = strawberry.Schema(query=Query, extensions=[DatadogTracingExtension]) ``` Note that if you're not running under ASGI you'd need to use the sync version of DatadogTracingExtension: ```python from strawberry.extensions.tracing import DatadogTracingExtensionSync schema = strawberry.Schema(query=Query, extensions=[DatadogTracingExtensionSync]) ``` ## Open Telemetry In addition to Datadog and Apollo Tracing we also support [opentelemetry](https://opentelemetry.io/), using the OpenTelemetryExtension. You also need to install the extras for opentelemetry by doing: ```shell pip install 'strawberry-graphql[opentelemetry]' ``` ```python from strawberry.extensions.tracing import OpenTelemetryExtension schema = strawberry.Schema(query=Query, extensions=[OpenTelemetryExtension]) ``` Note that if you're not running under ASGI you'd need to use the sync version of OpenTelemetryExtension: ```python from strawberry.extensions.tracing import OpenTelemetryExtensionSync schema = strawberry.Schema(query=Query, extensions=[OpenTelemetryExtensionSync]) ``` Example Elasticsearch, Kibana, APM, Collector docker-compose to track django and strawberry tracing metrics This will spin up: - an elastic search instance to keep your data - kibana to visualize data - the elastic APM Server for processing incoming traces - a [collector binding](https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-otlp) to transform the opentelemetry data (more exactly the Opentelementry Line Protocol OTLP) to something AMP can read ([our APM agent](https://github.com/open-telemetry/opentelemetry-collector)) For more details see the elasticsearch [docs](https://www.elastic.co/guide/en/apm/get-started/current/open-telemetry-elastic.html) ```yaml version: "3" services: elasticsearch: image: docker.elastic.co/elasticsearch/elasticsearch:7.16.2 container_name: elasticsearch restart: always ulimits: memlock: soft: -1 hard: -1 environment: - discovery.type=single-node - bootstrap.memory_lock=true - "ES_JAVA_OPTS=-Xms1024m -Xmx1024m" - ELASTIC_PASSWORD=changeme - xpack.security.enabled=true volumes: - elasticsearch-data:/usr/share/elasticsearch/data healthcheck: interval: 10s retries: 12 test: curl -s http://localhost:9200/_cluster/health | grep -vq '"status":"red"' kibana: image: docker.elastic.co/kibana/kibana:7.16.2 container_name: kibana environment: ELASTICSEARCH_URL: "http://elasticsearch:9200" ELASTICSEARCH_HOSTS: '["http://elasticsearch:9200"]' ELASTICSEARCH_USERNAME: elastic ELASTICSEARCH_PASSWORD: changeme restart: always depends_on: elasticsearch: condition: service_healthy ports: - 127.0.0.1:5601:5601 apm-server: image: docker.elastic.co/apm/apm-server:7.16.2 container_name: apm-server user: apm-server restart: always command: [ "--strict.perms=false", "-e", "-E", "apm-server.host=0.0.0.0:8200", "-E", "apm-server.kibana.enabled=true", "-E", "apm-server.kibana.host=kibana:5601", "-E", "apm-server.kibana.username=elastic", "-E", "apm-server.kibana.password=changeme", "-E", "output.elasticsearch.hosts=['elasticsearch:9200']", "-E", "output.elasticsearch.enabled=true", "-E", "output.elasticsearch.username=elastic", "-E", "output.elasticsearch.password=changeme", ] depends_on: elasticsearch: condition: service_healthy cap_add: ["CHOWN", "DAC_OVERRIDE", "SETGID", "SETUID"] cap_drop: ["ALL"] healthcheck: interval: 10s retries: 12 test: curl --write-out 'HTTP %{http_code}' --fail --silent --output /dev/null http://localhost:8200/ otel-collector: image: otel/opentelemetry-collector:0.41.0 container_name: otel-collector restart: always command: "--config=/etc/otel-collector-config.yaml" volumes: - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml:ro depends_on: apm-server: condition: service_healthy ports: - 127.0.0.1:4317:4317 volumes: elasticsearch-data: external: true ``` In the same directory add a `otel-collector-config.yaml`: ```yaml receivers: otlp: protocols: grpc: processors: memory_limiter: check_interval: 1s limit_mib: 2000 batch: exporters: logging: loglevel: warn otlp/elastic: endpoint: "apm-server:8200" tls: insecure: true service: pipelines: traces: receivers: [otlp] exporters: [logging, otlp/elastic] processors: [batch] metrics: receivers: [otlp] exporters: [logging, otlp/elastic] processors: [batch] ``` Spin this docker-compose up with (this will take a while, give it a minute): ```shell docker-compose up --force-recreate --build ``` Example Django Integration Requirements: ```shell pip install opentelemetry-api pip install opentelemetry-sdk pip install opentelemetry-exporter-otlp ``` in the manage.py ```python from opentelemetry import trace from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter from opentelemetry.instrumentation.django import DjangoInstrumentor from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor resource = Resource(attributes={"service.name": "yourservicename"}) trace.set_tracer_provider(TracerProvider(resource=resource)) tracer = trace.get_tracer(__name__) otlp_exporter = OTLPSpanExporter(endpoint="http://localhost:4317", insecure=True) span_processor = BatchSpanProcessor(otlp_exporter) trace.get_tracer_provider().add_span_processor(span_processor) ... def main(): DjangoInstrumentor().instrument() ... ``` strawberry-graphql-0.287.0/docs/types/000077500000000000000000000000001511033167500176265ustar00rootroot00000000000000strawberry-graphql-0.287.0/docs/types/defer-and-stream.md000066400000000000000000000116311511033167500232700ustar00rootroot00000000000000--- title: Defer and Stream --- # Defer and Stream Strawberry provides experimental support for GraphQL's `@defer` and `@stream` directives, which enable incremental delivery of response data. These directives allow parts of a GraphQL response to be delivered as they become available, rather than waiting for the entire response to be ready. This feature requires `graphql-core>=3.3.0a9` and is currently experimental. The API and behavior may change in future releases. **Important limitations:** - Extensions (most importantly `MaskErrors`) are not fully supported yet. Extensions currently only process the initial result and do not handle incremental payloads delivered by `@defer` and `@stream`. - This means error masking and other extension functionality will only apply to the initial response, not to deferred or streamed data. ## Enabling Defer and Stream To use `@defer` and `@stream` directives, you need to enable experimental incremental execution in your schema configuration: ```python import strawberry from strawberry.schema.config import StrawberryConfig @strawberry.type class Query: # Your query fields here pass schema = strawberry.Schema( query=Query, config=StrawberryConfig(enable_experimental_incremental_execution=True) ) ``` ## Using @defer The `@defer` directive allows you to mark parts of a query to be resolved asynchronously. The initial response will include all non-deferred fields, followed by incremental payloads containing the deferred data. ### Example ```python import asyncio import strawberry @strawberry.type class Author: id: strawberry.ID name: str bio: str @strawberry.type class Article: id: strawberry.ID title: str content: str @strawberry.field async def author(self) -> Author: # Simulate an expensive operation await asyncio.sleep(2) return Author( id=strawberry.ID("1"), name="Jane Doe", bio="A passionate writer and developer.", ) @strawberry.type class Query: @strawberry.field async def article(self, id: strawberry.ID) -> Article: return Article( id=id, title="Introduction to GraphQL Defer", content="Learn how to use the @defer directive...", ) ``` With this schema, you can query with `@defer`: ```graphql query GetArticle { article(id: "123") { id title content ... on Article @defer { author { id name bio } } } } ``` The response will be delivered incrementally: ```json # Initial payload { "data": { "article": { "id": "123", "title": "Introduction to GraphQL Defer", "content": "Learn how to use the @defer directive..." } }, "hasNext": true } # Subsequent payload { "incremental": [{ "data": { "author": { "id": "1", "name": "Jane Doe", "bio": "A passionate writer and developer." } }, "path": ["article"] }], "hasNext": false } ``` ## Using @stream with strawberry.Streamable The `@stream` directive works with list fields and allows items to be delivered as they become available. Strawberry provides a special `Streamable` type annotation for fields that can be streamed. ### Example ```python import asyncio import strawberry from typing import AsyncGenerator @strawberry.type class Comment: id: strawberry.ID content: str author_name: str @strawberry.type class BlogPost: id: strawberry.ID title: str @strawberry.field async def comments(self) -> strawberry.Streamable[Comment]: """Stream comments as they are fetched from the database.""" for i in range(5): # Simulate fetching comments from a database await asyncio.sleep(0.5) yield Comment( id=strawberry.ID(f"comment-{i}"), content=f"This is comment number {i}", author_name=f"User {i}", ) ``` Query with `@stream`: ```graphql query GetBlogPost { blogPost(id: "456") { id title comments @stream(initialCount: 2) { id content authorName } } } ``` The response will stream the comments: ```json # Initial payload with first 2 comments { "data": { "blogPost": { "id": "456", "title": "My Blog Post", "comments": [ { "id": "comment-0", "content": "This is comment number 0", "authorName": "User 0" }, { "id": "comment-1", "content": "This is comment number 1", "authorName": "User 1" } ] } }, "hasNext": true } # Subsequent payloads for remaining comments { "incremental": [{ "items": [{ "id": "comment-2", "content": "This is comment number 2", "authorName": "User 2" }], "path": ["blogPost", "comments", 2] }], "hasNext": true } # ... more incremental payloads ``` strawberry-graphql-0.287.0/docs/types/enums.md000066400000000000000000000073071511033167500213060ustar00rootroot00000000000000--- title: Enums --- # Enums Enums are a special kind of type that is restricted to a particular set of values. For example, we have a few options of ice cream available, and we want to allow user to choose only from those options. Strawberry supports defining enums using enums from python's standard library. Here's a quick tutorial on how to create an enum type in Strawberry: First, create a new class for the new type, which extends class Enum: ```python from enum import Enum class IceCreamFlavour(Enum): ... ``` Then, list options as variables in that class: ```python class IceCreamFlavour(Enum): VANILLA = "vanilla" STRAWBERRY = "strawberry" CHOCOLATE = "chocolate" ``` Finally we need to register our class as a strawberry type. It's done with the `strawberry.enum` decorator: ```python @strawberry.enum class IceCreamFlavour(Enum): VANILLA = "vanilla" STRAWBERRY = "strawberry" CHOCOLATE = "chocolate" ``` In some cases you already have an enum defined elsewhere in your code. You can safely use it in your schema and strawberry will generate a default graphql implementation of it. The only drawback is that it is not currently possible to configure it (documentation / renaming or using `strawberry.enum_value` on it). Let's see how we can use Enums in our schema. ```python @strawberry.type class Query: @strawberry.field def best_flavour(self) -> IceCreamFlavour: return IceCreamFlavour.STRAWBERRY ``` Defining the enum type above would produce this schema in GraphQL: ```graphql enum IceCreamFlavour { VANILLA STRAWBERRY CHOCOLATE } ``` Here's an example of how you'd use this newly created query: ```graphql query { bestFlavour } ``` Here is result of executed query: ```graphql { "data": { "bestFlavour": "STRAWBERRY" } } ``` We can also use enums when defining object types (using `strawberry.type`). Here is an example of an object that has a field using an Enum: ```python @strawberry.type class Cone: flavour: IceCreamFlavour num_scoops: int @strawberry.type class Query: @strawberry.field def cone(self) -> Cone: return Cone(flavour=IceCreamFlavour.STRAWBERRY, num_scoops=4) ``` And here's an example of how you'd use this query: ```graphql query { cone { flavour numScoops } } ``` Here is result of executed query: ```graphql { "data": { "cone": { "flavour": "STRAWBERRY", "numScoops": 4 } } } ``` GraphQL types are not a map of name: value, like in python enums. Strawberry uses the name of the members of the enum to create the GraphQL type. You can also deprecate enum value. To do so you need more verbose syntax using `strawberry.enum_value` and `deprecation_reason`. You can mix and match string and verbose syntax. ```python @strawberry.enum class IceCreamFlavour(Enum): VANILLA = strawberry.enum_value("vanilla") CHOCOLATE = "chocolate" STRAWBERRY = strawberry.enum_value( "strawberry", deprecation_reason="Let's call the whole thing off" ) ``` You can also give custom names to enum values in the GraphQL schema using the `name` parameter: ```python @strawberry.enum class IceCreamFlavour(Enum): VANILLA = "vanilla" CHOCOLATE_COOKIE = strawberry.enum_value("chocolate", name="chocolateCookie") ``` This will produce the following GraphQL schema: ```graphql enum IceCreamFlavour { VANILLA chocolateCookie } ``` When querying, the custom name will be used in the response: ```graphql { "data": { "bestFlavour": "chocolateCookie" } } ``` Note that the Python enum member name (`CHOCOLATE_COOKIE`) is still used in your Python code, while the custom name (`chocolateCookie`) is used in the GraphQL schema and responses. strawberry-graphql-0.287.0/docs/types/exceptions.md000066400000000000000000000106361511033167500223370ustar00rootroot00000000000000--- title: Exceptions toc: true --- # Strawberry Exceptions Strawberry defines its library-specific exceptions in `strawberry.exceptions`. ## Strawberry Schema Exceptions ### FieldWithResolverAndDefaultFactoryError This exception is raised when `strawberry.field` is used with both `resolver` and `default_factory` arguments. ```python @strawberry.type class Query: @strawberry.field(default_factory=lambda: "Example C") def c(self) -> str: return "I'm a resolver" # Throws 'Field "c" on type "Query" cannot define a default_factory and a resolver.' ``` ### FieldWithResolverAndDefaultValueError This exception is raised when `strawberry.field` is used with both `resolver` and `default` arguments. ```python def test_resolver() -> str: return "I'm a resolver" @strawberry.type class Query: c: str = strawberry.field(default="Example C", resolver=test_resolver) # Throws 'Field "c" on type "Query" cannot define a default value and a resolver.' ``` ### MissingTypesForGenericError This exception is raised when a `Generic` type is added to the Strawberry Schema without passing any type to make it concrete. ### MultipleStrawberryArgumentsError This exception is raised when `strawberry.argument` is used multiple times in a type annotation. ```python import strawberry from typing_extensions import Annotated @strawberry.field def name( argument: Annotated[ str, strawberry.argument(description="This is a description"), strawberry.argument(description="Another description"), ], ) -> str: return "Name" # Throws 'Annotation for argument `argument` on field `name` cannot have multiple `strawberry.argument`s' ``` ### UnsupportedTypeError This exception is thrown when the type-annotation used is not supported by `strawberry.field`. At the time of writing this exception is used by Pydantic only ```python class Model(pydantic.BaseModel): field: pydantic.Json @strawberry.experimental.pydantic.type(Model, fields=["field"]) class Type: pass ``` ### WrongNumberOfResultsReturned This exception is raised when the DataLoader returns a different number of results than requested. ```python async def idx(keys): return [1, 2] loader = DataLoader(load_fn=idx) await loader.load(1) # Throws 'Received wrong number of results in dataloader, expected: 1, received: 2' ``` ## Runtime exceptions Some errors are also thrown when trying to exectuing queries (mutations or subscriptions). ### MissingQueryError This exception is raised when the `request` is missing the `query` parameter. ```python client.post("/graphql", data={}) # Throws 'Request data is missing a "query" value' ``` ## UnallowedReturnTypeForUnion This error is raised when the return type of a `Union` is not in the list of Union types. ```python @strawberry.type class Outside: c: int @strawberry.type class A: a: int @strawberry.type class B: b: int @strawberry.type class Mutation: @strawberry.mutation def hello(self) -> Union[A, B]: return Outside(c=5) query = """ mutation { hello { __typename ... on A { a } ... on B { b } } } """ result = schema.execute_sync(query) # result will look like: # ExecutionResult( # data=None, # errors=[ # GraphQLError( # "The type \"\" of the field \"hello\" is not in the list of the types of the union: \"['A', 'B']\"", # locations=[SourceLocation(line=3, column=9)], # path=["hello"], # ) # ], # extensions={}, # ) ``` ## WrongReturnTypeForUnion This exception is thrown when the Union type cannot be resolved because it's not a `strawberry.field`. ```python @strawberry.type class A: a: int @strawberry.type class B: b: int @strawberry.type class Query: ab: Union[A, B] = "ciao" # missing `strawberry.field` ! query = """{ ab { __typename, ... on A { a } } }""" result = schema.execute_sync(query, root_value=Query()) # result will look like: # ExecutionResult( # data=None, # errors=[ # GraphQLError( # 'The type "" cannot be resolved for the field "ab" , are you using a strawberry.field?', # locations=[SourceLocation(line=2, column=9)], # path=["ab"], # ) # ], # extensions={}, # ) ``` strawberry-graphql-0.287.0/docs/types/generics.md000066400000000000000000000076441511033167500217620ustar00rootroot00000000000000--- title: Generics --- # Generics Strawberry supports using Python's `Generic` typing to dynamically create reusable types. Strawberry will automatically generate the correct GraphQL schema from the combination of the generic type and the type arguments. Generics are supported in Object types, Input types, and Arguments to queries, mutations, and scalars. Let's take a look at an example: # Object Types ```python from typing import Generic, List, TypeVar import strawberry T = TypeVar("T") @strawberry.type class Page(Generic[T]): number: int items: List[T] ``` This example defines a generic type `Page` that can be used to represent a page of any type. For example, we can create a page of `User` objects: ```python import strawberry @strawberry.type class User: name: str @strawberry.type class Query: users: Page[User] ``` ```graphql type Query { users: UserPage! } type User { name: String! } type UserPage { number: Int! items: [User!]! } ``` It is also possible to use a specialized generic type directly. For example, the same example above could be written like this: ```python import strawberry @strawberry.type class User: name: str @strawberry.type class UserPage(Page[User]): ... @strawberry.type class Query: users: UserPage ``` ```graphql type Query { users: UserPage! } type User { name: String! } type UserPage { number: Int! items: [User!]! } ``` # Input and Argument Types Arguments to queries and mutations can also be made generic by creating Generic Input types. Here we'll define an input type that can serve as a collection of anything, then create a specialization by using as a filled-in argument on a mutation. ```python import strawberry from typing import Generic, List, Optional, TypeVar T = TypeVar("T") @strawberry.input class CollectionInput(Generic[T]): values: List[T] @strawberry.input class PostInput: name: str @strawberry.type class Post: id: int name: str @strawberry.type class Mutation: @strawberry.mutation def add_posts(self, posts: CollectionInput[PostInput]) -> bool: return True @strawberry.type class Query: most_recent_post: Optional[Post] = None schema = strawberry.Schema(query=Query, mutation=Mutation) ``` ```graphql input PostInputCollectionInput { values: [PostInput!]! } input PostInput { name: String! } type Post { id: Int! name: String! } type Query { mostRecentPost: Post } type Mutation { addPosts(posts: PostInputCollectionInput!): Boolean! } ``` > **Note**: Pay attention to the fact that both `CollectionInput` and > `PostInput` are Input types. Providing `posts: CollectionInput[Post]` to > `add_posts` (i.e. using the non-input `Post` type) would have resulted in an > error: > > ``` > PostCollectionInput fields cannot be resolved. Input field type must be a > GraphQL input type > ``` # Multiple Specializations Using multiple specializations of a Generic type will work as expected. Here we define a `Point2D` type and then specialize it for both `int`s and `float`s. ```python from typing import Generic, TypeVar import strawberry T = TypeVar("T") @strawberry.input class Point2D(Generic[T]): x: T y: T @strawberry.type class Mutation: @strawberry.mutation def store_line_float(self, a: Point2D[float], b: Point2D[float]) -> bool: return True @strawberry.mutation def store_line_int(self, a: Point2D[int], b: Point2D[int]) -> bool: return True ``` ```graphql type Mutation { storeLineFloat(a: FloatPoint2D!, b: FloatPoint2D!): Boolean! storeLineInt(a: IntPoint2D!, b: IntPoint2D!): Boolean! } input FloatPoint2D { x: Float! y: Float! } input IntPoint2D { x: Int! y: Int! } ``` # Variadic Generics Variadic Generics, introduced in [PEP-646][pep-646], are currently unsupported. [pep-646]: https://peps.python.org/pep-0646/ strawberry-graphql-0.287.0/docs/types/input-types.md000066400000000000000000000074431511033167500224610ustar00rootroot00000000000000--- title: Input types --- # Input types In addition to [object types](./object-types) GraphQL also supports input types. While being similar to object types, they are better suited for input data as they limit the kind of types you can use for fields. This is how the [GraphQL spec defines the difference between object types and input types](https://spec.graphql.org/June2018/#sec-Input-Objects): > The GraphQL Object type (ObjectTypeDefinition)... is inappropriate for re‐use > (as input), because Object types can contain fields that define arguments or > contain references to interfaces and unions, neither of which is appropriate > for use as an input argument. For this reason, input objects have a separate > type in the system. ## Defining input types In Strawberry, you can define input types by using the `@strawberry.input` decorator, like this: ```python import strawberry @strawberry.input class Point2D: x: float y: float ``` ```graphql input Point2D { x: Float! y: Float! } ``` Then you can use input types as argument for your fields or mutations: ```python import strawberry @strawberry.type class Mutation: @strawberry.mutation def store_point(self, a: Point2D) -> bool: return True ``` If you want to include optional arguments, you need to provide them with a default. For example if we want to expand on the above example to allow optional labeling of our point we could do: ```python import strawberry from typing import Optional @strawberry.input class Point2D: x: float y: float label: Optional[str] = None ``` ```graphql type Point2D { x: Float! y: Float! label: String = null } ``` When you need to distinguish between a field being set to `null` versus being completely absent (common in update operations), you can use `strawberry.Maybe`. See the [Maybe documentation](./maybe.md) for comprehensive examples and usage patterns. ## API `@strawberry.input(name: str = None, description: str = None)` Creates an input type from a class definition. - `name`: if set this will be the GraphQL name, otherwise the GraphQL will be generated by camel-casing the name of the class. - `description`: this is the GraphQL description that will be returned when introspecting the schema or when navigating the schema using GraphiQL. ## One Of Input Types Strawberry also supports defining input types that can have only one field set. This is based on the [OneOf Input Objects RFC](https://github.com/graphql/graphql-spec/pull/825) To define a one of input type you can use the `one_of` flag on the `@strawberry.input` decorator: ```python import strawberry @strawberry.input(one_of=True) class SearchBy: name: strawberry.Maybe[str] email: strawberry.Maybe[str] ``` ```graphql input SearchBy @oneOf { name: String email: String } ``` OneOf inputs use `strawberry.Maybe` to distinguish between fields that are explicitly not provided versus those that might be set to null. See the [Maybe documentation](./maybe.md) for more details on this usage pattern. ## Deprecating fields Fields can be deprecated using the argument `deprecation_reason`. This does not prevent the field from being used, it's only for documentation. See: [GraphQL field deprecation](https://spec.graphql.org/June2018/#sec-Field-Deprecation). ```python import strawberry from typing import Optional @strawberry.input class Point2D: x: float y: float z: Optional[float] = strawberry.field( deprecation_reason="3D coordinates are deprecated" ) label: Optional[str] = None ``` ```graphql input Point2D { x: Float! y: Float! z: Float @deprecated(reason: "3D coordinates are deprecated") label: String = null } ``` strawberry-graphql-0.287.0/docs/types/interfaces.md000066400000000000000000000105001511033167500222670ustar00rootroot00000000000000--- title: Interfaces --- # Interfaces Interfaces are an abstract type which may be implemented by object types. An interface has fields, but it’s never instantiated. Instead, objects may implement interfaces, which makes them a member of that interface. Also, fields may return interface types. When this happens, the returned object may be any member of that interface. For example, let's say a `Customer` (interface) can either be an `Individual` (object) or a `Company` (object). Here's what that might look like in the [GraphQL Schema Definition Language](https://graphql.org/learn/schema/#type-language) (SDL): ```graphql interface Customer { name: String! } type Company implements Customer { employees: [Individual!]! name: String! } type Individual implements Customer { employed_by: Company name: String! } type Query { customers: [Customer!]! } ``` Notice that the `Customer` interface requires the `name: String!` field. Both `Company` and `Individual` implement that field so that they can satisfy the `Customer` interface. When querying, you can select the fields on an interface: ```graphql query { customers { name } } ``` Whether the object is a `Company` or an `Individual`, it doesn’t matter – you still get their name. If you want some object-specific fields, you can query them with an [inline fragment](https://graphql.org/learn/queries/#inline-fragments), for example: ```graphql query { customers { name ... on Individual { company { name } } } } ``` Interfaces are a good choice whenever you have a set of objects that are used interchangeably, and they have several significant fields in common. When they don’t have fields in common, use a [Union](/docs/types/union) instead. ## Defining interfaces Interfaces are defined using the `@strawberry.interface` decorator: ```python import strawberry @strawberry.interface class Customer: name: str ``` ```graphql interface Customer { name: String! } ``` Interface classes should never be instantiated directly. ## Implementing interfaces To define an object type that implements an interface, the type must inherit from the interface: ```python import strawberry @strawberry.type class Individual(Customer): # additional fields ... @strawberry.type class Company(Customer): # additional fields ... ``` If you add an object type which implements an interface, but that object type doesn’t appear in your schema as a field return type or a union member, then you will need to add that object to the Schema definition directly. ```python schema = strawberry.Schema(query=Query, types=[Individual, Company]) ``` Interfaces can also implement other interfaces: ```python import strawberry @strawberry.interface class Error: message: str @strawberry.interface class FieldError(Error): message: str field: str @strawberry.type class PasswordTooShort(FieldError): message: str field: str min_length: int ``` ```graphql interface Error { message: String! } interface FieldError implements Error { message: String! field: String! } type PasswordTooShort implements FieldError & Error { message: String! field: String! minLength: Int! } ``` ## Implementing fields Interfaces can provide field implementations as well. For example: ```python import strawberry @strawberry.interface class Customer: @strawberry.field def name(self) -> str: return self.name.title() ``` This resolve method will be called by objects who implement the interface. Object classes can override the implementation by defining their own `name` field: ```python import strawberry @strawberry.type class Company(Customer): @strawberry.field def name(self) -> str: return f"{self.name} Limited" ``` ## Resolving an interface When a field’s return type is an interface, GraphQL needs to know what specific object type to use for the return value. In the example above, each customer must be categorized as an `Individual` or `Company`. To do this you need to always return an instance of an object type from your resolver: ```python import strawberry @strawberry.type class Query: @strawberry.field def best_customer(self) -> Customer: return Individual(name="Patrick") ``` strawberry-graphql-0.287.0/docs/types/lazy.md000066400000000000000000000022341511033167500211300ustar00rootroot00000000000000--- title: Lazy Types --- # Lazy Types Strawberry supports lazy types, which are useful when you have circular dependencies between types. For example, let's say we have a `User` type that has a list of `Post` types, and each `Post` type has a `User` field. In this case, we can't define the `User` type before the `Post` type, and vice versa. To solve this, we can use lazy types: ```python # posts.py from typing import TYPE_CHECKING, Annotated import strawberry if TYPE_CHECKING: from .users import User @strawberry.type class Post: title: str author: Annotated["User", strawberry.lazy(".users")] ``` ```python # users.py from typing import TYPE_CHECKING, Annotated, List import strawberry if TYPE_CHECKING: from .posts import Post @strawberry.type class User: name: str posts: List[Annotated["Post", strawberry.lazy(".posts")]] ``` `strawberry.lazy` in combination with `Annotated` allows us to define the path of the module of the type we want to use, this allows us to leverage Python's type hints, while preventing circular imports and preserving type safety by using `TYPE_CHECKING` to tell type checkers where to look for the type. strawberry-graphql-0.287.0/docs/types/maybe.md000066400000000000000000000135451511033167500212550ustar00rootroot00000000000000--- title: Maybe --- # Maybe In GraphQL, there's an important distinction between a field that is `null` and a field that is completely absent from the input. Strawberry's `Maybe` type allows you to differentiate between these states: For `Maybe[str]`: 1. **Field present with a value**: `Some("hello")` 2. **Field completely absent**: `None` For `Maybe[str | None]` (when you need to handle explicit nulls): 1. **Field present with a value**: `Some("hello")` 2. **Field present but explicitly null**: `Some(None)` 3. **Field completely absent**: `None` This is particularly useful for update operations where you need to distinguish between "set this field to null" and "don't change this field at all". ## What problem does `strawberry.Maybe` solve? Consider this common scenario: you have a user profile with an optional phone number, and you want to provide an update mutation. With traditional nullable types, you can't distinguish between: - "Set the phone number to null" (remove the phone number) - "Don't change the phone number" (leave it as is) Both would be represented as `phone: null` in your GraphQL mutation. ## Basic Usage Here's how to use `Maybe` in your Strawberry schema: ```python import strawberry @strawberry.input class UpdateUserInput: name: str | None = None # Traditional optional field phone: strawberry.Maybe[str | None] # Maybe field @strawberry.type class User: name: str phone: str | None @strawberry.type class Mutation: @strawberry.mutation def update_user(self, user_id: str, input: UpdateUserInput) -> User: user = get_user(user_id) # Your user retrieval logic # Traditional optional field - only update if provided if input.name is not None: user.name = input.name # Maybe field - check if field was provided at all if input.phone is not None: # Field was provided user.phone = input.phone.value # Access the actual value # If input.phone is None, the field wasn't provided - no change return user ``` ## Understanding Some() When a `Maybe` field has a value (including `null`), it's wrapped in a `Some()` container: ```python # Field provided with a string value phone = strawberry.Some("555-1234") print(phone.value) # "555-1234" # Field provided with null value phone = strawberry.Some(None) print(phone.value) # None # Field not provided at all phone = None print(phone) # None ``` ## GraphQL Schema When you use `Maybe` in your schema, it appears as a nullable field in GraphQL: ```python @strawberry.input class UpdateUserInput: phone: strawberry.Maybe[str | None] ``` Generates this GraphQL schema: ```graphql input UpdateUserInput { phone: String } ``` ## Common Patterns ### Input Types for Updates `Maybe` is most commonly used in input types for update operations: ```python @strawberry.input class UpdatePostInput: title: strawberry.Maybe[str] # Can be set to new value or omitted content: strawberry.Maybe[str] published: strawberry.Maybe[bool] tags: strawberry.Maybe[list[str] | None] # Can be set, cleared, or omitted @strawberry.type class Mutation: @strawberry.mutation def update_post(self, post_id: str, input: UpdatePostInput) -> Post: post = get_post(post_id) # Only update fields that were explicitly provided if input.title: post.title = input.title.value if input.content: post.content = input.content.value if input.published: post.published = input.published.value if input.tags: post.tags = input.tags.value # Could be None to clear tags return post ``` ## Maybe vs Optional vs Nullable Understanding the differences between these approaches: | Type | Python | GraphQL | Absent | Null | Value | | ------------------------------- | ---------- | --------- | -------- | ------------- | -------------- | | `str` | Required | `String!` | ❌ Error | ❌ Error | ✅ Value | | `str \| None` | Optional | `String` | ✅ None | ✅ None | ✅ Value | | `strawberry.Maybe[str]` | Maybe | `String` | ✅ None | ❌ Error | ✅ Some(value) | | `strawberry.Maybe[str \| None]` | Maybe+Null | `String` | ✅ None | ✅ Some(None) | ✅ Some(value) | ## Best Practices ### When to Use Maybe Use `Maybe` when you need to distinguish between: - Field not provided (no change) - Field provided with null (clear/remove) - Field provided with value (set/update) Common use cases: - Update mutations - Patch operations - Optional filters that need to distinguish between "not filtering" and "filtering by null" ### When to Use Optional Instead Use regular optional types (`str | None`) when: - You only need two states: value or null - The field absence and null have the same meaning - You're defining output types (GraphQL responses) ### Error Handling Always check if a `Maybe` field was provided before accessing its value: ```python # Good if input.phone is not None: user.phone = input.phone.value # Bad - will raise AttributeError if phone is None user.phone = input.phone.value ``` ### Helper Functions You can create helper functions to make Maybe handling cleaner: ```python def update_if_provided(obj, field_name: str, maybe_value): """Update object field only if Maybe value was provided.""" if maybe_value is not None: setattr(obj, field_name, maybe_value.value) # Usage update_if_provided(user, "phone", input.phone) update_if_provided(user, "email", input.email) ``` ## Related Types - [Input Types](./input-types.md) - Using Maybe in input type definitions - [Resolvers](./resolvers.md) - Using Maybe in resolver arguments - [Union Types](./union.md) - Combining Maybe with union types - [Scalars](./scalars.md) - Custom scalar types with Maybe strawberry-graphql-0.287.0/docs/types/object-types.md000066400000000000000000000037631511033167500225710ustar00rootroot00000000000000--- title: Object types --- # Object types Object types are the fundamentals of any GraphQL schema, they are used to define the kind of objects that exist in a schema. Object types are created by defining a name and a list of fields, here’s an example object type defined using the GraphQL schema language: ```graphql type Character { name: String! age: Int! } ``` ## A note on Query, Mutation and Subscription While reading about GraphQL you might have encountered 3 special object types: `Query`, `Mutation` and `Subscription`. They are defined as standard object types, with the difference that they are also used as entry points for your schema (also referred as root types). - `Query` is the entry point for all the query operations - `Mutation` is the entry point for all the mutations - `Subscription` is the entry point for all the subscriptions. For a walk-through on how to define schemas, read the [schema basics](../general/schema-basics.md). ## Defining object types In Strawberry, you can define object types by using the `@strawberry.type` decorator, like this: ```python import strawberry @strawberry.type class Character: name: str age: int ``` ```graphql type Character { name: String! age: int! } ``` You can also refer to other types, like this: ```python import strawberry @strawberry.type class Character: name: str age: int @strawberry.type class Book: title: str main_character: Character ``` ```graphql type Character { name: String! age: Int! } type Book { title: String! mainCharacter: Character! } ``` ## API `@strawberry.type(name: str = None, description: str = None)` Creates an object type from a class definition. `name`: if set this will be the GraphQL name, otherwise the GraphQL will be generated by camel-casing the name of the class. `description`: this is the GraphQL description that will be returned when introspecting the schema or when navigating the schema using GraphiQL. strawberry-graphql-0.287.0/docs/types/operation-directives.md000066400000000000000000000066311511033167500243150ustar00rootroot00000000000000--- title: Operation directives --- # Operation directives GraphQL uses directives to modify the evaluation of an item in the schema or the operation. Operation directives can be included inside any operation (query, subscription, mutation) and can be used to modify the execution of the operation or the values returned by the operation. Directives can help avoid having to create resolvers for values that can be computed via the values of additional fields. All Directives are proceeded by `@` symbol # Default Operation directives Strawberry provides the following default operation directives: - `@skip(if: Boolean!)` - if Boolean is true, the given item is NOT resolved by the GraphQL Server - `@include(if: Boolean!)` - if Boolean is false, the given item is NOT resolved by the GraphQL Server ## Experimental Directives When [experimental incremental execution](./schema-configurations#enable_experimental_incremental_execution) is enabled, these additional directives become available: - `@defer(if: Boolean, label: String)` - Allows fields to be resolved asynchronously and delivered incrementally. The field will be omitted from the initial response and sent in a subsequent payload. - `@stream(if: Boolean, label: String, initialCount: Int)` - Enables streaming of list fields. The list will be delivered incrementally, with `initialCount` items in the initial response and remaining items in subsequent payloads. These experimental directives require `graphql-core>=3.3.0a9` and must be enabled via schema configuration. See [Defer and Stream](./defer-and-stream) for detailed usage information. `@deprecated(reason: String)` IS NOT compatible with Operation directives. `@deprecated` is exclusive to [Schema Directives](./schema-directives.md) **Examples of Default Operation directives** ```graphql # @include query getPerson($includePoints: Boolean!) { person { name points @include(if: $includePoints) } } # @skip query getPerson($hideName: Boolean!) { person { name @skip(if: $hideName) points } } ``` # Custom Operation directives Custom directives must be defined in the schema to be used within the query and can be used to decorate other parts of the schema. ```python # Definition @strawberry.directive( locations=[DirectiveLocation.FIELD], description="Make string uppercase" ) def turn_uppercase(value: str): return value.upper() @strawberry.directive(locations=[DirectiveLocation.FIELD]) def replace(value: str, old: str, new: str): return value.replace(old, new) ``` ```graphql # Use query People($identified: Boolean!) { person { name @turnUppercase } jess: person { name @replace(old: "Jess", new: "Jessica") } johnDoe: person { name @replace(old: "Jess", new: "John") @include(if: $identified) } } ``` # Locations for Operation directives Directives can only appear in _specific_ locations inside the query. These locations must be included in the directive's definition. In Strawberry the location is defined in the directive function's parameter `locations`. ```graphql @strawberry.directive(locations=[DirectiveLocation.FIELD]) ``` **Operation directives possible locations** Operation directives can be applied to many different parts of an operation. Here's the list of all the allowed locations: - `QUERY` - `MUTATION` - `SUBSCRIPTION` - `FIELD` - `FRAGMENT_DEFINITION` - `FRAGMENT_SPREAD` - `INLINE_FRAGMENT` strawberry-graphql-0.287.0/docs/types/private.md000066400000000000000000000032451511033167500216260ustar00rootroot00000000000000--- title: Private Fields --- # Private Fields Private (external) fields can provide local context for later resolution. These fields will act as plain fields so will not be exposed in the GraphQL API. Some uses include: - Context that relies upon field inputs. - Avoiding fully materializing an object hierarchy (lazy resolution) # Defining a private field Specifying a field with `strawberry.Private[...]` will desigate it as internal and not for GraphQL. # Example Consider the following type, which can accept any Python object and handle converting it to string, representation, or templated output: ```python @strawberry.type class Stringable: value: strawberry.Private[object] @strawberry.field def string(self) -> str: return str(self.value) @strawberry.field def repr(self) -> str: return repr(self.value) @strawberry.field def format(self, template: str) -> str: return template.format(my=self.value) ``` The `Private[...]` type lets Strawberry know that this field is not a GraphQL field. "value" is a regular field on the class, but it is not exposed on the GraphQL API. ```python @strawberry.type class Query: @strawberry.field def now(self) -> Stringable: return Stringable(value=datetime.datetime.now()) ``` Queries can then select the fields and formats desired, but formatting only happens as requested: ```graphql { now { format(template: "{my.year}") string repr } } ``` ```json { "data": { "now": { "format": "2022", "string": "2022-09-03 17:03:04.923068", "repr": "datetime.datetime(2022, 9, 3, 17, 3, 4, 923068)" } } } ``` strawberry-graphql-0.287.0/docs/types/resolvers.md000066400000000000000000000135761511033167500222100ustar00rootroot00000000000000--- title: Resolvers --- # Resolvers When defining a GraphQL schema, you usually start with the definition of the schema for your API, for example, let's take a look at this schema: ```python import strawberry @strawberry.type class User: name: str @strawberry.type class Query: last_user: User ``` ```graphql type User { name: String! } type Query { lastUser: User! } ``` We have defined a `User` type and a `Query` type. Next, to define how the data is returned from our server, we will attach resolvers to our fields. ## Let's define a resolver Let's create a resolver and attach it to the `lastUser` field. A resolver is a Python function that returns data. In Strawberry there are two ways of defining resolvers; the first is to pass a function to the field definition, like this: ```python def get_last_user() -> User: return User(name="Marco") @strawberry.type class Query: last_user: User = strawberry.field(resolver=get_last_user) ``` Now when Strawberry executes the following query, it will call the `get_last_user` function to fetch the data for the `lastUser` field: ```graphql { lastUser { name } } ``` ```json { "data": { "lastUser": { "name": "Marco" } } } ``` ## Defining resolvers as methods The other way to define a resolver is to use `strawberry.field` as a decorator, like here: ```python @strawberry.type class Query: @strawberry.field def last_user(self) -> User: return User(name="Marco") ``` This is useful when you want to co-locate resolvers and types or when you have very small resolvers. If you're curious how the `self` parameter works in the resolver, you can read more about it in the [accessing parent data guide](../guides/accessing-parent-data.md). ## Defining arguments Fields can also have arguments; in Strawberry the arguments for a field are defined on the resolver, as you would normally do in a Python function. Let's define a field on a Query that returns a user by ID: ```python import strawberry @strawberry.type class User: name: str @strawberry.type class Query: @strawberry.field def user(self, id: strawberry.ID) -> User: # here you'd use the `id` to get the user from the database return User(name="Marco") ``` ```graphql type User { name: String! } type Query { user(id: ID!): User! } ``` ### Optional arguments Optional or nullable arguments can be expressed using `Optional`: ```python from typing import Optional import strawberry @strawberry.type class Query: @strawberry.field def hello(self, name: Optional[str] = None) -> str: if name is None: return "Hello world!" return f"Hello {name}!" ``` If you need to differentiate between `null` and no arguments being passed (for example, distinguishing between "set to null" vs "don't change"), you can use `strawberry.Maybe`. See the [Maybe documentation](./maybe.md) for comprehensive usage examples and patterns. ### Annotated Arguments Additional metadata can be added to arguments, for example a custom name or deprecation reason, using `strawberry.argument` with [typing.Annotated](https://docs.python.org/3/library/typing.html#typing.Annotated): ```python from typing import Optional, Annotated import strawberry @strawberry.type class Query: @strawberry.field def greet( self, name: Optional[str] = None, is_morning: Annotated[ Optional[bool], strawberry.argument( name="morning", deprecation_reason="The field now automatically detects if it's morning or not", ), ] = None, ) -> str: ... ``` ## Accessing execution information Sometimes it is useful to access the information for the current execution context. Strawberry allows to declare a parameter of type `Info` that will be automatically passed to the resolver. This parameter contains the information for the current execution context. ```python import strawberry from strawberry.types import Info def full_name(root: "User", info: strawberry.Info) -> str: return f"{root.first_name} {root.last_name} {info.field_name}" @strawberry.type class User: first_name: str last_name: str full_name: str = strawberry.field(resolver=full_name) ``` You don't have to call this parameter `info`, its name can be anything. Strawberry uses the type to pass the correct value to the resolver. ### API Info objects contain information for the current execution context: `class Info(Generic[ContextType, RootValueType])` | Parameter name | Type | Description | | --------------- | ------------------------- | --------------------------------------------------------------------- | | field_name | `str` | The name of the current field (generally camel-cased) | | python_name | `str` | The 'Python name' of the field (generally snake-cased) | | context | `ContextType` | The value of the context | | root_value | `RootValueType` | The value for the root type | | variable_values | `Dict[str, Any]` | The variables for this operation | | operation | `OperationDefinitionNode` | The ast for the current operation (public API might change in future) | | path | `Path` | The path for the current field | | selected_fields | `List[SelectedField]` | Additional information related to the current field | | schema | `Schema` | The Strawberry schema instance | strawberry-graphql-0.287.0/docs/types/scalars.md000066400000000000000000000173741511033167500216140ustar00rootroot00000000000000--- title: Scalars --- # Scalars Scalar types represent concrete values at the leaves of a query. For example in the following query the `name` field will resolve to a scalar type (in this case it's a `String` type): ```graphql "name" { user { name } } ``` ```json '"name": "Patrick"' { "data": { "user": { "name": "Patrick" } } } ``` There are several built-in scalars, and you can define custom scalars too. ([Enums](/docs/types/enums) are also leaf values.) The built in scalars are: - `String`, maps to Python’s `str` - `Int`, a signed 32-bit integer, maps to Python’s `int` - `Float`, a signed double-precision floating-point value, maps to Python’s `float` - `Boolean`, true or false, maps to Python’s `bool` - `ID`, a specialised `String` for representing unique object identifiers - `Date`, an ISO-8601 encoded [date](https://docs.python.org/3/library/datetime.html#date-objects) - `DateTime`, an ISO-8601 encoded [datetime](https://docs.python.org/3/library/datetime.html#datetime-objects) - `Time`, an ISO-8601 encoded [time](https://docs.python.org/3/library/datetime.html#time-objects) - `Decimal`, a [Decimal](https://docs.python.org/3/library/decimal.html#decimal.Decimal) value serialized as a string - `UUID`, a [UUID](https://docs.python.org/3/library/uuid.html#uuid.UUID) value serialized as a string - `Void`, always null, maps to Python’s `None` - `JSON`, a JSON value as specified in [ECMA-404](https://ecma-international.org/publications-and-standards/standards/ecma-404/) standard, maps to Python’s `dict` - `Base16`, `Base32`, `Base64`, represents hexadecimal strings encoded with `Base16`/`Base32`/`Base64`. As specified in [RFC4648](https://datatracker.ietf.org/doc/html/rfc4648.html). Maps to Python’s `str` Fields can return built-in scalars by using the Python equivalent: ```python import datetime import decimal import uuid import strawberry @strawberry.type class Product: id: uuid.UUID name: str stock: int is_available: bool available_from: datetime.date same_day_shipping_before: datetime.time created_at: datetime.datetime price: decimal.Decimal void: None ``` ```graphql type Product { id: UUID! name: String! stock: Int! isAvailable: Boolean! availableFrom: Date! sameDayShippingBefore: Time! createdAt: DateTime! price: Decimal! void: Void } ``` Scalar types can also be used as inputs: ```python 'date_input: datetime.date' import datetime import strawberry @strawberry.type class Query: @strawberry.field def one_week_from(self, date_input: datetime.date) -> datetime.date: return date_input + datetime.timedelta(weeks=1) ``` ## Custom scalars You can create custom scalars for your schema to represent specific types in your data model. This can be helpful to let clients know what kind of data they can expect for a particular field. To define a custom scalar you need to give it a name and functions that tell Strawberry how to serialize and deserialise the type. For example here is a custom scalar type to represent a Base64 string: ```python import base64 from typing import NewType import strawberry Base64 = strawberry.scalar( NewType("Base64", bytes), serialize=lambda v: base64.b64encode(v).decode("utf-8"), parse_value=lambda v: base64.b64decode(v.encode("utf-8")), ) @strawberry.type class Query: @strawberry.field def base64(self) -> Base64: return Base64(b"hi") schema = strawberry.Schema(Query) result = schema.execute_sync("{ base64 }") assert results.data == {"base64": "aGk="} ``` The `Base16`, `Base32` and `Base64` scalar types are available in `strawberry.scalars` ```python from strawberry.scalars import Base16, Base32, Base64 ``` ## Example JSONScalar ```python import json from typing import Any, NewType import strawberry JSON = strawberry.scalar( NewType("JSON", object), description="The `JSON` scalar type represents JSON values as specified by ECMA-404", serialize=lambda v: v, parse_value=lambda v: v, ) ``` Usage: ```python @strawberry.type class Query: @strawberry.field def data(self, info) -> JSON: return {"hello": {"a": 1}, "someNumbers": [1, 2, 3]} ``` ```graphql query ExampleDataQuery { data } ``` ```json { "data": { "hello": { "a": 1 }, "someNumbers": [1, 2, 3] } } ``` The `JSON` scalar type is available in `strawberry.scalars` ```python from strawberry.scalars import JSON ``` ## Overriding built in scalars To override the behaviour of the built in scalars you can pass a map of overrides to your schema. Here is a full example of replacing the built in `DateTime` scalar with one that serializes all datetimes as unix timestamps: ```python from datetime import datetime, timezone import strawberry # Define your custom scalar EpochDateTime = strawberry.scalar( datetime, serialize=lambda value: int(value.timestamp()), parse_value=lambda value: datetime.fromtimestamp(int(value), timezone.utc), ) @strawberry.type class Query: @strawberry.field def current_time(self) -> datetime: return datetime.now() schema = strawberry.Schema( Query, scalar_overrides={ datetime: EpochDateTime, }, ) result = schema.execute_sync("{ currentTime }") assert result.data == {"currentTime": 1628683200} ``` ### Replacing datetime with the popular `pendulum` library To override with a pendulum instance you'd want to serialize and parse_value like the above example. Let's throw them in a class this time. In addition we'll be using the `Union` clause to combine possible input types. Since pendulum isn't typed yet, we'll have to silence mypy's errors using `# type: ignore` ```python import pendulum from datetime import datetime class DateTime: """ This class is used to convert the pendulum.DateTime type to a string and back to a pendulum.DateTime type """ @staticmethod def serialize(dt: Union[pendulum.DateTime, datetime]) -> str: # type: ignore try: return dt.isoformat() except ValueError: return dt.to_iso8601_string() # type: ignore @staticmethod def parse_value(value: str) -> Union[pendulum.DateTime, datetime]: # type: ignore return pendulum.parse(value) # type: ignore date_time = strawberry.scalar( Union[pendulum.DateTime, datetime], # type: ignore name="datetime", description="A date and time", serialize=DateTime.serialize, parse_value=DateTime.parse_value, ) ``` ## BigInt (64-bit integers) Python by default allows, integer size to be 2^64. However the graphql spec has capped it to 2^32. This will inevitably raise errors. Instead of using strings on the client as a workaround, you could use the following scalar: ```python # This is needed because GraphQL does not support 64 bit integers BigInt = strawberry.scalar( Union[int, str], # type: ignore serialize=lambda v: int(v), parse_value=lambda v: str(v), description="BigInt field", ) ``` You can adapt your schema to automatically use this scalar for all integers by using the `scalar_overrides` parameter Only use this override if you expect most of your integers to be 64-bit. Since most GraphQL schemas follow standardized design patterns and most clients require additional effort to handle all numbers as strings, it makes more sense to reserve BigInt for numbers that actually exceed the 32-bit limit. You can achieve this by annotating `BigInt` instead of `int` in your resolvers handling large python integers. ```python user_schema = strawberry.Schema( query=Query, mutation=Mutation, subscription=Subscription, scalar_overrides={datetime: date_time, int: BigInt}, ) ``` strawberry-graphql-0.287.0/docs/types/schema-configurations.md000066400000000000000000000100311511033167500244330ustar00rootroot00000000000000--- title: Schema Configurations --- # Schema Configurations Strawberry allows to customise how the schema is generated by passing configurations. To customise the schema you can create an instance of `StrawberryConfig`, as shown in the example below: ```python import strawberry from strawberry.schema.config import StrawberryConfig @strawberry.type class Query: example_field: str schema = strawberry.Schema(query=Query, config=StrawberryConfig(auto_camel_case=False)) ``` In this case we are disabling the auto camel casing feature, so your output schema will look like this: ```graphql type Query { example_field: String! } ``` ## Available configurations Here's a list of the available configurations: ### auto_camel_case By default Strawberry will convert the field names to camel case, so a field like `example_field` will be converted to `exampleField`. You can disable this feature by setting `auto_camel_case` to `False`. ```python schema = strawberry.Schema(query=Query, config=StrawberryConfig(auto_camel_case=False)) ``` ### default_resolver By default Strawberry will use the `getattr` function as the default resolver. You can customise this by setting the `default_resolver` configuration. This can be useful in cases you want to allow returning a dictionary from a resolver. ```python import strawberry from strawberry.schema.config import StrawberryConfig def custom_resolver(obj, field): try: return obj[field] except (KeyError, TypeError): return getattr(obj, field) @strawberry.type class User: name: str @strawberry.type class Query: @strawberry.field def user(self, info) -> User: # this won't type check, but will work at runtime return {"name": "Patrick"} schema = strawberry.Schema( query=Query, config=StrawberryConfig(default_resolver=custom_resolver) ) ``` ### relay_max_results By default Strawberry's max limit for relay connections is 100. You can customise this by setting the `relay_max_results` configuration. ```python schema = strawberry.Schema(query=Query, config=StrawberryConfig(relay_max_results=50)) ``` ### disable_field_suggestions By default Strawberry will suggest fields when a field is not found in the schema. You can disable this feature by setting `disable_field_suggestions` to `True`. ```python schema = strawberry.Schema( query=Query, config=StrawberryConfig(disable_field_suggestions=True) ) ``` ### info_class By default Strawberry will create an object of type `strawberry.Info` when the user defines `info: Info` as a parameter to a type or query. You can change this behaviour by setting `info_class` to a subclass of `strawberry.Info`. This can be useful when you want to create a simpler interface for info- or context-based properties, or if you wanted to attach additional properties to the `Info` class. ```python class CustomInfo(Info): @property def response_headers(self) -> Headers: return self.context["response"].headers schema = strawberry.Schema(query=Query, config=StrawberryConfig(info_class=CustomInfo)) ``` ### enable_experimental_incremental_execution This is an experimental feature that requires `graphql-core>=3.3.0a9`. The API and behavior may change in future releases. By default, Strawberry executes GraphQL queries synchronously and returns the complete result. When you enable experimental incremental execution, Strawberry adds support for the `@defer` and `@stream` directives, allowing parts of the response to be delivered incrementally. ```python schema = strawberry.Schema( query=Query, config=StrawberryConfig(enable_experimental_incremental_execution=True) ) ``` When enabled: - The `@defer` directive becomes available for deferred field resolution - The `@stream` directive becomes available for streaming list fields - Fields returning `strawberry.Streamable[T]` can be streamed incrementally - The schema uses `graphql.experimental_execute_incrementally` for execution For more information on using these directives, see the [Defer and Stream](./defer-and-stream) documentation. strawberry-graphql-0.287.0/docs/types/schema-directives.md000066400000000000000000000057021511033167500235530ustar00rootroot00000000000000--- title: Schema Directives --- # Schema Directives Strawberry supports [schema directives](https://spec.graphql.org/June2018/#TypeSystemDirectiveLocation), which are directives that don't change the behavior of your GraphQL schema but instead provide a way to add additional metadata to it. > For example our [Apollo Federation integration](../guides/federation.md) is > based on schema directives. Let's see how you can implement a schema directive in Strawberry, here we are creating a directive called `keys` that can be applied to [Object types definitions](./object-types.md) and accepts one parameter called `fields`. Note that directive names, by default, are converted to camelCase on the GraphQL schema. Here's how we can use it in our schema: ```python import strawberry from strawberry.schema_directive import Location @strawberry.schema_directive(locations=[Location.OBJECT]) class Keys: fields: str from .directives import Keys @strawberry.type(directives=[Keys(fields="id")]) class User: id: strawberry.ID name: str ``` This will result in the following schema: ```graphql type User @keys(fields: "id") { id: ID! name: String! } ``` ## Overriding field names You can use `strawberry.directive_field` to override the name of a field: ```python @strawberry.schema_directive(locations=[Location.OBJECT]) class Keys: fields: str = strawberry.directive_field(name="as") ``` ## Locations Schema directives can be applied to many different parts of a schema. Here's the list of all the allowed locations: | Name | | Description | | ---------------------- | ----------------------- | -------------------------------------------------------- | | SCHEMA | `strawberry.Schema` | The definition of a schema | | SCALAR | `strawberry.scalar` | The definition of a scalar | | OBJECT | `strawberry.type` | The definition of an object type | | FIELD_DEFINITION | `strawberry.field` | The definition of a field on an object type or interface | | ARGUMENT_DEFINITION | `strawberry.argument` | The definition of an argument | | INTERFACE | `strawberry.interface` | The definition of an interface | | UNION | `strawberry.union` | The definition of an union | | ENUM | `strawberry.enum` | The definition of a enum | | ENUM_VALUE | `strawberry.enum_value` | The definition of a enum value | | INPUT_OBJECT | `strawberry.input` | The definition of an input object type | | INPUT_FIELD_DEFINITION | `strawberry.field` | The definition of a field on an input type | strawberry-graphql-0.287.0/docs/types/schema.md000066400000000000000000000160501511033167500214120ustar00rootroot00000000000000--- title: Schema --- # Schema Every GraphQL API has a schema and that is used to define all the functionalities for an API. A schema is defined by passing 3 [object types](./object-types): `Query`, `Mutation` and `Subscription`. `Mutation` and `Subscription` are optional, meanwhile `Query` has to always be there. This is an example of a schema defined using Strawberry: ```python import strawberry @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "Hello World" schema = strawberry.Schema(Query) ``` ## API reference ```python class Schema(Query, mutation=None, subscription=None, **kwargs): ... ``` #### `query: Type` The root query Strawberry type. Usually called `Query`. A query type is always required when creating a Schema. #### `mutation: Optional[Type] = None` The root mutation type. Usually called `Mutation`. #### `subscription: Optional[Type] = None` The root subscription type. Usually called `Subscription`. #### `config: Optional[StrawberryConfig] = None` Pass a `StrawberryConfig` object to configure how the schema is generated. [Read more](/docs/types/schema-configurations). #### `types: List[Type] = []` List of extra types to register with the Schema that are not directly linked to from the root Query.
Defining extra `types` when using Interfaces ```python from datetime import date import strawberry @strawberry.interface class Customer: name: str @strawberry.type class Individual(Customer): date_of_birth: date @strawberry.type class Company(Customer): founded: date @strawberry.type class Query: @strawberry.field def get_customer( self, id: strawberry.ID ) -> Customer: # note we're returning the interface here if id == "mark": return Individual(name="Mark", date_of_birth=date(1984, 5, 14)) if id == "facebook": return Company(name="Facebook", founded=date(2004, 2, 1)) schema = strawberry.Schema(Query, types=[Individual, Company]) ```
#### `extensions: List[Type[SchemaExtension]] = []` List of [extensions](/docs/extensions) to add to your Schema. #### `scalar_overrides: Optional[Dict[object, ScalarWrapper]] = None` Override the implementation of the built in scalars. [More information](/docs/types/scalars#overriding-built-in-scalars). --- ## Methods ### `.execute()` (async) Executes a GraphQL operation against a schema (async) ```python async def execute( query, variable_values, context_value, root_value, operation_name ): ... ``` #### `query: str` The GraphQL document to be executed. #### `variable_values: Optional[Dict[str, Any]] = None` The variables for this operation. #### `context_value: Optional[Any] = None` The value of the context that will be passed down to resolvers. #### `root_value: Optional[Any] = None` The value for the root value that will passed to root resolvers. #### `operation_name: Optional[str] = None` The name of the operation you want to execute, useful when sending a document with multiple operations. If no `operation_name` is specified the first operation in the document will be executed. ### `.execute_sync()` Executes a GraphQL operation against a schema ```python def execute_sync(query, variable_values, context_value, root_value, operation_name): ... ``` #### `query: str` The GraphQL document to be executed. #### `variable_values: Optional[Dict[str, Any]] = None` The variables for this operation. #### `context_value: Optional[Any] = None` The value of the context that will be passed down to resolvers. #### `root_value: Optional[Any] = None` The value for the root value that will passed to root resolvers. #### `operation_name: Optional[str] = None` The name of the operation you want to execute, useful when sending a document with multiple operations. If no `operation_name` is specified the first operation in the document will be executed. --- ## Handling execution errors By default Strawberry will log any errors encountered during a query execution to a `strawberry.execution` logger. This behaviour can be changed by overriding the `process_errors` function on the `strawberry.Schema` class. The default functionality looks like this: ```python # strawberry/schema/base.py from strawberry.types import ExecutionContext logger = logging.getLogger("strawberry.execution") class BaseSchema: ... def process_errors( self, errors: List[GraphQLError], execution_context: Optional[ExecutionContext] = None, ) -> None: for error in errors: StrawberryLogger.error(error, execution_context) ``` ```python # strawberry/utils/logging.py from strawberry.types import ExecutionContext class StrawberryLogger: logger: Final[logging.Logger] = logging.getLogger("strawberry.execution") @classmethod def error( cls, error: GraphQLError, execution_context: Optional[ExecutionContext] = None, # https://www.python.org/dev/peps/pep-0484/#arbitrary-argument-lists-and-default-argument-values **logger_kwargs: Any, ) -> None: # "stack_info" is a boolean; check for None explicitly if logger_kwargs.get("stack_info") is None: logger_kwargs["stack_info"] = True logger_kwargs["stacklevel"] = 3 cls.logger.error(error, exc_info=error.original_error, **logger_kwargs) ``` ## Filtering/customising fields You can customise the fields that are exposed on a schema by subclassing the `Schema` class and overriding the `get_fields` method, for example you can use this to create different GraphQL APIs, such as a public and an internal API. Here's an example of this: ```python @strawberry.type class User: name: str email: str = strawberry.field(metadata={"tags": ["internal"]}) @strawberry.type class Query: user: User def public_field_filter(field: StrawberryField) -> bool: return "internal" not in field.metadata.get("tags", []) class PublicSchema(strawberry.Schema): def get_fields( self, type_definition: StrawberryObjectDefinition ) -> List[StrawberryField]: return list(filter(public_field_filter, type_definition.fields)) schema = PublicSchema(query=Query) ``` The `get_fields` method is only called once when creating the schema, this is not intended to be used to dynamically customise the schema. ## Deprecating fields Fields can be deprecated using the argument `deprecation_reason`. This does not prevent the field from being used, it's only for documentation. See: [GraphQL field deprecation](https://spec.graphql.org/June2018/#sec-Field-Deprecation). ```python import strawberry import datetime from typing import Optional @strawberry.type class User: name: str dob: datetime.date age: Optional[int] = strawberry.field(deprecation_reason="Age is deprecated") ``` ```graphql type User { name: String! dob: Date! age: Int @deprecated(reason: "Age is deprecated") } ``` strawberry-graphql-0.287.0/docs/types/union.md000066400000000000000000000074021511033167500213030ustar00rootroot00000000000000--- title: Union types --- # Union types Union types are similar to [interfaces](/docs/types/interfaces) however, while interfaces dictate fields that must be common to all implementations, unions do not. Unions only represent a selection of allowed types and make no requirements on those types. Here’s a union, expressed in [GraphQL Schema Definition Language](https://graphql.org/learn/schema/#type-language) (SDL): ```graphql union MediaItem = Audio | Video | Image ``` Whenever we return a `MediaItem` in our schema, we might get an `Audio`, a `Video` or an `Image`. Note that members of a union type need to be concrete object types; you cannot create a union type out of interfaces, other unions or scalars. A good use case for unions would be on a search field. For example: ```graphql searchMedia(term: "strawberry") { ... on Audio { duration } ... on Video { thumbnailUrl } ... on Image { src } } ``` Here, the `searchMedia` field returns `[MediaItem!]!`, a list where each member is part of the `MediaItem` union. So, for each member, we want to select different fields depending on which kind of object that member is. We can do that by using [inline fragments](https://graphql.org/learn/queries/#inline-fragments). ## Defining unions In Strawberry there are two ways to define a union: You can use the `Union` type from the `typing` module which will autogenerate the type name from the names of the union members: ```python from typing import Union import strawberry @strawberry.type class Audio: duration: int @strawberry.type class Video: thumbnail_url: str @strawberry.type class Image: src: str @strawberry.type class Query: latest_media: Union[Audio, Video, Image] ``` ```graphql union AudioVideoImage = Audio | Video | Image type Query { latestMedia: AudioVideoImage! } type Audio { duration: Int! } type Video { thumbnailUrl: String! } type Image { src: String! } ``` Or if you need to specify a name or a description for a union you can use Annotated with the `strawberry.union` function: ```python import strawberry from typing import Union, Annotated @strawberry.type class Query: latest_media: Annotated[Union[Audio, Video, Image], strawberry.union("MediaItem")] ``` ```graphql union MediaItem = Audio | Video | Image type Query { latest_media: MediaItem! } type Audio { duration: Int! } type Video { thumbnailUrl: String! } type Image { src: String! } ``` ## Resolving a union When a field’s return type is a union, GraphQL needs to know what specific object type to use for the return value. In the example above, each `MediaItem` must be categorized as an `Audio`, `Image` or `Video` type. To do this you need to always return an instance of an object type from your resolver: ```python from typing import Union import strawberry @strawberry.type class Query: @strawberry.field def latest_media(self) -> Union[Audio, Video, Image]: return Video( thumbnail_url="https://i.ytimg.com/vi/dQw4w9WgXcQ/hq720.jpg", ) ``` ## Single member union Sometimes you might want to define a union with only one member. This is useful for future proofing your schema, for example if you want to add more types to the union in the future. Python's `typing.Union` does not really support this use case, but using Annotated and `strawberry.union` you can tell Strawberry that you want to define a union with only one member: ```python import strawberry from typing import Annotated @strawberry.type class Audio: duration: int @strawberry.type class Query: latest_media: Annotated[Audio, strawberry.union("MediaItem")] ``` ```graphql union MediaItem = Audio type Query { latestMedia: MediaItem! } ``` strawberry-graphql-0.287.0/e2e/000077500000000000000000000000001511033167500162055ustar00rootroot00000000000000strawberry-graphql-0.287.0/e2e/.gitattributes000066400000000000000000000000311511033167500210720ustar00rootroot00000000000000*.graphql.ts auto eol=lf strawberry-graphql-0.287.0/e2e/.gitignore000066400000000000000000000004521511033167500201760ustar00rootroot00000000000000# Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? !src/lib/ playwright-report/ test-results/ strawberry-graphql-0.287.0/e2e/app.py000066400000000000000000000024331511033167500173410ustar00rootroot00000000000000import asyncio # noqa: INP001 import random import strawberry from strawberry.schema.config import StrawberryConfig @strawberry.type class Author: id: strawberry.ID name: str @strawberry.type class Comment: id: strawberry.ID content: str @strawberry.field async def author(self) -> Author: await asyncio.sleep(random.randint(0, 2)) # noqa: S311 return Author(id=strawberry.ID("Author:1"), name="John Doe") @strawberry.type class BlogPost: id: strawberry.ID title: str content: str @strawberry.field async def comments(self) -> strawberry.Streamable[Comment]: for x in range(5): await asyncio.sleep(random.choice([0, 0.5, 1, 1.5, 2])) # noqa: S311 yield Comment(id=strawberry.ID(f"Comment:{x}"), content="Great post!") @strawberry.type class Query: @strawberry.field async def hello(self, delay: float = 0) -> str: await asyncio.sleep(delay) return "Hello, world!" @strawberry.field async def blog_post(self, id: strawberry.ID) -> BlogPost: return BlogPost(id=id, title="My Blog Post", content="This is my blog post.") schema = strawberry.Schema( query=Query, config=StrawberryConfig( enable_experimental_incremental_execution=True, ), ) strawberry-graphql-0.287.0/e2e/app.ts000066400000000000000000000057621511033167500173470ustar00rootroot00000000000000import { buildSchema, experimentalExecuteIncrementally, GraphQLObjectType, GraphQLString, GraphQLID, GraphQLNonNull, GraphQLList, GraphQLFloat, GraphQLSchema, GraphQLDirective, GraphQLBoolean, GraphQLInt, DirectiveLocation, } from "graphql"; import { createHandler } from "graphql-http/lib/use/express"; import express from "express"; import cors from "cors"; // Simulate async delay const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); // Define directives const DeferDirective = new GraphQLDirective({ name: "defer", locations: [ DirectiveLocation.FRAGMENT_SPREAD, DirectiveLocation.INLINE_FRAGMENT, ], args: { if: { type: GraphQLBoolean }, label: { type: GraphQLString }, }, }); const StreamDirective = new GraphQLDirective({ name: "stream", locations: [DirectiveLocation.FIELD], args: { if: { type: GraphQLBoolean }, label: { type: GraphQLString }, initialCount: { type: GraphQLInt, defaultValue: 0 }, }, }); // Define types using GraphQLObjectType const CommentType = new GraphQLObjectType({ name: "Comment", fields: { id: { type: new GraphQLNonNull(GraphQLID) }, content: { type: new GraphQLNonNull(GraphQLString) }, }, }); const BlogPostType = new GraphQLObjectType({ name: "BlogPost", fields: { id: { type: new GraphQLNonNull(GraphQLID) }, title: { type: new GraphQLNonNull(GraphQLString) }, content: { type: new GraphQLNonNull(GraphQLString) }, comments: { type: new GraphQLNonNull( new GraphQLList(new GraphQLNonNull(CommentType)), ), resolve: async () => { await delay(4000); return [ { id: "1", content: "Great post!" }, { id: "2", content: "Thanks for sharing!" }, ]; }, }, }, }); const QueryType = new GraphQLObjectType({ name: "Query", fields: { hello: { type: new GraphQLNonNull(GraphQLString), args: { delay: { type: GraphQLFloat, defaultValue: 0 }, }, resolve: async (_: unknown, { delay: delayMs }: { delay: number }) => { await delay(delayMs * 1000); return "Hello, world!"; }, }, blogPost: { type: new GraphQLNonNull(BlogPostType), args: { id: { type: GraphQLID }, }, resolve: (_: unknown, { id }: { id: string }) => { return { id, title: "My Blog Post", content: "This is my blog post.", }; }, }, }, }); // Create the schema const schema = new GraphQLSchema({ query: QueryType, directives: [DeferDirective, StreamDirective], }); const app = express(); // Enable CORS for all routes app.use( cors({ origin: "http://localhost:5173", // Allow requests from your frontend methods: ["GET", "POST", "OPTIONS"], // Allow these HTTP methods allowedHeaders: ["Content-Type", "Authorization"], // Allow these headers credentials: true, // Allow credentials (cookies, authorization headers, etc.) }), ); // Create and use the GraphQL handler. app.all( "/graphql", createHandler({ schema, execute: experimentalExecuteIncrementally, }), ); app.listen(4000, () => { console.log("Server is running on port 4000"); }); strawberry-graphql-0.287.0/e2e/bun.lock000066400000000000000000002737731511033167500176660ustar00rootroot00000000000000{ "lockfileVersion": 1, "workspaces": { "": { "name": "e2e", "dependencies": { "@apollo/client": "^3.13.8", "@radix-ui/react-slot": "^1.2.3", "@tailwindcss/vite": "^4.1.11", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "graphql": "^16.11.0", "graphql-http": "^1.22.4", "lucide-react": "^0.484.0", "react": "^19.1.0", "react-dom": "^19.1.0", "react-relay": "^18.2.0", "relay-runtime": "^18.2.0", "tailwind-merge": "^3.3.1", "tailwindcss": "^4.1.11", "tailwindcss-animate": "^1.0.7", "tw-animate-css": "^1.3.5", }, "devDependencies": { "@eslint/js": "^9.31.0", "@playwright/test": "^1.54.1", "@types/node": "^22.16.4", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@types/react-relay": "^18.2.1", "@types/relay-runtime": "^18.2.5", "@vitejs/plugin-react": "^4.6.0", "babel-plugin-relay": "^18.2.0", "eslint": "^9.31.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", "globals": "^15.15.0", "relay-compiler": "^18.2.0", "typescript": "~5.7.3", "typescript-eslint": "^8.37.0", "vite": "^6.3.5", "vite-plugin-relay": "^2.1.0", }, }, }, "packages": { "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], "@apollo/client": ["@apollo/client@3.13.8", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@wry/caches": "^1.0.0", "@wry/equality": "^0.5.6", "@wry/trie": "^0.5.0", "graphql-tag": "^2.12.6", "hoist-non-react-statics": "^3.3.2", "optimism": "^0.18.0", "prop-types": "^15.7.2", "rehackt": "^0.1.0", "symbol-observable": "^4.0.0", "ts-invariant": "^0.10.3", "tslib": "^2.3.0", "zen-observable-ts": "^1.2.5" }, "peerDependencies": { "graphql": "^15.0.0 || ^16.0.0", "graphql-ws": "^5.5.5 || ^6.0.3", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc", "subscriptions-transport-ws": "^0.9.0 || ^0.11.0" }, "optionalPeers": ["graphql-ws", "react", "react-dom", "subscriptions-transport-ws"] }, "sha512-YM9lQpm0VfVco4DSyKooHS/fDTiKQcCHfxr7i3iL6a0kP/jNO5+4NFK6vtRDxaYisd5BrwOZHLJpPBnvRVpKPg=="], "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], "@babel/compat-data": ["@babel/compat-data@7.28.0", "", {}, "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw=="], "@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="], "@babel/generator": ["@babel/generator@7.28.0", "", { "dependencies": { "@babel/parser": "^7.28.0", "@babel/types": "^7.28.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg=="], "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="], "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], "@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="], "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.27.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.27.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg=="], "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="], "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], "@babel/helpers": ["@babel/helpers@7.27.6", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.27.6" } }, "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug=="], "@babel/parser": ["@babel/parser@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.0" }, "bin": "./bin/babel-parser.js" }, "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g=="], "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], "@babel/runtime": ["@babel/runtime@7.27.0", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw=="], "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], "@babel/traverse": ["@babel/traverse@7.28.0", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/types": "^7.28.0", "debug": "^4.3.1" } }, "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg=="], "@babel/types": ["@babel/types@7.28.1", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ=="], "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.25.1", "", { "os": "android", "cpu": "arm" }, "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q=="], "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.1", "", { "os": "android", "cpu": "arm64" }, "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA=="], "@esbuild/android-x64": ["@esbuild/android-x64@0.25.1", "", { "os": "android", "cpu": "x64" }, "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw=="], "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ=="], "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA=="], "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A=="], "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww=="], "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.1", "", { "os": "linux", "cpu": "arm" }, "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ=="], "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ=="], "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ=="], "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.1", "", { "os": "linux", "cpu": "none" }, "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg=="], "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.1", "", { "os": "linux", "cpu": "none" }, "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg=="], "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg=="], "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.1", "", { "os": "linux", "cpu": "none" }, "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ=="], "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ=="], "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA=="], "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.1", "", { "os": "none", "cpu": "arm64" }, "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g=="], "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.1", "", { "os": "none", "cpu": "x64" }, "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA=="], "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg=="], "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw=="], "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg=="], "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ=="], "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A=="], "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.1", "", { "os": "win32", "cpu": "x64" }, "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg=="], "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.5.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w=="], "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="], "@eslint/config-array": ["@eslint/config-array@0.21.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ=="], "@eslint/config-helpers": ["@eslint/config-helpers@0.3.0", "", {}, "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw=="], "@eslint/core": ["@eslint/core@0.15.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA=="], "@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="], "@eslint/js": ["@eslint/js@9.31.0", "", {}, "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw=="], "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], "@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.3", "", { "dependencies": { "@eslint/core": "^0.15.1", "levn": "^0.4.1" } }, "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag=="], "@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="], "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], "@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="], "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.2", "", {}, "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ=="], "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="], "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], "@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="], "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], "@playwright/test": ["@playwright/test@1.54.1", "", { "dependencies": { "playwright": "1.54.1" }, "bin": { "playwright": "cli.js" } }, "sha512-FS8hQ12acieG2dYSksmLOF7BNxnVf2afRJdCuM1eMSxj6QTSE6G4InGF7oApGgDb65MX7AwMVlIkpru0yZA4Xw=="], "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.19", "", {}, "sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.37.0", "", { "os": "android", "cpu": "arm" }, "sha512-l7StVw6WAa8l3vA1ov80jyetOAEo1FtHvZDbzXDO/02Sq/QVvqlHkYoFwDJPIMj0GKiistsBudfx5tGFnwYWDQ=="], "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.37.0", "", { "os": "android", "cpu": "arm64" }, "sha512-6U3SlVyMxezt8Y+/iEBcbp945uZjJwjZimu76xoG7tO1av9VO691z8PkhzQ85ith2I8R2RddEPeSfcbyPfD4hA=="], "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.37.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+iTQ5YHuGmPt10NTzEyMPbayiNTcOZDWsbxZYR1ZnmLnZxG17ivrPSWFO9j6GalY0+gV3Jtwrrs12DBscxnlYA=="], "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.37.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-m8W2UbxLDcmRKVjgl5J/k4B8d7qX2EcJve3Sut7YGrQoPtCIQGPH5AMzuFvYRWZi0FVS0zEY4c8uttPfX6bwYQ=="], "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.37.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-FOMXGmH15OmtQWEt174v9P1JqqhlgYge/bUjIbiVD1nI1NeJ30HYT9SJlZMqdo1uQFyt9cz748F1BHghWaDnVA=="], "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.37.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-SZMxNttjPKvV14Hjck5t70xS3l63sbVwl98g3FlVVx2YIDmfUIy29jQrsw06ewEYQ8lQSuY9mpAPlmgRD2iSsA=="], "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.37.0", "", { "os": "linux", "cpu": "arm" }, "sha512-hhAALKJPidCwZcj+g+iN+38SIOkhK2a9bqtJR+EtyxrKKSt1ynCBeqrQy31z0oWU6thRZzdx53hVgEbRkuI19w=="], "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.37.0", "", { "os": "linux", "cpu": "arm" }, "sha512-jUb/kmn/Gd8epbHKEqkRAxq5c2EwRt0DqhSGWjPFxLeFvldFdHQs/n8lQ9x85oAeVb6bHcS8irhTJX2FCOd8Ag=="], "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.37.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-oNrJxcQT9IcbcmKlkF+Yz2tmOxZgG9D9GRq+1OE6XCQwCVwxixYAa38Z8qqPzQvzt1FCfmrHX03E0pWoXm1DqA=="], "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.37.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-pfxLBMls+28Ey2enpX3JvjEjaJMBX5XlPCZNGxj4kdJyHduPBXtxYeb8alo0a7bqOoWZW2uKynhHxF/MWoHaGQ=="], "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.37.0", "", { "os": "linux", "cpu": "none" }, "sha512-yCE0NnutTC/7IGUq/PUHmoeZbIwq3KRh02e9SfFh7Vmc1Z7atuJRYWhRME5fKgT8aS20mwi1RyChA23qSyRGpA=="], "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.37.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NxcICptHk06E2Lh3a4Pu+2PEdZ6ahNHuK7o6Np9zcWkrBMuv21j10SQDJW3C9Yf/A/P7cutWoC/DptNLVsZ0VQ=="], "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.37.0", "", { "os": "linux", "cpu": "none" }, "sha512-PpWwHMPCVpFZLTfLq7EWJWvrmEuLdGn1GMYcm5MV7PaRgwCEYJAwiN94uBuZev0/J/hFIIJCsYw4nLmXA9J7Pw=="], "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.37.0", "", { "os": "linux", "cpu": "none" }, "sha512-DTNwl6a3CfhGTAOYZ4KtYbdS8b+275LSLqJVJIrPa5/JuIufWWZ/QFvkxp52gpmguN95eujrM68ZG+zVxa8zHA=="], "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.37.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-hZDDU5fgWvDdHFuExN1gBOhCuzo/8TMpidfOR+1cPZJflcEzXdCy1LjnklQdW8/Et9sryOPJAKAQRw8Jq7Tg+A=="], "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.37.0", "", { "os": "linux", "cpu": "x64" }, "sha512-pKivGpgJM5g8dwj0ywBwe/HeVAUSuVVJhUTa/URXjxvoyTT/AxsLTAbkHkDHG7qQxLoW2s3apEIl26uUe08LVQ=="], "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.37.0", "", { "os": "linux", "cpu": "x64" }, "sha512-E2lPrLKE8sQbY/2bEkVTGDEk4/49UYRVWgj90MY8yPjpnGBQ+Xi1Qnr7b7UIWw1NOggdFQFOLZ8+5CzCiz143w=="], "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.37.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Jm7biMazjNzTU4PrQtr7VS8ibeys9Pn29/1bm4ph7CP2kf21950LgN+BaE2mJ1QujnvOc6p54eWWiVvn05SOBg=="], "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.37.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-e3/1SFm1OjefWICB2Ucstg2dxYDkDTZGDYgwufcbsxTHyqQps1UQf33dFEChBNmeSsTOyrjw2JJq0zbG5GF6RA=="], "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.37.0", "", { "os": "win32", "cpu": "x64" }, "sha512-LWbXUBwn/bcLx2sSsqy7pK5o+Nr+VCoRoAohfJ5C/aBio9nfJmGQqHAhU6pwxV/RmyTk5AqdySma7uwWGlmeuA=="], "@tailwindcss/node": ["@tailwindcss/node@4.1.11", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.11" } }, "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q=="], "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.11", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.11", "@tailwindcss/oxide-darwin-arm64": "4.1.11", "@tailwindcss/oxide-darwin-x64": "4.1.11", "@tailwindcss/oxide-freebsd-x64": "4.1.11", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", "@tailwindcss/oxide-linux-x64-musl": "4.1.11", "@tailwindcss/oxide-wasm32-wasi": "4.1.11", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" } }, "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg=="], "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.11", "", { "os": "android", "cpu": "arm64" }, "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg=="], "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ=="], "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw=="], "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.11", "", { "os": "freebsd", "cpu": "x64" }, "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA=="], "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11", "", { "os": "linux", "cpu": "arm" }, "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg=="], "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ=="], "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ=="], "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.11", "", { "os": "linux", "cpu": "x64" }, "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg=="], "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.11", "", { "os": "linux", "cpu": "x64" }, "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q=="], "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.11", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@emnapi/wasi-threads": "^1.0.2", "@napi-rs/wasm-runtime": "^0.2.11", "@tybys/wasm-util": "^0.9.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g=="], "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w=="], "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.11", "", { "os": "win32", "cpu": "x64" }, "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg=="], "@tailwindcss/vite": ["@tailwindcss/vite@4.1.11", "", { "dependencies": { "@tailwindcss/node": "4.1.11", "@tailwindcss/oxide": "4.1.11", "tailwindcss": "4.1.11" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], "@types/babel__generator": ["@types/babel__generator@7.6.8", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw=="], "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], "@types/babel__traverse": ["@types/babel__traverse@7.20.6", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg=="], "@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="], "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/node": ["@types/node@22.16.4", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-PYRhNtZdm2wH/NT2k/oAJ6/f2VD2N2Dag0lGlx2vWgMSJXGNmlce5MiTQzoWAiIJtso30mjnfQCOKVH+kAQC/g=="], "@types/parse-json": ["@types/parse-json@4.0.2", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="], "@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="], "@types/react-dom": ["@types/react-dom@19.1.6", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw=="], "@types/react-relay": ["@types/react-relay@18.2.1", "", { "dependencies": { "@types/react": "*", "@types/relay-runtime": "*" } }, "sha512-KgmFapsxAylhxcFfaAv5GZZJhTHnDvV8IDZVsUm5afpJUvgZC1Y68ssfOGsFfiFY/2EhxHM/YPfpdKbfmF3Ecg=="], "@types/relay-runtime": ["@types/relay-runtime@18.2.5", "", {}, "sha512-2ukE6xQMIpa3kG8F33kXZMcGl/VJdDjNW3eKjQVe1AZQaYnIpNX8vXpcRD6pc+No2rOwVFLbQ6thaPSXPs8ouQ=="], "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.37.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.37.0", "@typescript-eslint/type-utils": "8.37.0", "@typescript-eslint/utils": "8.37.0", "@typescript-eslint/visitor-keys": "8.37.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.37.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-jsuVWeIkb6ggzB+wPCsR4e6loj+rM72ohW6IBn2C+5NCvfUVY8s33iFPySSVXqtm5Hu29Ne/9bnA0JmyLmgenA=="], "@typescript-eslint/parser": ["@typescript-eslint/parser@8.37.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.37.0", "@typescript-eslint/types": "8.37.0", "@typescript-eslint/typescript-estree": "8.37.0", "@typescript-eslint/visitor-keys": "8.37.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA=="], "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.37.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.37.0", "@typescript-eslint/types": "^8.37.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-BIUXYsbkl5A1aJDdYJCBAo8rCEbAvdquQ8AnLb6z5Lp1u3x5PNgSSx9A/zqYc++Xnr/0DVpls8iQ2cJs/izTXA=="], "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.37.0", "", { "dependencies": { "@typescript-eslint/types": "8.37.0", "@typescript-eslint/visitor-keys": "8.37.0" } }, "sha512-0vGq0yiU1gbjKob2q691ybTg9JX6ShiVXAAfm2jGf3q0hdP6/BruaFjL/ManAR/lj05AvYCH+5bbVo0VtzmjOA=="], "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.37.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-1/YHvAVTimMM9mmlPvTec9NP4bobA1RkDbMydxG8omqwJJLEW/Iy2C4adsAESIXU3WGLXFHSZUU+C9EoFWl4Zg=="], "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.37.0", "", { "dependencies": { "@typescript-eslint/types": "8.37.0", "@typescript-eslint/typescript-estree": "8.37.0", "@typescript-eslint/utils": "8.37.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-SPkXWIkVZxhgwSwVq9rqj/4VFo7MnWwVaRNznfQDc/xPYHjXnPfLWn+4L6FF1cAz6e7dsqBeMawgl7QjUMj4Ow=="], "@typescript-eslint/types": ["@typescript-eslint/types@8.37.0", "", {}, "sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ=="], "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.37.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.37.0", "@typescript-eslint/tsconfig-utils": "8.37.0", "@typescript-eslint/types": "8.37.0", "@typescript-eslint/visitor-keys": "8.37.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-zuWDMDuzMRbQOM+bHyU4/slw27bAUEcKSKKs3hcv2aNnc/tvE/h7w60dwVw8vnal2Pub6RT1T7BI8tFZ1fE+yg=="], "@typescript-eslint/utils": ["@typescript-eslint/utils@8.37.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.37.0", "@typescript-eslint/types": "8.37.0", "@typescript-eslint/typescript-estree": "8.37.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-TSFvkIW6gGjN2p6zbXo20FzCABbyUAuq6tBvNRGsKdsSQ6a7rnV6ADfZ7f4iI3lIiXc4F4WWvtUfDw9CJ9pO5A=="], "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.37.0", "", { "dependencies": { "@typescript-eslint/types": "8.37.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-YzfhzcTnZVPiLfP/oeKtDp2evwvHLMe0LOy7oe+hb9KKIumLNohYS9Hgp1ifwpu42YWxhZE8yieggz6JpqO/1w=="], "@vitejs/plugin-react": ["@vitejs/plugin-react@4.6.0", "", { "dependencies": { "@babel/core": "^7.27.4", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.19", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" } }, "sha512-5Kgff+m8e2PB+9j51eGHEpn5kUzRKH2Ry0qGoe8ItJg7pqnkPrYPkDQZGgGmTa0EGarHrkjLvOdU3b1fzI8otQ=="], "@wry/caches": ["@wry/caches@1.0.1", "", { "dependencies": { "tslib": "^2.3.0" } }, "sha512-bXuaUNLVVkD20wcGBWRyo7j9N3TxePEWFZj2Y+r9OoUzfqmavM84+mFykRicNsBqatba5JLay1t48wxaXaWnlA=="], "@wry/context": ["@wry/context@0.7.4", "", { "dependencies": { "tslib": "^2.3.0" } }, "sha512-jmT7Sb4ZQWI5iyu3lobQxICu2nC/vbUhP0vIdd6tHC9PTfenmRmuIFqktc6GH9cgi+ZHnsLWPvfSvc4DrYmKiQ=="], "@wry/equality": ["@wry/equality@0.5.7", "", { "dependencies": { "tslib": "^2.3.0" } }, "sha512-BRFORjsTuQv5gxcXsuDXx6oGRhuVsEGwZy6LOzRRfgu+eSfxbhUQ9L9YtSEIuIjY/o7g3iWFjrc5eSY1GXP2Dw=="], "@wry/trie": ["@wry/trie@0.5.0", "", { "dependencies": { "tslib": "^2.3.0" } }, "sha512-FNoYzHawTMk/6KMQoEG5O4PuioX19UbwdQKF44yw0nLfOypfQdjtfZzo/UIJWAJ23sNIFbD1Ug9lbaDGMwbqQA=="], "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], "asap": ["asap@2.0.6", "", {}, "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="], "babel-plugin-macros": ["babel-plugin-macros@2.8.0", "", { "dependencies": { "@babel/runtime": "^7.7.2", "cosmiconfig": "^6.0.0", "resolve": "^1.12.0" } }, "sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg=="], "babel-plugin-relay": ["babel-plugin-relay@18.2.0", "", { "dependencies": { "babel-plugin-macros": "^2.0.0", "cosmiconfig": "^5.0.5", "graphql": "15.3.0" } }, "sha512-icTICGxKvlmhOvw+MESI/xBsY3J+vsNzgyq6/FL2c/hMnsYJ10IIM6Nps4M3cdUxQfS9CCUTXsbz2/kfmYLzhQ=="], "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], "browserslist": ["browserslist@4.24.4", "", { "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" } }, "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A=="], "caller-callsite": ["caller-callsite@2.0.0", "", { "dependencies": { "callsites": "^2.0.0" } }, "sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ=="], "caller-path": ["caller-path@2.0.0", "", { "dependencies": { "caller-callsite": "^2.0.0" } }, "sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A=="], "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], "caniuse-lite": ["caniuse-lite@1.0.30001707", "", {}, "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw=="], "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], "cosmiconfig": ["cosmiconfig@5.2.1", "", { "dependencies": { "import-fresh": "^2.0.0", "is-directory": "^0.3.1", "js-yaml": "^3.13.1", "parse-json": "^4.0.0" } }, "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA=="], "cross-fetch": ["cross-fetch@3.2.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], "detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="], "electron-to-chromium": ["electron-to-chromium@1.5.123", "", {}, "sha512-refir3NlutEZqlKaBLK0tzlVLe5P2wDKS7UQt/3SpibizgsRAPOsqQC3ffw1nlv3ze5gjRQZYHoPymgVZkplFA=="], "enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="], "error-ex": ["error-ex@1.3.2", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g=="], "esbuild": ["esbuild@0.25.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.1", "@esbuild/android-arm": "0.25.1", "@esbuild/android-arm64": "0.25.1", "@esbuild/android-x64": "0.25.1", "@esbuild/darwin-arm64": "0.25.1", "@esbuild/darwin-x64": "0.25.1", "@esbuild/freebsd-arm64": "0.25.1", "@esbuild/freebsd-x64": "0.25.1", "@esbuild/linux-arm": "0.25.1", "@esbuild/linux-arm64": "0.25.1", "@esbuild/linux-ia32": "0.25.1", "@esbuild/linux-loong64": "0.25.1", "@esbuild/linux-mips64el": "0.25.1", "@esbuild/linux-ppc64": "0.25.1", "@esbuild/linux-riscv64": "0.25.1", "@esbuild/linux-s390x": "0.25.1", "@esbuild/linux-x64": "0.25.1", "@esbuild/netbsd-arm64": "0.25.1", "@esbuild/netbsd-x64": "0.25.1", "@esbuild/openbsd-arm64": "0.25.1", "@esbuild/openbsd-x64": "0.25.1", "@esbuild/sunos-x64": "0.25.1", "@esbuild/win32-arm64": "0.25.1", "@esbuild/win32-ia32": "0.25.1", "@esbuild/win32-x64": "0.25.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], "eslint": ["eslint@9.31.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.0", "@eslint/core": "^0.15.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.31.0", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ=="], "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="], "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.20", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA=="], "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="], "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], "fbjs": ["fbjs@3.0.5", "", { "dependencies": { "cross-fetch": "^3.1.5", "fbjs-css-vars": "^1.0.0", "loose-envify": "^1.0.0", "object-assign": "^4.1.0", "promise": "^7.1.1", "setimmediate": "^1.0.5", "ua-parser-js": "^1.0.35" } }, "sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg=="], "fbjs-css-vars": ["fbjs-css-vars@1.0.2", "", {}, "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ=="], "fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="], "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], "globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="], "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], "graphql": ["graphql@16.11.0", "", {}, "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw=="], "graphql-http": ["graphql-http@1.22.4", "", { "peerDependencies": { "graphql": ">=0.11 <=16" } }, "sha512-OC3ucK988teMf+Ak/O+ZJ0N2ukcgrEurypp8ePyJFWq83VzwRAmHxxr+XxrMpxO/FIwI4a7m/Fzv3tWGJv0wPA=="], "graphql-tag": ["graphql-tag@2.12.6", "", { "dependencies": { "tslib": "^2.1.0" }, "peerDependencies": { "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, "sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg=="], "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], "hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="], "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "import-fresh": ["import-fresh@2.0.0", "", { "dependencies": { "caller-path": "^2.0.0", "resolve-from": "^3.0.0" } }, "sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg=="], "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], "invariant": ["invariant@2.2.4", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA=="], "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], "is-directory": ["is-directory@0.3.1", "", {}, "sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw=="], "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="], "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], "json-parse-better-errors": ["json-parse-better-errors@1.0.2", "", {}, "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw=="], "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], "lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="], "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="], "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="], "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig=="], "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.1", "", { "os": "linux", "cpu": "arm" }, "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q=="], "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw=="], "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ=="], "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw=="], "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ=="], "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA=="], "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="], "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], "lucide-react": ["lucide-react@0.484.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-oZy8coK9kZzvqhSgfbGkPtTgyjpBvs3ukLgDPv14dSOZtBtboryWF5o8i3qen7QbGg7JhiJBz5mK1p8YoMZTLQ=="], "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], "minizlib": ["minizlib@3.0.2", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA=="], "mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="], "nullthrows": ["nullthrows@1.1.1", "", {}, "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], "optimism": ["optimism@0.18.1", "", { "dependencies": { "@wry/caches": "^1.0.0", "@wry/context": "^0.7.0", "@wry/trie": "^0.5.0", "tslib": "^2.3.0" } }, "sha512-mLXNwWPa9dgFyDqkNi54sjDyNJ9/fTI6WGBLgnXku1vdKY/jovHfZT5r+aiVeFFLOz+foPNOm5YJ4mqgld2GBQ=="], "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], "parse-json": ["parse-json@4.0.0", "", { "dependencies": { "error-ex": "^1.3.1", "json-parse-better-errors": "^1.0.1" } }, "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw=="], "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], "playwright": ["playwright@1.54.1", "", { "dependencies": { "playwright-core": "1.54.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g=="], "playwright-core": ["playwright-core@1.54.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA=="], "postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="], "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], "promise": ["promise@7.3.1", "", { "dependencies": { "asap": "~2.0.3" } }, "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg=="], "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], "react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="], "react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="], "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], "react-relay": ["react-relay@18.2.0", "", { "dependencies": { "@babel/runtime": "^7.25.0", "fbjs": "^3.0.2", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "relay-runtime": "18.2.0" }, "peerDependencies": { "react": "^16.9.0 || ^17 || ^18" } }, "sha512-/yHlv4pj3IQQPtDzYLfVk6P4wrOSvYPu/5ySPcRp4xV3pn9xoVOHVczSLcZkRVf72lJu8CIjoT8rn3roaGoFyQ=="], "regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="], "rehackt": ["rehackt@0.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "*" }, "optionalPeers": ["@types/react", "react"] }, "sha512-7kRDOuLHB87D/JESKxQoRwv4DzbIdwkAGQ7p6QKGdVlY1IZheUnVhlk/4UZlNUVxdAXpyxikE3URsG067ybVzw=="], "relay-compiler": ["relay-compiler@18.2.0", "", { "bin": { "relay-compiler": "cli.js" } }, "sha512-P3o5/Gv/oLC9hckUaz/a+KvDgbFERpjtz5lgsJSIQILg9paaF3k1yaX9qSxErGuU4icZvjoK5G82a/bfPgGZpA=="], "relay-runtime": ["relay-runtime@18.2.0", "", { "dependencies": { "@babel/runtime": "^7.25.0", "fbjs": "^3.0.2", "invariant": "^2.2.4" } }, "sha512-r4FYWlx1dPwFW+KQnuF1ugwVKR4toIFduCGheEf2paRc6nEEcCIgd2p2Jx2gRF+4niO3mCbRZvqdCoKaznUDzg=="], "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], "resolve-from": ["resolve-from@3.0.0", "", {}, "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw=="], "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], "rollup": ["rollup@4.37.0", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.37.0", "@rollup/rollup-android-arm64": "4.37.0", "@rollup/rollup-darwin-arm64": "4.37.0", "@rollup/rollup-darwin-x64": "4.37.0", "@rollup/rollup-freebsd-arm64": "4.37.0", "@rollup/rollup-freebsd-x64": "4.37.0", "@rollup/rollup-linux-arm-gnueabihf": "4.37.0", "@rollup/rollup-linux-arm-musleabihf": "4.37.0", "@rollup/rollup-linux-arm64-gnu": "4.37.0", "@rollup/rollup-linux-arm64-musl": "4.37.0", "@rollup/rollup-linux-loongarch64-gnu": "4.37.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.37.0", "@rollup/rollup-linux-riscv64-gnu": "4.37.0", "@rollup/rollup-linux-riscv64-musl": "4.37.0", "@rollup/rollup-linux-s390x-gnu": "4.37.0", "@rollup/rollup-linux-x64-gnu": "4.37.0", "@rollup/rollup-linux-x64-musl": "4.37.0", "@rollup/rollup-win32-arm64-msvc": "4.37.0", "@rollup/rollup-win32-ia32-msvc": "4.37.0", "@rollup/rollup-win32-x64-msvc": "4.37.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-iAtQy/L4QFU+rTJ1YUjXqJOJzuwEghqWzCEYD2FEghT7Gsy1VdABntrO4CLopA5IkflTyqNiLNwPcOJ3S7UKLg=="], "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="], "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], "symbol-observable": ["symbol-observable@4.0.0", "", {}, "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ=="], "tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="], "tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="], "tailwindcss-animate": ["tailwindcss-animate@1.0.7", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="], "tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="], "tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="], "tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], "ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="], "ts-invariant": ["ts-invariant@0.10.3", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "tw-animate-css": ["tw-animate-css@1.3.5", "", {}, "sha512-t3u+0YNoloIhj1mMXs779P6MO9q3p3mvGn4k1n3nJPqJw/glZcuijG2qTSN4z4mgNRfW5ZC3aXJFLwDtiipZXA=="], "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], "typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], "typescript-eslint": ["typescript-eslint@8.37.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.37.0", "@typescript-eslint/parser": "8.37.0", "@typescript-eslint/typescript-estree": "8.37.0", "@typescript-eslint/utils": "8.37.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-TnbEjzkE9EmcO0Q2zM+GE8NQLItNAJpMmED1BdgoBMYNdqMhzlbqfdSwiRlAzEK2pA9UzVW0gzaaIzXWg2BjfA=="], "ua-parser-js": ["ua-parser-js@1.0.40", "", { "bin": { "ua-parser-js": "script/cli.js" } }, "sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew=="], "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="], "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], "vite": ["vite@6.3.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="], "vite-plugin-relay": ["vite-plugin-relay@2.1.0", "", { "dependencies": { "@babel/core": "^7.23.5" }, "peerDependencies": { "babel-plugin-relay": ">=14.1.0", "vite": ">=2.0.0" } }, "sha512-k7tQFeJQlJy0S6OrQxuk5WrauHouGfRgFFCos0PatYiUY2gnwPZPi30KPuUtvdrzTPPTgZVHj51xJAp/3oMRiQ=="], "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], "yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="], "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], "zen-observable": ["zen-observable@0.8.15", "", {}, "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ=="], "zen-observable-ts": ["zen-observable-ts@1.2.5", "", { "dependencies": { "zen-observable": "0.8.15" } }, "sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg=="], "@babel/generator/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.12", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg=="], "@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@eslint/eslintrc/espree": ["espree@10.3.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.0" } }, "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg=="], "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], "@eslint/eslintrc/import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], "@eslint/eslintrc/js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.4", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-A9CnAbC6ARNMKcIcrQwq6HeHCjpcBZ5wSx4U01WXCqEKlrzB9F9315WDNHkrs2xbx7YjjSxbUYxuN6EQzpcY2g=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.4", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-8K5IFFsQqF9wQNJptGbS6FNKgUTsSRYnTqNCG1vPP8jFdjSv18n2mQfJpkt2Oibo9iBEzcDnDxNwKTzC7svlJw=="], "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" }, "bundled": true }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="], "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "@types/babel__core/@babel/parser": ["@babel/parser@7.27.0", "", { "dependencies": { "@babel/types": "^7.27.0" }, "bin": "./bin/babel-parser.js" }, "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg=="], "@types/babel__core/@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="], "@types/babel__generator/@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="], "@types/babel__template/@babel/parser": ["@babel/parser@7.27.0", "", { "dependencies": { "@babel/types": "^7.27.0" }, "bin": "./bin/babel-parser.js" }, "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg=="], "@types/babel__template/@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="], "@types/babel__traverse/@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="], "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], "@typescript-eslint/utils/@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="], "babel-plugin-macros/cosmiconfig": ["cosmiconfig@6.0.0", "", { "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.1.0", "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.7.2" } }, "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg=="], "babel-plugin-relay/graphql": ["graphql@15.3.0", "", {}, "sha512-GTCJtzJmkFLWRfFJuoo9RWWa/FfamUHgiFosxi/X1Ani4AVWbeyBenZTNX6dM+7WSbbFfTo/25eh0LLkwHMw2w=="], "caller-callsite/callsites": ["callsites@2.0.0", "", {}, "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ=="], "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], "rollup/@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="], "vite-plugin-relay/@babel/core": ["@babel/core@7.26.10", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.26.10", "@babel/helper-compilation-targets": "^7.26.5", "@babel/helper-module-transforms": "^7.26.0", "@babel/helpers": "^7.26.10", "@babel/parser": "^7.26.10", "@babel/template": "^7.26.9", "@babel/traverse": "^7.26.10", "@babel/types": "^7.26.10", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ=="], "@eslint/eslintrc/espree/acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="], "@eslint/eslintrc/espree/eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="], "@eslint/eslintrc/import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], "@eslint/eslintrc/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="], "@types/babel__core/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="], "@types/babel__core/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="], "@types/babel__generator/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="], "@types/babel__generator/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="], "@types/babel__template/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="], "@types/babel__template/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="], "@types/babel__traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="], "@types/babel__traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], "@typescript-eslint/utils/@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "babel-plugin-macros/cosmiconfig/import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], "babel-plugin-macros/cosmiconfig/parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], "vite-plugin-relay/@babel/core/@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="], "vite-plugin-relay/@babel/core/@babel/generator": ["@babel/generator@7.27.0", "", { "dependencies": { "@babel/parser": "^7.27.0", "@babel/types": "^7.27.0", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw=="], "vite-plugin-relay/@babel/core/@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.0", "", { "dependencies": { "@babel/compat-data": "^7.26.8", "@babel/helper-validator-option": "^7.25.9", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA=="], "vite-plugin-relay/@babel/core/@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.26.0", "", { "dependencies": { "@babel/helper-module-imports": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9", "@babel/traverse": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw=="], "vite-plugin-relay/@babel/core/@babel/helpers": ["@babel/helpers@7.27.0", "", { "dependencies": { "@babel/template": "^7.27.0", "@babel/types": "^7.27.0" } }, "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg=="], "vite-plugin-relay/@babel/core/@babel/parser": ["@babel/parser@7.27.0", "", { "dependencies": { "@babel/types": "^7.27.0" }, "bin": "./bin/babel-parser.js" }, "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg=="], "vite-plugin-relay/@babel/core/@babel/template": ["@babel/template@7.27.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/parser": "^7.27.0", "@babel/types": "^7.27.0" } }, "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA=="], "vite-plugin-relay/@babel/core/@babel/traverse": ["@babel/traverse@7.27.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.27.0", "@babel/parser": "^7.27.0", "@babel/template": "^7.27.0", "@babel/types": "^7.27.0", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA=="], "vite-plugin-relay/@babel/core/@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="], "babel-plugin-macros/cosmiconfig/import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], "babel-plugin-macros/cosmiconfig/parse-json/@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="], "vite-plugin-relay/@babel/core/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="], "vite-plugin-relay/@babel/core/@babel/helper-compilation-targets/@babel/compat-data": ["@babel/compat-data@7.26.8", "", {}, "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ=="], "vite-plugin-relay/@babel/core/@babel/helper-compilation-targets/@babel/helper-validator-option": ["@babel/helper-validator-option@7.25.9", "", {}, "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw=="], "vite-plugin-relay/@babel/core/@babel/helper-module-transforms/@babel/helper-module-imports": ["@babel/helper-module-imports@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw=="], "vite-plugin-relay/@babel/core/@babel/helper-module-transforms/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="], "vite-plugin-relay/@babel/core/@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="], "vite-plugin-relay/@babel/core/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="], "vite-plugin-relay/@babel/core/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="], "babel-plugin-macros/cosmiconfig/parse-json/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="], } } strawberry-graphql-0.287.0/e2e/components.json000066400000000000000000000006521511033167500212700ustar00rootroot00000000000000{ "$schema": "https://ui.shadcn.com/schema.json", "style": "new-york", "rsc": false, "tsx": true, "tailwind": { "config": "", "css": "src/index.css", "baseColor": "neutral", "cssVariables": true, "prefix": "" }, "aliases": { "components": "@/components", "utils": "@/lib/utils", "ui": "@/components/ui", "lib": "@/lib", "hooks": "@/hooks" }, "iconLibrary": "lucide" } strawberry-graphql-0.287.0/e2e/eslint.config.js000066400000000000000000000013361511033167500213100ustar00rootroot00000000000000import js from '@eslint/js' import globals from 'globals' import reactHooks from 'eslint-plugin-react-hooks' import reactRefresh from 'eslint-plugin-react-refresh' import tseslint from 'typescript-eslint' export default tseslint.config( { ignores: ['dist'] }, { extends: [js.configs.recommended, ...tseslint.configs.recommended], files: ['**/*.{ts,tsx}'], languageOptions: { ecmaVersion: 2020, globals: globals.browser, }, plugins: { 'react-hooks': reactHooks, 'react-refresh': reactRefresh, }, rules: { ...reactHooks.configs.recommended.rules, 'react-refresh/only-export-components': [ 'warn', { allowConstantExport: true }, ], }, }, ) strawberry-graphql-0.287.0/e2e/index.html000066400000000000000000000011421511033167500202000ustar00rootroot00000000000000 🍓 📊
strawberry-graphql-0.287.0/e2e/package.json000066400000000000000000000026731511033167500205030ustar00rootroot00000000000000{ "name": "e2e", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "relay-compiler --validate && tsc -b && vite build", "lint": "eslint .", "preview": "vite preview", "relay": "relay-compiler", "test": "@playwright/test" }, "dependencies": { "@apollo/client": "^3.13.8", "@radix-ui/react-slot": "^1.2.3", "@tailwindcss/vite": "^4.1.11", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "graphql": "^16.11.0", "graphql-http": "^1.22.4", "lucide-react": "^0.484.0", "react": "^19.1.0", "react-dom": "^19.1.0", "react-relay": "^18.2.0", "relay-runtime": "^18.2.0", "tailwind-merge": "^3.3.1", "tailwindcss": "^4.1.11", "tailwindcss-animate": "^1.0.7", "tw-animate-css": "^1.3.5" }, "devDependencies": { "@eslint/js": "^9.31.0", "@playwright/test": "^1.54.1", "@types/node": "^22.16.4", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@types/react-relay": "^18.2.1", "@types/relay-runtime": "^18.2.5", "@vitejs/plugin-react": "^4.6.0", "babel-plugin-relay": "^18.2.0", "eslint": "^9.31.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", "globals": "^15.15.0", "relay-compiler": "^18.2.0", "typescript": "~5.7.3", "typescript-eslint": "^8.37.0", "vite": "^6.3.5", "vite-plugin-relay": "^2.1.0" } } strawberry-graphql-0.287.0/e2e/playwright.config.ts000066400000000000000000000010421511033167500222100ustar00rootroot00000000000000import { defineConfig, devices } from "@playwright/test"; export default defineConfig({ testDir: "./src/tests", fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 1 : undefined, reporter: "html", use: { baseURL: "http://localhost:5173", trace: "on-first-retry", }, projects: [ { name: "chromium", use: { ...devices["Desktop Chrome"] }, }, ], webServer: { command: "bun run dev", url: "http://localhost:5173", reuseExistingServer: !process.env.CI, }, }); strawberry-graphql-0.287.0/e2e/relay.config.json000066400000000000000000000003161511033167500214600ustar00rootroot00000000000000{ "src": "./src", "language": "typescript", "schema": "./src/schema.graphql", "exclude": [ "**/node_modules/**", "**/__mocks__/**", "**/__generated__/**" ], "eagerEsModules": true } strawberry-graphql-0.287.0/e2e/src/000077500000000000000000000000001511033167500167745ustar00rootroot00000000000000strawberry-graphql-0.287.0/e2e/src/App.tsx000066400000000000000000000015351511033167500202600ustar00rootroot00000000000000import { ApolloClient, InMemoryCache, ApolloProvider } from "@apollo/client"; import ApolloTests from "@/components/apollo-tests"; import { RelayEnvironmentProvider } from "react-relay"; import RelayTests from "./components/relay-tests"; import { RelayEnvironment } from "./RelayEnvironment"; const client = new ApolloClient({ uri: "http://localhost:8000/graphql", cache: new InMemoryCache(), }); function App() { return (
🍓 End to End Tests
); } export default App; strawberry-graphql-0.287.0/e2e/src/RelayEnvironment.ts000066400000000000000000000042371511033167500226530ustar00rootroot00000000000000import { serializeFetchParameter } from "@apollo/client"; import type { RequestParameters } from "relay-runtime"; import { Environment, Network, Observable, RecordSource, Store, } from "relay-runtime"; import type { Variables } from "relay-runtime"; import { maybe } from "@apollo/client/utilities"; import { handleError, readMultipartBody, } from "@apollo/client/link/http/parseAndCheckHttpResponse"; const uri = "http://localhost:8000/graphql"; const backupFetch = maybe(() => fetch); function fetchQuery(operation: RequestParameters, variables: Variables) { const body = { operationName: operation.name, variables, query: operation.text || "", }; const options: { method: string; // biome-ignore lint/suspicious/noExplicitAny: :) headers: Record; body?: string; } = { method: "POST", headers: { "Content-Type": "application/json", accept: "multipart/mixed;deferSpec=20220824,application/json", }, }; return Observable.create((sink) => { try { options.body = serializeFetchParameter(body, "Payload"); } catch (parseError) { sink.error(parseError as Error); } const currentFetch = maybe(() => fetch) || backupFetch; // biome-ignore lint/suspicious/noExplicitAny: :) const observerNext = (data: any) => { console.log("data", data); if ("incremental" in data) { for (const item of data.incremental) { sink.next(item); } } else if ("data" in data) { sink.next(data); } }; // biome-ignore lint/style/noNonNullAssertion: :) currentFetch!(uri, options) .then(async (response) => { console.log("response", response); const ctype = response.headers?.get("content-type"); if (ctype !== null && /^multipart\/mixed/i.test(ctype)) { const result = readMultipartBody(response, observerNext); return result; } const json = await response.json(); console.log("json", json); observerNext(json); }) .then(() => { sink.complete(); }) .catch((err: any) => { handleError(err, sink); }); }); } const network = Network.create(fetchQuery); export const RelayEnvironment = new Environment({ network, store: new Store(new RecordSource()), }); strawberry-graphql-0.287.0/e2e/src/components/000077500000000000000000000000001511033167500211615ustar00rootroot00000000000000strawberry-graphql-0.287.0/e2e/src/components/__generated__/000077500000000000000000000000001511033167500237135ustar00rootroot00000000000000strawberry-graphql-0.287.0/e2e/src/components/__generated__/relayTestsBlogPostQuery.graphql.ts000066400000000000000000000074621511033167500325700ustar00rootroot00000000000000/** * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ /* tslint:disable */ /* eslint-disable */ // @ts-nocheck import { ConcreteRequest } from 'relay-runtime'; import { FragmentRefs } from "relay-runtime"; export type relayTestsBlogPostQuery$variables = { id: string; shouldDefer?: boolean | null | undefined; }; export type relayTestsBlogPostQuery$data = { readonly blogPost: { readonly content: string; readonly title: string; readonly " $fragmentSpreads": FragmentRefs<"relayTestsCommentsFragment">; }; }; export type relayTestsBlogPostQuery = { response: relayTestsBlogPostQuery$data; variables: relayTestsBlogPostQuery$variables; }; const node: ConcreteRequest = (function(){ var v0 = [ { "defaultValue": null, "kind": "LocalArgument", "name": "id" }, { "defaultValue": false, "kind": "LocalArgument", "name": "shouldDefer" } ], v1 = [ { "kind": "Variable", "name": "id", "variableName": "id" } ], v2 = { "alias": null, "args": null, "kind": "ScalarField", "name": "title", "storageKey": null }, v3 = { "alias": null, "args": null, "kind": "ScalarField", "name": "content", "storageKey": null }, v4 = { "alias": null, "args": null, "kind": "ScalarField", "name": "id", "storageKey": null }; return { "fragment": { "argumentDefinitions": (v0/*: any*/), "kind": "Fragment", "metadata": null, "name": "relayTestsBlogPostQuery", "selections": [ { "alias": null, "args": (v1/*: any*/), "concreteType": "BlogPost", "kind": "LinkedField", "name": "blogPost", "plural": false, "selections": [ (v2/*: any*/), (v3/*: any*/), { "kind": "Defer", "selections": [ { "args": null, "kind": "FragmentSpread", "name": "relayTestsCommentsFragment" } ] } ], "storageKey": null } ], "type": "Query", "abstractKey": null }, "kind": "Request", "operation": { "argumentDefinitions": (v0/*: any*/), "kind": "Operation", "name": "relayTestsBlogPostQuery", "selections": [ { "alias": null, "args": (v1/*: any*/), "concreteType": "BlogPost", "kind": "LinkedField", "name": "blogPost", "plural": false, "selections": [ (v2/*: any*/), (v3/*: any*/), { "if": "shouldDefer", "kind": "Defer", "label": "relayTestsBlogPostQuery$defer$relayTestsCommentsFragment", "selections": [ { "alias": null, "args": null, "concreteType": "Comment", "kind": "LinkedField", "name": "comments", "plural": true, "selections": [ (v4/*: any*/), (v3/*: any*/) ], "storageKey": null } ] }, (v4/*: any*/) ], "storageKey": null } ] }, "params": { "cacheID": "353f101f55e146c2c22598ddd0818c9e", "id": null, "metadata": {}, "name": "relayTestsBlogPostQuery", "operationKind": "query", "text": "query relayTestsBlogPostQuery(\n $id: ID!\n $shouldDefer: Boolean = false\n) {\n blogPost(id: $id) {\n title\n content\n ...relayTestsCommentsFragment @defer(label: \"relayTestsBlogPostQuery$defer$relayTestsCommentsFragment\", if: $shouldDefer)\n id\n }\n}\n\nfragment relayTestsCommentsFragment on BlogPost {\n comments {\n id\n content\n }\n}\n" } }; })(); (node as any).hash = "f04ba754cec2dd44c29bb48d17e151f4"; export default node; strawberry-graphql-0.287.0/e2e/src/components/__generated__/relayTestsCommentsFragment.graphql.ts000066400000000000000000000027051511033167500332550ustar00rootroot00000000000000/** * @generated SignedSource<<8a2413eb8c2bdec10f525eccc282227e>> * @lightSyntaxTransform * @nogrep */ /* tslint:disable */ /* eslint-disable */ // @ts-nocheck import { ReaderFragment } from 'relay-runtime'; import { FragmentRefs } from "relay-runtime"; export type relayTestsCommentsFragment$data = { readonly comments: ReadonlyArray<{ readonly content: string; readonly id: string; }>; readonly " $fragmentType": "relayTestsCommentsFragment"; }; export type relayTestsCommentsFragment$key = { readonly " $data"?: relayTestsCommentsFragment$data; readonly " $fragmentSpreads": FragmentRefs<"relayTestsCommentsFragment">; }; const node: ReaderFragment = { "argumentDefinitions": [], "kind": "Fragment", "metadata": null, "name": "relayTestsCommentsFragment", "selections": [ { "alias": null, "args": null, "concreteType": "Comment", "kind": "LinkedField", "name": "comments", "plural": true, "selections": [ { "alias": null, "args": null, "kind": "ScalarField", "name": "id", "storageKey": null }, { "alias": null, "args": null, "kind": "ScalarField", "name": "content", "storageKey": null } ], "storageKey": null } ], "type": "BlogPost", "abstractKey": null }; (node as any).hash = "2bc5eb5c1b00234ad057cf1e9f9a06bc"; export default node; strawberry-graphql-0.287.0/e2e/src/components/__generated__/relayTestsHelloQuery.graphql.ts000066400000000000000000000031401511033167500320670ustar00rootroot00000000000000/** * @generated SignedSource<<9877a444a31075bba9e891f8eff57350>> * @lightSyntaxTransform * @nogrep */ /* tslint:disable */ /* eslint-disable */ // @ts-nocheck import { ConcreteRequest } from 'relay-runtime'; export type relayTestsHelloQuery$variables = { delay?: number | null | undefined; }; export type relayTestsHelloQuery$data = { readonly hello: string; }; export type relayTestsHelloQuery = { response: relayTestsHelloQuery$data; variables: relayTestsHelloQuery$variables; }; const node: ConcreteRequest = (function(){ var v0 = [ { "defaultValue": 0, "kind": "LocalArgument", "name": "delay" } ], v1 = [ { "alias": null, "args": [ { "kind": "Variable", "name": "delay", "variableName": "delay" } ], "kind": "ScalarField", "name": "hello", "storageKey": null } ]; return { "fragment": { "argumentDefinitions": (v0/*: any*/), "kind": "Fragment", "metadata": null, "name": "relayTestsHelloQuery", "selections": (v1/*: any*/), "type": "Query", "abstractKey": null }, "kind": "Request", "operation": { "argumentDefinitions": (v0/*: any*/), "kind": "Operation", "name": "relayTestsHelloQuery", "selections": (v1/*: any*/) }, "params": { "cacheID": "32f886d6d7f0ca3b24c6b8c0897c33e5", "id": null, "metadata": {}, "name": "relayTestsHelloQuery", "operationKind": "query", "text": "query relayTestsHelloQuery(\n $delay: Float = 0\n) {\n hello(delay: $delay)\n}\n" } }; })(); (node as any).hash = "da986b16812095417497d884d89a6820"; export default node; strawberry-graphql-0.287.0/e2e/src/components/apollo-tests.tsx000066400000000000000000000044761511033167500243620ustar00rootroot00000000000000import { gql, useQuery } from "@apollo/client"; import { Button } from "@/components/ui/button"; import { useState } from "react"; const HELLO_QUERY = gql` query GetHello($delay: Float) { hello(delay: $delay) } `; const BLOG_POST_QUERY = gql` query GetBlogPost($id: ID!, $shouldDefer: Boolean) { blogPost(id: $id) { title content ... CommentsFragment @defer(if: $shouldDefer) } } fragment CommentsFragment on BlogPost { comments { id content } } `; interface ApolloQueryWrapperProps { query: typeof HELLO_QUERY; // biome-ignore lint/suspicious/noExplicitAny: typing this would be a pain variables?: any; buttonText?: string; testId?: string; } function ApolloQueryWrapper({ query, variables, buttonText = "Run Query", testId, }: ApolloQueryWrapperProps) { const [shouldRun, setShouldRun] = useState(false); const { data, loading, error } = useQuery(query, { variables, skip: !shouldRun, fetchPolicy: "network-only", }); if (!shouldRun) { return ( ); } if (loading) return

Loading...

; if (error) return

Error: {error.message}

; return (
{JSON.stringify(data, null, 2)}
); } function ApolloTests() { return (

Apollo Tests

# Basic Query

# Hello With Delay

# Blog Post

# Blog Post With Defer

); } export default ApolloTests; strawberry-graphql-0.287.0/e2e/src/components/relay-tests.tsx000066400000000000000000000117071511033167500242030ustar00rootroot00000000000000import { graphql, useLazyLoadQuery, useFragment } from "react-relay"; import { Button } from "@/components/ui/button"; import { Suspense, useState, Component } from "react"; const HELLO_QUERY = graphql` query relayTestsHelloQuery($delay: Float = 0) { hello(delay: $delay) } `; const COMMENTS_FRAGMENT = graphql` fragment relayTestsCommentsFragment on BlogPost { comments { id content } } `; const BLOG_POST_QUERY = graphql` query relayTestsBlogPostQuery($id: ID!, $shouldDefer: Boolean = false) { blogPost(id: $id) { title content ...relayTestsCommentsFragment @defer(if: $shouldDefer) } } `; interface RelayQueryWrapperProps { query: typeof HELLO_QUERY; // biome-ignore lint/suspicious/noExplicitAny: typing this would be a pain variables?: any; buttonText?: string; fragment?: typeof COMMENTS_FRAGMENT; } const filterData = (obj: unknown): unknown => { if (typeof obj !== "object" || obj === null) return obj; if (Array.isArray(obj)) return obj.map(filterData); return Object.fromEntries( Object.entries(obj as Record) .filter(([key]) => !key.startsWith("__")) .map(([key, value]) => [key, filterData(value)]), ); }; // Add this type to help with TypeScript type BlogPostQuery = { blogPost?: { title: string; content: string; }; }; function RelayFragmentWrapper({ fragment, data, testId, }: { fragment: typeof COMMENTS_FRAGMENT; data: any; testId?: string; }) { const fragmentData = useFragment(fragment, data); return (
{JSON.stringify(fragmentData, null, 2)}
); } function RelayFetchQuery({ query, variables, fragment, testId, }: RelayQueryWrapperProps) { const data = useLazyLoadQuery(query, variables ?? {}); const filteredData = filterData(data); return ( <>
				{JSON.stringify(filteredData, null, 2)}
			
Loading fragment... } > {fragment && data ? ( ) : null} ); } function RelayQueryWrapper({ query, variables, buttonText = "Run Query", fragment, testId, }: RelayQueryWrapperProps & { testId?: string }) { const [shouldRun, setShouldRun] = useState(false); if (!shouldRun) { return ( ); } return ( Loading...} > ); } class ErrorBoundary extends Component< { children: React.ReactNode }, { hasError: boolean; error: Error | null } > { constructor(props: { children: React.ReactNode }) { super(props); this.state = { hasError: false, error: null }; } static getDerivedStateFromError(error: Error) { return { hasError: true, error }; } componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { console.log(errorInfo.componentStack); console.error("Error caught by boundary:", error, errorInfo); } render() { if (this.state.hasError) { return (

Something went wrong.

Please try refreshing the page.

{this.state.error && (
							{this.state.error.stack}
						
)}
); } return this.props.children; } } function RelayTests() { return (

Relay Tests

# Basic Query

# Hello With Delay

# Blog Post

# Blog Post With Defer

); } export default RelayTests; strawberry-graphql-0.287.0/e2e/src/components/ui/000077500000000000000000000000001511033167500215765ustar00rootroot00000000000000strawberry-graphql-0.287.0/e2e/src/components/ui/button.tsx000066400000000000000000000041131511033167500236500ustar00rootroot00000000000000import * as React from "react" import { Slot } from "@radix-ui/react-slot" import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" const buttonVariants = cva( "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", { variants: { variant: { default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", destructive: "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", link: "text-primary underline-offset-4 hover:underline", }, size: { default: "h-9 px-4 py-2 has-[>svg]:px-3", sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", lg: "h-10 rounded-md px-6 has-[>svg]:px-4", icon: "size-9", }, }, defaultVariants: { variant: "default", size: "default", }, } ) function Button({ className, variant, size, asChild = false, ...props }: React.ComponentProps<"button"> & VariantProps & { asChild?: boolean }) { const Comp = asChild ? Slot : "button" return ( ) } export { Button, buttonVariants } strawberry-graphql-0.287.0/e2e/src/index.css000066400000000000000000000077131511033167500206250ustar00rootroot00000000000000@import "tailwindcss"; @plugin "tailwindcss-animate"; @custom-variant dark (&:is(.dark *)); :root { --radius: 0.625rem; --background: oklch(1 0 0); --foreground: oklch(0.145 0 0); --card: oklch(1 0 0); --card-foreground: oklch(0.145 0 0); --popover: oklch(1 0 0); --popover-foreground: oklch(0.145 0 0); --primary: oklch(0.205 0 0); --primary-foreground: oklch(0.985 0 0); --secondary: oklch(0.97 0 0); --secondary-foreground: oklch(0.205 0 0); --muted: oklch(0.97 0 0); --muted-foreground: oklch(0.556 0 0); --accent: oklch(0.97 0 0); --accent-foreground: oklch(0.205 0 0); --destructive: oklch(0.577 0.245 27.325); --border: oklch(0.922 0 0); --input: oklch(0.922 0 0); --ring: oklch(0.708 0 0); --chart-1: oklch(0.646 0.222 41.116); --chart-2: oklch(0.6 0.118 184.704); --chart-3: oklch(0.398 0.07 227.392); --chart-4: oklch(0.828 0.189 84.429); --chart-5: oklch(0.769 0.188 70.08); --sidebar: oklch(0.985 0 0); --sidebar-foreground: oklch(0.145 0 0); --sidebar-primary: oklch(0.205 0 0); --sidebar-primary-foreground: oklch(0.985 0 0); --sidebar-accent: oklch(0.97 0 0); --sidebar-accent-foreground: oklch(0.205 0 0); --sidebar-border: oklch(0.922 0 0); --sidebar-ring: oklch(0.708 0 0); } .dark { --background: oklch(0.145 0 0); --foreground: oklch(0.985 0 0); --card: oklch(0.205 0 0); --card-foreground: oklch(0.985 0 0); --popover: oklch(0.205 0 0); --popover-foreground: oklch(0.985 0 0); --primary: oklch(0.922 0 0); --primary-foreground: oklch(0.205 0 0); --secondary: oklch(0.269 0 0); --secondary-foreground: oklch(0.985 0 0); --muted: oklch(0.269 0 0); --muted-foreground: oklch(0.708 0 0); --accent: oklch(0.269 0 0); --accent-foreground: oklch(0.985 0 0); --destructive: oklch(0.704 0.191 22.216); --border: oklch(1 0 0 / 10%); --input: oklch(1 0 0 / 15%); --ring: oklch(0.556 0 0); --chart-1: oklch(0.488 0.243 264.376); --chart-2: oklch(0.696 0.17 162.48); --chart-3: oklch(0.769 0.188 70.08); --chart-4: oklch(0.627 0.265 303.9); --chart-5: oklch(0.645 0.246 16.439); --sidebar: oklch(0.205 0 0); --sidebar-foreground: oklch(0.985 0 0); --sidebar-primary: oklch(0.488 0.243 264.376); --sidebar-primary-foreground: oklch(0.985 0 0); --sidebar-accent: oklch(0.269 0 0); --sidebar-accent-foreground: oklch(0.985 0 0); --sidebar-border: oklch(1 0 0 / 10%); --sidebar-ring: oklch(0.556 0 0); } @theme inline { --radius-sm: calc(var(--radius) - 4px); --radius-md: calc(var(--radius) - 2px); --radius-lg: var(--radius); --radius-xl: calc(var(--radius) + 4px); --color-background: var(--background); --color-foreground: var(--foreground); --color-card: var(--card); --color-card-foreground: var(--card-foreground); --color-popover: var(--popover); --color-popover-foreground: var(--popover-foreground); --color-primary: var(--primary); --color-primary-foreground: var(--primary-foreground); --color-secondary: var(--secondary); --color-secondary-foreground: var(--secondary-foreground); --color-muted: var(--muted); --color-muted-foreground: var(--muted-foreground); --color-accent: var(--accent); --color-accent-foreground: var(--accent-foreground); --color-destructive: var(--destructive); --color-border: var(--border); --color-input: var(--input); --color-ring: var(--ring); --color-chart-1: var(--chart-1); --color-chart-2: var(--chart-2); --color-chart-3: var(--chart-3); --color-chart-4: var(--chart-4); --color-chart-5: var(--chart-5); --color-sidebar: var(--sidebar); --color-sidebar-foreground: var(--sidebar-foreground); --color-sidebar-primary: var(--sidebar-primary); --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); --color-sidebar-accent: var(--sidebar-accent); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); --color-sidebar-border: var(--sidebar-border); --color-sidebar-ring: var(--sidebar-ring); } @layer base { * { @apply border-border outline-ring/50; } body { @apply bg-background text-foreground; } } @theme { --font-mono: "Space Mono", "monospace"; } strawberry-graphql-0.287.0/e2e/src/lib/000077500000000000000000000000001511033167500175425ustar00rootroot00000000000000strawberry-graphql-0.287.0/e2e/src/lib/utils.ts000066400000000000000000000002461511033167500212540ustar00rootroot00000000000000import { clsx, type ClassValue } from "clsx" import { twMerge } from "tailwind-merge" export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } strawberry-graphql-0.287.0/e2e/src/main.tsx000066400000000000000000000003471511033167500204640ustar00rootroot00000000000000import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import "./index.css"; import App from "./App.tsx"; createRoot(document.getElementById("root")!).render( , ); strawberry-graphql-0.287.0/e2e/src/schema.graphql000066400000000000000000000006531511033167500216200ustar00rootroot00000000000000# TODO: strawberry doesn't generate these directives directive @defer( if: Boolean label: String ) on FRAGMENT_SPREAD | INLINE_FRAGMENT directive @stream(if: Boolean, label: String, initialCount: Int = 0) on FIELD type BlogPost { id: ID! title: String! content: String! comments: [Comment!]! } type Comment { id: ID! content: String! } type Query { hello(delay: Float! = 0): String! blogPost(id: ID!): BlogPost! } strawberry-graphql-0.287.0/e2e/src/tests/000077500000000000000000000000001511033167500201365ustar00rootroot00000000000000strawberry-graphql-0.287.0/e2e/src/tests/graphql-clients.spec.ts000066400000000000000000000065671511033167500245520ustar00rootroot00000000000000import { expect, type Page, test } from "@playwright/test"; test.describe("GraphQL Client Tests", () => { test.beforeEach(async ({ page }: { page: Page }) => { // Navigate to the test page await page.goto("/"); }); test.describe("Apollo Tests", () => { test("basic query works", async ({ page }: { page: Page }) => { const button = page.getByTestId("apollo-basic-query-button"); await button.scrollIntoViewIfNeeded(); await button.click(); // Verify the exact result const result = page.getByTestId("apollo-basic-query-result"); await expect(result).toContainText('"hello": "Hello, world!"'); }); test("delayed query works", async ({ page }: { page: Page }) => { const button = page.getByTestId("apollo-delayed-query-button"); await button.scrollIntoViewIfNeeded(); await button.click(); // Verify the result const result = page.getByTestId("apollo-delayed-query-result"); await expect(result).toContainText('"hello": "Hello, world!"'); }); test("blog post with defer works", async ({ page }: { page: Page }) => { const button = page.getByTestId("apollo-defer-button"); await button.scrollIntoViewIfNeeded(); await button.click(); // Verify initial data loads const result = page.getByTestId("apollo-defer-result"); await expect(result).toContainText('"title":'); await expect(result).toContainText('"content":'); // Verify deferred comments load await expect(result).toContainText('"comments":', { timeout: 10000 }); }); }); test.describe("Relay Tests", () => { test("basic query works", async ({ page }: { page: Page }) => { const button = page.getByTestId("relay-basic-query-button"); await button.scrollIntoViewIfNeeded(); await button.click(); // Verify the exact result const result = page.getByTestId("relay-basic-query-result"); await expect(result).toContainText('"hello": "Hello, world!"'); }); test("delayed query works", async ({ page }: { page: Page }) => { const button = page.getByTestId("relay-delayed-query-button"); await button.scrollIntoViewIfNeeded(); await button.click(); // Verify the result const result = page.getByTestId("relay-delayed-query-result"); await expect(result).toContainText('"hello": "Hello, world!"'); }); test("blog post with defer works", async ({ page }: { page: Page }) => { const button = page.getByTestId("relay-defer-button"); await button.scrollIntoViewIfNeeded(); await button.click(); // Verify initial data loads const result = page.getByTestId("relay-defer-result"); await expect(result).toContainText('"title":'); await expect(result).toContainText('"content":'); // Verify deferred comments load in fragment const comments = page.getByTestId("relay-defer-comments"); await expect(comments).toContainText('"comments":'); }); test("error boundary catches errors", async ({ page }: { page: Page }) => { // Force an error by manipulating network conditions await page.route("**/graphql", async (route) => { await route.abort(); }); const button = page.getByTestId("relay-basic-query-button"); await button.scrollIntoViewIfNeeded(); await button.click(); // Verify error boundary catches and displays error await expect(page.getByTestId("error-boundary-message")).toBeVisible(); await expect( page.getByTestId("error-boundary-refresh-message"), ).toBeVisible(); }); }); }); strawberry-graphql-0.287.0/e2e/src/vite-env.d.ts000066400000000000000000000000461511033167500213230ustar00rootroot00000000000000/// strawberry-graphql-0.287.0/e2e/tsconfig.app.json000066400000000000000000000013551511033167500214770ustar00rootroot00000000000000{ "compilerOptions": { "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "isolatedModules": true, "moduleDetection": "force", "noEmit": true, "jsx": "react-jsx", /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true, "baseUrl": ".", "paths": { "@/*": [ "./src/*" ] } }, "include": ["src"] } strawberry-graphql-0.287.0/e2e/tsconfig.json000066400000000000000000000005501511033167500207140ustar00rootroot00000000000000{ "compilerOptions": { "target": "es2021", "lib": ["es2021", "dom"], "module": "commonjs", "moduleResolution": "node", "types": ["node", "@playwright/test"], "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["src/**/*.ts", "playwright.config.ts"] } strawberry-graphql-0.287.0/e2e/tsconfig.node.json000066400000000000000000000011211511033167500216330ustar00rootroot00000000000000{ "compilerOptions": { "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", "target": "ES2022", "lib": ["ES2023"], "module": "ESNext", "skipLibCheck": true, /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "isolatedModules": true, "moduleDetection": "force", "noEmit": true, /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, "include": ["vite.config.ts"] } strawberry-graphql-0.287.0/e2e/vite.config.ts000066400000000000000000000005601511033167500207710ustar00rootroot00000000000000import relay from "vite-plugin-relay"; import path from "path"; import tailwindcss from "@tailwindcss/vite"; import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; // https://vite.dev/config/ export default defineConfig({ plugins: [relay, react(), tailwindcss()], resolve: { alias: { "@": path.resolve(__dirname, "./src"), }, }, }); strawberry-graphql-0.287.0/federation-compatibility/000077500000000000000000000000001511033167500225215ustar00rootroot00000000000000strawberry-graphql-0.287.0/federation-compatibility/Dockerfile000066400000000000000000000005601511033167500245140ustar00rootroot00000000000000FROM python:3.10-slim WORKDIR /web RUN apt update && apt install -y gcc python3-dev RUN pip install poetry COPY strawberry ./strawberry COPY pyproject.toml ./ COPY poetry.lock ./ COPY README.md ./ RUN poetry install COPY federation-compatibility/schema.py ./ EXPOSE 4001 CMD ["poetry", "run", "strawberry", "dev", "-p", "4001", "-h", "0.0.0.0", "schema:schema"] strawberry-graphql-0.287.0/federation-compatibility/docker-compose.yml000066400000000000000000000002051511033167500261530ustar00rootroot00000000000000services: products: build: context: . dockerfile: federation-compatibility/Dockerfile ports: - 4001:4001 strawberry-graphql-0.287.0/federation-compatibility/schema.py000066400000000000000000000202341511033167500243340ustar00rootroot00000000000000from typing import Any, Optional import strawberry from strawberry.schema_directive import Location # ------- data ------- dimension = { "size": "small", "weight": 1, "unit": "kg", } user = { "email": "support@apollographql.com", "name": "Jane Smith", "total_products_created": 1337, "years_of_employment": 10, } deprecated_product = { "sku": "apollo-federation-v1", "package": "@apollo/federation-v1", "reason": "Migrate to Federation V2", "created_by": user["email"], } products_research = [ { "study": { "case_number": "1234", "description": "Federation Study", }, "outcome": None, }, { "study": { "case_number": "1235", "description": "Studio Study", }, "outcome": None, }, ] products = [ { "id": "apollo-federation", "sku": "federation", "package": "@apollo/federation", "variation": {"id": "OSS"}, "dimensions": dimension, "research": [products_research[0]], "created_by": user["email"], "notes": None, }, { "id": "apollo-studio", "sku": "studio", "package": "", "variation": {"id": "platform"}, "dimensions": dimension, "research": [products_research[1]], "created_by": user["email"], "notes": None, }, ] # ------- resolvers ------- def get_product_by_id(id: strawberry.ID) -> Optional["Product"]: data = next((product for product in products if product["id"] == id), None) if not data: return None return Product.from_data(data) def get_product_by_sku_and_package(sku: str, package: str) -> Optional["Product"]: data = next( ( product for product in products if product["sku"] == sku and product["package"] == package ), None, ) return Product.from_data(data) if data else None def get_product_by_sku_and_variation(sku: str, variation: dict) -> Optional["Product"]: data = next( ( product for product in products if product["sku"] == sku and product["variation"]["id"] == variation["id"] ), None, ) return Product.from_data(data) if data else None # ------- types ------- @strawberry.federation.schema_directive( locations=[Location.OBJECT], name="custom", compose=True, import_url="https://myspecs.dev/myCustomDirective/v1.0", ) class Custom: ... @strawberry.federation.type(extend=True, keys=["email"]) class User: email: strawberry.ID = strawberry.federation.field(external=True) name: str | None = strawberry.federation.field(override="users") total_products_created: int | None = strawberry.federation.field(external=True) years_of_employment: int = strawberry.federation.field(external=True) # TODO: the camel casing will be fixed in a future release of Strawberry @strawberry.federation.field(requires=["totalProductsCreated", "yearsOfEmployment"]) def average_products_created_per_year(self) -> int | None: if self.total_products_created is not None: return round(self.total_products_created / self.years_of_employment) return None @classmethod def resolve_reference(cls, **data: Any) -> Optional["User"]: if email := data.get("email"): years_of_employment = data.get("yearsOfEmployment") return User( email=email, name="Jane Smith", total_products_created=1337, years_of_employment=years_of_employment, ) return None @strawberry.federation.type(shareable=True) class ProductDimension: size: str | None weight: float | None unit: str | None = strawberry.federation.field(inaccessible=True) @strawberry.type class ProductVariation: id: strawberry.ID @strawberry.type class CaseStudy: case_number: strawberry.ID description: str | None @strawberry.federation.type(keys=["study { caseNumber }"]) class ProductResearch: study: CaseStudy outcome: str | None @classmethod def from_data(cls, data: dict) -> "ProductResearch": return ProductResearch( study=CaseStudy( case_number=data["study"]["case_number"], description=data["study"]["description"], ), outcome=data["outcome"], ) @classmethod def resolve_reference(cls, **data: Any) -> Optional["ProductResearch"]: study = data.get("study") if not study: return None case_number = study["caseNumber"] research = next( ( product_research for product_research in products_research if product_research["study"]["case_number"] == case_number ), None, ) return ProductResearch.from_data(research) if research else None @strawberry.federation.type(keys=["sku package"]) class DeprecatedProduct: sku: str package: str reason: str | None created_by: User | None @classmethod def resolve_reference(cls, **data: Any) -> Optional["DeprecatedProduct"]: if deprecated_product["sku"] == data.get("sku") and deprecated_product[ "package" ] == data.get("package"): return DeprecatedProduct( sku=deprecated_product["sku"], package=deprecated_product["package"], reason=deprecated_product["reason"], created_by=User.resolve_reference( email=deprecated_product["created_by"] ), ) return None @strawberry.federation.type( keys=["id", "sku package", "sku variation { id }"], directives=[Custom()] ) class Product: id: strawberry.ID sku: str | None package: str | None variation_id: strawberry.Private[str] @strawberry.field def variation(self) -> ProductVariation | None: return ( ProductVariation(strawberry.ID(self.variation_id)) if self.variation_id else None ) @strawberry.field def dimensions(self) -> ProductDimension | None: return ProductDimension(**dimension) @strawberry.federation.field(provides=["totalProductsCreated"]) def created_by(self) -> User | None: return User(**user) notes: str | None = strawberry.federation.field(tags=["internal"]) research: list[ProductResearch] @classmethod def from_data(cls, data: dict) -> "Product": research = [ ProductResearch.from_data(research) for research in data.get("research", []) ] return cls( id=data["id"], sku=data["sku"], package=data["package"], variation_id=data["variation"], notes="hello", research=research, ) @classmethod def resolve_reference(cls, **data: Any) -> Optional["Product"]: if "id" in data: return get_product_by_id(id=data["id"]) if "sku" in data: if "variation" in data: return get_product_by_sku_and_variation( sku=data["sku"], variation=data["variation"] ) if "package" in data: return get_product_by_sku_and_package( sku=data["sku"], package=data["package"] ) return None @strawberry.federation.interface_object(keys=["id"]) class Inventory: id: strawberry.ID deprecated_products: list[DeprecatedProduct] @classmethod def resolve_reference(cls, id: strawberry.ID) -> "Inventory": return Inventory( id=id, deprecated_products=[DeprecatedProduct(**deprecated_product)] ) @strawberry.federation.type(extend=True) class Query: product: Product | None = strawberry.field(resolver=get_product_by_id) @strawberry.field(deprecation_reason="Use product query instead") def deprecated_product(self, sku: str, package: str) -> DeprecatedProduct | None: return None schema = strawberry.federation.Schema( query=Query, types=[Inventory], federation_version="2.7" ) strawberry-graphql-0.287.0/mypy.ini000066400000000000000000000027101511033167500172310ustar00rootroot00000000000000[mypy] files = strawberry plugins = pydantic.mypy, strawberry.ext.mypy_plugin implicit_reexport = False warn_unused_configs = True warn_unused_ignores = True check_untyped_defs = True ignore_errors = False strict_optional = True show_error_codes = True warn_redundant_casts = True ignore_missing_imports = True install_types = True non_interactive = True show_traceback = True # TODO: enable strict at some point ;strict = True ; Disabled because of this bug: https://github.com/python/mypy/issues/9689 ; disallow_untyped_decorators = True [mypy-graphql.*] ignore_errors = True [mypy-pydantic.*] ignore_errors = True [mypy-pydantic_core.*] ignore_errors = True [mypy-rich.*] ignore_errors = True [mypy-libcst.*] ignore_errors = True [mypy-pygments.*] ignore_missing_imports = True [mypy-email_validator.*] ignore_missing_imports = True ignore_errors = True [mypy-dotenv.*] ignore_missing_imports = True [mypy-django.apps.*] ignore_missing_imports = True [mypy-django.http.*] ignore_missing_imports = True [mypy-strawberry_django.*] ignore_missing_imports = True [mypy-cached_property.*] ignore_missing_imports = True [mypy-importlib_metadata.*] ignore_errors = True [mypy-anyio.*] ignore_errors = True [mypy-dns.*] ignore_errors = True [mypy-click.*] ignore_errors = True [mypy-h11.*] ignore_errors = True [mypy-httpx.*] ignore_errors = True [mypy-httpcore.*] ignore_errors = True [mypy-idna.*] ignore_errors = True [mypy-markdown_it.*] ignore_errors = True strawberry-graphql-0.287.0/noxfile.py000066400000000000000000000134041511033167500175520ustar00rootroot00000000000000import itertools from collections.abc import Callable from typing import Any import nox from nox_poetry import Session, session nox.options.reuse_existing_virtualenvs = True nox.options.error_on_external_run = True nox.options.default_venv_backend = "uv" PYTHON_VERSIONS = ["3.14", "3.13", "3.12", "3.11", "3.10"] GQL_CORE_VERSIONS = [ "3.2.6", "3.3.0a9", ] COMMON_PYTEST_OPTIONS = [ "--cov=.", "--cov-append", "--cov-report=xml", "-n", "auto", "--showlocals", "-vv", "--ignore=tests/typecheckers", "--ignore=tests/cli", "--ignore=tests/benchmarks", "--ignore=tests/experimental/pydantic", ] INTEGRATIONS = [ "asgi", "aiohttp", "chalice", "channels", "django", "fastapi", "flask", "quart", "sanic", "litestar", "pydantic", ] def _install_gql_core(session: Session, version: str) -> None: session._session.install(f"graphql-core=={version}") gql_core_parametrize = nox.parametrize( "gql_core", GQL_CORE_VERSIONS, ) def with_gql_core_parametrize(name: str, params: list[str]) -> Callable[[Any], Any]: # github cache doesn't support comma in the name, this is a workaround. arg_names = f"{name}, gql_core" combinations = list(itertools.product(params, GQL_CORE_VERSIONS)) ids = [f"{name}-{comb[0]}__graphql-core-{comb[1]}" for comb in combinations] return lambda fn: nox.parametrize(arg_names, combinations, ids=ids)(fn) @session(python=PYTHON_VERSIONS, name="Tests", tags=["tests"]) @gql_core_parametrize def tests(session: Session, gql_core: str) -> None: session.run_always("poetry", "install", "--without=integrations", external=True) _install_gql_core(session, gql_core) markers = ( ["-m", f"not {integration}", f"--ignore=tests/{integration}"] for integration in INTEGRATIONS ) markers = [item for sublist in markers for item in sublist] session.run( "pytest", *COMMON_PYTEST_OPTIONS, *markers, ) @session(python=["3.12"], name="Django tests", tags=["tests"]) @with_gql_core_parametrize("django", ["5.1.3", "5.0.9", "4.2.0"]) def tests_django(session: Session, django: str, gql_core: str) -> None: session.run_always("poetry", "install", "--without=integrations", external=True) _install_gql_core(session, gql_core) session._session.install(f"django~={django}") # type: ignore session._session.install("pytest-django") # type: ignore session.run("pytest", *COMMON_PYTEST_OPTIONS, "-m", "django") @session(python=["3.11"], name="Starlette tests", tags=["tests"]) @gql_core_parametrize def tests_starlette(session: Session, gql_core: str) -> None: session.run_always("poetry", "install", "--without=integrations", external=True) session._session.install("starlette") # type: ignore _install_gql_core(session, gql_core) session.run("pytest", *COMMON_PYTEST_OPTIONS, "-m", "asgi") @session(python=["3.11"], name="Test integrations", tags=["tests"]) @with_gql_core_parametrize( "integration", [ "aiohttp", "chalice", "channels", "fastapi", "flask", "quart", "sanic", "litestar", ], ) def tests_integrations(session: Session, integration: str, gql_core: str) -> None: session.run_always("poetry", "install", "--without=integrations", external=True) session._session.install(integration) # type: ignore _install_gql_core(session, gql_core) if integration == "aiohttp": session._session.install("pytest-aiohttp") # type: ignore elif integration == "channels": session._session.install("pytest-django") # type: ignore session._session.install("daphne") # type: ignore session.run("pytest", *COMMON_PYTEST_OPTIONS, "-m", integration) @session( python=["3.10", "3.11", "3.12", "3.13"], name="Pydantic V1 tests", tags=["tests", "pydantic"], ) @gql_core_parametrize def test_pydantic(session: Session, gql_core: str) -> None: session.run_always("poetry", "install", "--without=integrations", external=True) session._session.install("pydantic~=1.10") # type: ignore _install_gql_core(session, gql_core) session.run( "pytest", "--cov=.", "--cov-append", "--cov-report=xml", "-m", "pydantic", "--ignore=tests/cli", "--ignore=tests/benchmarks", ) @session(python=PYTHON_VERSIONS, name="Pydantic tests", tags=["tests", "pydantic"]) @gql_core_parametrize def test_pydantic_v2(session: Session, gql_core: str) -> None: session.run_always("poetry", "install", "--without=integrations", external=True) session._session.install("pydantic~=2.12") # type: ignore _install_gql_core(session, gql_core) session.run( "pytest", "--cov=.", "--cov-append", "--cov-report=xml", "-m", "pydantic", "--ignore=tests/cli", "--ignore=tests/benchmarks", ) @session(python=PYTHON_VERSIONS, name="Type checkers tests", tags=["tests"]) def tests_typecheckers(session: Session) -> None: session.run_always("poetry", "install", external=True) session.install("pyright") session.install("pydantic") session.install("mypy") session.run( "pytest", "--cov=.", "--cov-append", "--cov-report=xml", "tests/typecheckers", "-vv", ) @session(python=PYTHON_VERSIONS, name="CLI tests", tags=["tests"]) def tests_cli(session: Session) -> None: session.run_always("poetry", "install", "--without=integrations", external=True) session._session.install("uvicorn") # type: ignore session._session.install("starlette") # type: ignore session.run( "pytest", "--cov=.", "--cov-append", "--cov-report=xml", "tests/cli", "-vv", ) strawberry-graphql-0.287.0/poetry.lock000066400000000000000000020221731511033167500177350ustar00rootroot00000000000000# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. [[package]] name = "aiofiles" version = "24.1.0" description = "File support for asyncio." optional = false python-versions = ">=3.8" groups = ["main", "integrations"] files = [ {file = "aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5"}, {file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"}, ] [[package]] name = "aiohappyeyeballs" version = "2.6.1" description = "Happy Eyeballs for asyncio" optional = false python-versions = ">=3.9" groups = ["main", "integrations"] files = [ {file = "aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"}, {file = "aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558"}, ] [[package]] name = "aiohttp" version = "3.13.0" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.9" groups = ["main", "integrations"] files = [ {file = "aiohttp-3.13.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ca69ec38adf5cadcc21d0b25e2144f6a25b7db7bea7e730bac25075bc305eff0"}, {file = "aiohttp-3.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:240f99f88a9a6beb53ebadac79a2e3417247aa756202ed234b1dbae13d248092"}, {file = "aiohttp-3.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a4676b978a9711531e7cea499d4cdc0794c617a1c0579310ab46c9fdf5877702"}, {file = "aiohttp-3.13.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48fcdd5bc771cbbab8ccc9588b8b6447f6a30f9fe00898b1a5107098e00d6793"}, {file = "aiohttp-3.13.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:eeea0cdd2f687e210c8f605f322d7b0300ba55145014a5dbe98bd4be6fff1f6c"}, {file = "aiohttp-3.13.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b3f01d5aeb632adaaf39c5e93f040a550464a768d54c514050c635adcbb9d0"}, {file = "aiohttp-3.13.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a4dc0b83e25267f42ef065ea57653de4365b56d7bc4e4cfc94fabe56998f8ee6"}, {file = "aiohttp-3.13.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:72714919ed9b90f030f761c20670e529c4af96c31bd000917dd0c9afd1afb731"}, {file = "aiohttp-3.13.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:564be41e85318403fdb176e9e5b3e852d528392f42f2c1d1efcbeeed481126d7"}, {file = "aiohttp-3.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:84912962071087286333f70569362e10793f73f45c48854e6859df11001eb2d3"}, {file = "aiohttp-3.13.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:90b570f1a146181c3d6ae8f755de66227ded49d30d050479b5ae07710f7894c5"}, {file = "aiohttp-3.13.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:2d71ca30257ce756e37a6078b1dff2d9475fee13609ad831eac9a6531bea903b"}, {file = "aiohttp-3.13.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:cd45eb70eca63f41bb156b7dffbe1a7760153b69892d923bdb79a74099e2ed90"}, {file = "aiohttp-3.13.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5ae3a19949a27982c7425a7a5a963c1268fdbabf0be15ab59448cbcf0f992519"}, {file = "aiohttp-3.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ea6df292013c9f050cbf3f93eee9953d6e5acd9e64a0bf4ca16404bfd7aa9bcc"}, {file = "aiohttp-3.13.0-cp310-cp310-win32.whl", hash = "sha256:3b64f22fbb6dcd5663de5ef2d847a5638646ef99112503e6f7704bdecb0d1c4d"}, {file = "aiohttp-3.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:f8d877aa60d80715b2afc565f0f1aea66565824c229a2d065b31670e09fed6d7"}, {file = "aiohttp-3.13.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:99eb94e97a42367fef5fc11e28cb2362809d3e70837f6e60557816c7106e2e20"}, {file = "aiohttp-3.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4696665b2713021c6eba3e2b882a86013763b442577fe5d2056a42111e732eca"}, {file = "aiohttp-3.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3e6a38366f7f0d0f6ed7a1198055150c52fda552b107dad4785c0852ad7685d1"}, {file = "aiohttp-3.13.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aab715b1a0c37f7f11f9f1f579c6fbaa51ef569e47e3c0a4644fba46077a9409"}, {file = "aiohttp-3.13.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7972c82bed87d7bd8e374b60a6b6e816d75ba4f7c2627c2d14eed216e62738e1"}, {file = "aiohttp-3.13.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca8313cb852af788c78d5afdea24c40172cbfff8b35e58b407467732fde20390"}, {file = "aiohttp-3.13.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c333a2385d2a6298265f4b3e960590f787311b87f6b5e6e21bb8375914ef504"}, {file = "aiohttp-3.13.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc6d5fc5edbfb8041d9607f6a417997fa4d02de78284d386bea7ab767b5ea4f3"}, {file = "aiohttp-3.13.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7ddedba3d0043349edc79df3dc2da49c72b06d59a45a42c1c8d987e6b8d175b8"}, {file = "aiohttp-3.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23ca762140159417a6bbc959ca1927f6949711851e56f2181ddfe8d63512b5ad"}, {file = "aiohttp-3.13.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:bfe824d6707a5dc3c5676685f624bc0c63c40d79dc0239a7fd6c034b98c25ebe"}, {file = "aiohttp-3.13.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3c11fa5dd2ef773a8a5a6daa40243d83b450915992eab021789498dc87acc114"}, {file = "aiohttp-3.13.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:00fdfe370cffede3163ba9d3f190b32c0cfc8c774f6f67395683d7b0e48cdb8a"}, {file = "aiohttp-3.13.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6475e42ef92717a678bfbf50885a682bb360a6f9c8819fb1a388d98198fdcb80"}, {file = "aiohttp-3.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:77da5305a410910218b99f2a963092f4277d8a9c1f429c1ff1b026d1826bd0b6"}, {file = "aiohttp-3.13.0-cp311-cp311-win32.whl", hash = "sha256:2f9d9ea547618d907f2ee6670c9a951f059c5994e4b6de8dcf7d9747b420c820"}, {file = "aiohttp-3.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:0f19f7798996d4458c669bd770504f710014926e9970f4729cf55853ae200469"}, {file = "aiohttp-3.13.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1c272a9a18a5ecc48a7101882230046b83023bb2a662050ecb9bfcb28d9ab53a"}, {file = "aiohttp-3.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:97891a23d7fd4e1afe9c2f4473e04595e4acb18e4733b910b6577b74e7e21985"}, {file = "aiohttp-3.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:475bd56492ce5f4cffe32b5533c6533ee0c406d1d0e6924879f83adcf51da0ae"}, {file = "aiohttp-3.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c32ada0abb4bc94c30be2b681c42f058ab104d048da6f0148280a51ce98add8c"}, {file = "aiohttp-3.13.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4af1f8877ca46ecdd0bc0d4a6b66d4b2bddc84a79e2e8366bc0d5308e76bceb8"}, {file = "aiohttp-3.13.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e04ab827ec4f775817736b20cdc8350f40327f9b598dec4e18c9ffdcbea88a93"}, {file = "aiohttp-3.13.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a6d9487b9471ec36b0faedf52228cd732e89be0a2bbd649af890b5e2ce422353"}, {file = "aiohttp-3.13.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e66c57416352f36bf98f6641ddadd47c93740a22af7150d3e9a1ef6e983f9a8"}, {file = "aiohttp-3.13.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:469167d5372f5bb3aedff4fc53035d593884fff2617a75317740e885acd48b04"}, {file = "aiohttp-3.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a9f3546b503975a69b547c9fd1582cad10ede1ce6f3e313a2f547c73a3d7814f"}, {file = "aiohttp-3.13.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6b4174fcec98601f0cfdf308ee29a6ae53c55f14359e848dab4e94009112ee7d"}, {file = "aiohttp-3.13.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a533873a7a4ec2270fb362ee5a0d3b98752e4e1dc9042b257cd54545a96bd8ed"}, {file = "aiohttp-3.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ce887c5e54411d607ee0959cac15bb31d506d86a9bcaddf0b7e9d63325a7a802"}, {file = "aiohttp-3.13.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d871f6a30d43e32fc9252dc7b9febe1a042b3ff3908aa83868d7cf7c9579a59b"}, {file = "aiohttp-3.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:222c828243b4789d79a706a876910f656fad4381661691220ba57b2ab4547865"}, {file = "aiohttp-3.13.0-cp312-cp312-win32.whl", hash = "sha256:682d2e434ff2f1108314ff7f056ce44e457f12dbed0249b24e106e385cf154b9"}, {file = "aiohttp-3.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:0a2be20eb23888df130214b91c262a90e2de1553d6fb7de9e9010cec994c0ff2"}, {file = "aiohttp-3.13.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:00243e51f16f6ec0fb021659d4af92f675f3cf9f9b39efd142aa3ad641d8d1e6"}, {file = "aiohttp-3.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:059978d2fddc462e9211362cbc8446747ecd930537fa559d3d25c256f032ff54"}, {file = "aiohttp-3.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:564b36512a7da3b386143c611867e3f7cfb249300a1bf60889bd9985da67ab77"}, {file = "aiohttp-3.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4aa995b9156ae499393d949a456a7ab0b994a8241a96db73a3b73c7a090eff6a"}, {file = "aiohttp-3.13.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55ca0e95a3905f62f00900255ed807c580775174252999286f283e646d675a49"}, {file = "aiohttp-3.13.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:49ce7525853a981fc35d380aa2353536a01a9ec1b30979ea4e35966316cace7e"}, {file = "aiohttp-3.13.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2117be9883501eaf95503bd313eb4c7a23d567edd44014ba15835a1e9ec6d852"}, {file = "aiohttp-3.13.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d169c47e40c911f728439da853b6fd06da83761012e6e76f11cb62cddae7282b"}, {file = "aiohttp-3.13.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:703ad3f742fc81e543638a7bebddd35acadaa0004a5e00535e795f4b6f2c25ca"}, {file = "aiohttp-3.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5bf635c3476f4119b940cc8d94ad454cbe0c377e61b4527f0192aabeac1e9370"}, {file = "aiohttp-3.13.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:cfe6285ef99e7ee51cef20609be2bc1dd0e8446462b71c9db8bb296ba632810a"}, {file = "aiohttp-3.13.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:34d8af6391c5f2e69749d7f037b614b8c5c42093c251f336bdbfa4b03c57d6c4"}, {file = "aiohttp-3.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:12f5d820fadc5848d4559ea838aef733cf37ed2a1103bba148ac2f5547c14c29"}, {file = "aiohttp-3.13.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0f1338b61ea66f4757a0544ed8a02ccbf60e38d9cfb3225888888dd4475ebb96"}, {file = "aiohttp-3.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:582770f82513419512da096e8df21ca44f86a2e56e25dc93c5ab4df0fe065bf0"}, {file = "aiohttp-3.13.0-cp313-cp313-win32.whl", hash = "sha256:3194b8cab8dbc882f37c13ef1262e0a3d62064fa97533d3aa124771f7bf1ecee"}, {file = "aiohttp-3.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:7897298b3eedc790257fef8a6ec582ca04e9dbe568ba4a9a890913b925b8ea21"}, {file = "aiohttp-3.13.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c417f8c2e1137775569297c584a8a7144e5d1237789eae56af4faf1894a0b861"}, {file = "aiohttp-3.13.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f84b53326abf8e56ebc28a35cebf4a0f396a13a76300f500ab11fe0573bf0b52"}, {file = "aiohttp-3.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:990a53b9d6a30b2878789e490758e568b12b4a7fb2527d0c89deb9650b0e5813"}, {file = "aiohttp-3.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c811612711e01b901e18964b3e5dec0d35525150f5f3f85d0aee2935f059910a"}, {file = "aiohttp-3.13.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ee433e594d7948e760b5c2a78cc06ac219df33b0848793cf9513d486a9f90a52"}, {file = "aiohttp-3.13.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:19bb08e56f57c215e9572cd65cb6f8097804412c54081d933997ddde3e5ac579"}, {file = "aiohttp-3.13.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f27b7488144eb5dd9151cf839b195edd1569629d90ace4c5b6b18e4e75d1e63a"}, {file = "aiohttp-3.13.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d812838c109757a11354a161c95708ae4199c4fd4d82b90959b20914c1d097f6"}, {file = "aiohttp-3.13.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7c20db99da682f9180fa5195c90b80b159632fb611e8dbccdd99ba0be0970620"}, {file = "aiohttp-3.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cf8b0870047900eb1f17f453b4b3953b8ffbf203ef56c2f346780ff930a4d430"}, {file = "aiohttp-3.13.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5b8a5557d5af3f4e3add52a58c4cf2b8e6e59fc56b261768866f5337872d596d"}, {file = "aiohttp-3.13.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:052bcdd80c1c54b8a18a9ea0cd5e36f473dc8e38d51b804cea34841f677a9971"}, {file = "aiohttp-3.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:76484ba17b2832776581b7ab466d094e48eba74cb65a60aea20154dae485e8bd"}, {file = "aiohttp-3.13.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:62d8a0adcdaf62ee56bfb37737153251ac8e4b27845b3ca065862fb01d99e247"}, {file = "aiohttp-3.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5004d727499ecb95f7c9147dd0bfc5b5670f71d355f0bd26d7af2d3af8e07d2f"}, {file = "aiohttp-3.13.0-cp314-cp314-win32.whl", hash = "sha256:a1c20c26af48aea984f63f96e5d7af7567c32cb527e33b60a0ef0a6313cf8b03"}, {file = "aiohttp-3.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:56f7d230ec66e799fbfd8350e9544f8a45a4353f1cf40c1fea74c1780f555b8f"}, {file = "aiohttp-3.13.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:2fd35177dc483ae702f07b86c782f4f4b100a8ce4e7c5778cea016979023d9fd"}, {file = "aiohttp-3.13.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:4df1984c8804ed336089e88ac81a9417b1fd0db7c6f867c50a9264488797e778"}, {file = "aiohttp-3.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e68c0076052dd911a81d3acc4ef2911cc4ef65bf7cadbfbc8ae762da24da858f"}, {file = "aiohttp-3.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc95c49853cd29613e4fe4ff96d73068ff89b89d61e53988442e127e8da8e7ba"}, {file = "aiohttp-3.13.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3b3bdc89413117b40cc39baae08fd09cbdeb839d421c4e7dce6a34f6b54b3ac1"}, {file = "aiohttp-3.13.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3e77a729df23be2116acc4e9de2767d8e92445fbca68886dd991dc912f473755"}, {file = "aiohttp-3.13.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e88ab34826d6eeb6c67e6e92400b9ec653faf5092a35f07465f44c9f1c429f82"}, {file = "aiohttp-3.13.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:019dbef24fe28ce2301419dd63a2b97250d9760ca63ee2976c2da2e3f182f82e"}, {file = "aiohttp-3.13.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2c4aeaedd20771b7b4bcdf0ae791904445df6d856c02fc51d809d12d17cffdc7"}, {file = "aiohttp-3.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b3a8e6a2058a0240cfde542b641d0e78b594311bc1a710cbcb2e1841417d5cb3"}, {file = "aiohttp-3.13.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8e38d55ca36c15f36d814ea414ecb2401d860de177c49f84a327a25b3ee752b"}, {file = "aiohttp-3.13.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a921edbe971aade1bf45bcbb3494e30ba6863a5c78f28be992c42de980fd9108"}, {file = "aiohttp-3.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:474cade59a447cb4019c0dce9f0434bf835fb558ea932f62c686fe07fe6db6a1"}, {file = "aiohttp-3.13.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:99a303ad960747c33b65b1cb65d01a62ac73fa39b72f08a2e1efa832529b01ed"}, {file = "aiohttp-3.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bb34001fc1f05f6b323e02c278090c07a47645caae3aa77ed7ed8a3ce6abcce9"}, {file = "aiohttp-3.13.0-cp314-cp314t-win32.whl", hash = "sha256:dea698b64235d053def7d2f08af9302a69fcd760d1c7bd9988fd5d3b6157e657"}, {file = "aiohttp-3.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1f164699a060c0b3616459d13c1464a981fddf36f892f0a5027cbd45121fb14b"}, {file = "aiohttp-3.13.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:fcc425fb6fd2a00c6d91c85d084c6b75a61bc8bc12159d08e17c5711df6c5ba4"}, {file = "aiohttp-3.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7c2c4c9ce834801651f81d6760d0a51035b8b239f58f298de25162fcf6f8bb64"}, {file = "aiohttp-3.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f91e8f9053a07177868e813656ec57599cd2a63238844393cd01bd69c2e40147"}, {file = "aiohttp-3.13.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df46d9a3d78ec19b495b1107bf26e4fcf97c900279901f4f4819ac5bb2a02a4c"}, {file = "aiohttp-3.13.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3b1eb9871cbe43b6ca6fac3544682971539d8a1d229e6babe43446279679609d"}, {file = "aiohttp-3.13.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:62a3cddf8d9a2eae1f79585fa81d32e13d0c509bb9e7ad47d33c83b45a944df7"}, {file = "aiohttp-3.13.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0f735e680c323ee7e9ef8e2ea26425c7dbc2ede0086fa83ce9d7ccab8a089f26"}, {file = "aiohttp-3.13.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6a51839f778b0e283b43cd82bb17f1835ee2cc1bf1101765e90ae886e53e751c"}, {file = "aiohttp-3.13.0-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac90cfab65bc281d6752f22db5fa90419e33220af4b4fa53b51f5948f414c0e7"}, {file = "aiohttp-3.13.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:62fd54f3e6f17976962ba67f911d62723c760a69d54f5d7b74c3ceb1a4e9ef8d"}, {file = "aiohttp-3.13.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:cf2b60b65df05b6b2fa0d887f2189991a0dbf44a0dd18359001dc8fcdb7f1163"}, {file = "aiohttp-3.13.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:1ccedfe280e804d9a9d7fe8b8c4309d28e364b77f40309c86596baa754af50b1"}, {file = "aiohttp-3.13.0-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:ea01ffbe23df53ece0c8732d1585b3d6079bb8c9ee14f3745daf000051415a31"}, {file = "aiohttp-3.13.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:19ba8625fa69523627b67f7e9901b587a4952470f68814d79cdc5bc460e9b885"}, {file = "aiohttp-3.13.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4b14bfae90598d331b5061fd15a7c290ea0c15b34aeb1cf620464bb5ec02a602"}, {file = "aiohttp-3.13.0-cp39-cp39-win32.whl", hash = "sha256:cf7a4b976da219e726d0043fc94ae8169c0dba1d3a059b3c1e2c964bafc5a77d"}, {file = "aiohttp-3.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:6b9697d15231aeaed4786f090c9c8bc3ab5f0e0a6da1e76c135a310def271020"}, {file = "aiohttp-3.13.0.tar.gz", hash = "sha256:378dbc57dd8cf341ce243f13fa1fa5394d68e2e02c15cd5f28eae35a70ec7f67"}, ] [package.dependencies] aiohappyeyeballs = ">=2.5.0" aiosignal = ">=1.4.0" async-timeout = {version = ">=4.0,<6.0", markers = "python_version < \"3.11\""} attrs = ">=17.3.0" frozenlist = ">=1.1.1" multidict = ">=4.5,<7.0" propcache = ">=0.2.0" yarl = ">=1.17.0,<2.0" [package.extras] speedups = ["Brotli ; platform_python_implementation == \"CPython\"", "aiodns (>=3.3.0)", "brotlicffi ; platform_python_implementation != \"CPython\"", "zstandard ; platform_python_implementation == \"CPython\" and python_version < \"3.14\""] [[package]] name = "aiosignal" version = "1.4.0" description = "aiosignal: a list of registered asynchronous callbacks" optional = false python-versions = ">=3.9" groups = ["main", "integrations"] files = [ {file = "aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e"}, {file = "aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7"}, ] [package.dependencies] frozenlist = ">=1.1.0" typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""} [[package]] name = "annotated-types" version = "0.7.0" description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" groups = ["main", "dev", "integrations"] files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, ] [[package]] name = "ansicon" version = "1.89.0" description = "Python wrapper for loading Jason Hood's ANSICON" optional = false python-versions = "*" groups = ["main", "dev", "integrations"] markers = "platform_system == \"Windows\"" files = [ {file = "ansicon-1.89.0-py2.py3-none-any.whl", hash = "sha256:f1def52d17f65c2c9682cf8370c03f541f410c1752d6a14029f97318e4b9dfec"}, {file = "ansicon-1.89.0.tar.gz", hash = "sha256:e4d039def5768a47e4afec8e89e83ec3ae5a26bf00ad851f914d1240b444d2b1"}, ] [[package]] name = "anyio" version = "4.11.0" description = "High-level concurrency and networking framework on top of asyncio or Trio" optional = false python-versions = ">=3.9" groups = ["main", "dev", "integrations"] files = [ {file = "anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc"}, {file = "anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4"}, ] [package.dependencies] exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} idna = ">=2.8" sniffio = ">=1.1" typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] trio = ["trio (>=0.31.0)"] [[package]] name = "argcomplete" version = "3.6.2" description = "Bash tab completion for argparse" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "argcomplete-3.6.2-py3-none-any.whl", hash = "sha256:65b3133a29ad53fb42c48cf5114752c7ab66c1c38544fdf6460f450c09b42591"}, {file = "argcomplete-3.6.2.tar.gz", hash = "sha256:d0519b1bc867f5f4f4713c41ad0aba73a4a5f007449716b16f385f2166dc6adf"}, ] [package.extras] test = ["coverage", "mypy", "pexpect", "ruff", "wheel"] [[package]] name = "asgiref" version = "3.10.0" description = "ASGI specs, helper code, and adapters" optional = false python-versions = ">=3.9" groups = ["main", "dev", "integrations"] files = [ {file = "asgiref-3.10.0-py3-none-any.whl", hash = "sha256:aef8a81283a34d0ab31630c9b7dfe70c812c95eba78171367ca8745e88124734"}, {file = "asgiref-3.10.0.tar.gz", hash = "sha256:d89f2d8cd8b56dada7d52fa7dc8075baa08fb836560710d38c292a7a3f78c04e"}, ] [package.dependencies] typing_extensions = {version = ">=4", markers = "python_version < \"3.11\""} [package.extras] tests = ["mypy (>=1.14.0)", "pytest", "pytest-asyncio"] [[package]] name = "asttokens" version = "3.0.0" description = "Annotate AST trees with source code positions" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2"}, {file = "asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7"}, ] [package.extras] astroid = ["astroid (>=2,<4)"] test = ["astroid (>=2,<4)", "pytest", "pytest-cov", "pytest-xdist"] [[package]] name = "async-timeout" version = "5.0.1" description = "Timeout context manager for asyncio programs" optional = false python-versions = ">=3.8" groups = ["main", "integrations"] markers = "python_version == \"3.10\"" files = [ {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, ] [[package]] name = "attrs" version = "25.4.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.9" groups = ["main", "dev", "integrations"] files = [ {file = "attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"}, {file = "attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11"}, ] [[package]] name = "autobahn" version = "24.4.2" description = "WebSocket client & server library, WAMP real-time framework" optional = false python-versions = ">=3.9" groups = ["integrations"] files = [ {file = "autobahn-24.4.2-py2.py3-none-any.whl", hash = "sha256:c56a2abe7ac78abbfb778c02892d673a4de58fd004d088cd7ab297db25918e81"}, {file = "autobahn-24.4.2.tar.gz", hash = "sha256:a2d71ef1b0cf780b6d11f8b205fd2c7749765e65795f2ea7d823796642ee92c9"}, ] [package.dependencies] cryptography = ">=3.4.6" hyperlink = ">=21.0.0" setuptools = "*" txaio = ">=21.2.1" [package.extras] all = ["PyGObject (>=3.40.0)", "argon2-cffi (>=20.1.0)", "attrs (>=20.3.0)", "base58 (>=2.1.0)", "bitarray (>=2.7.5)", "cbor2 (>=5.2.0)", "cffi (>=1.14.5)", "click (>=8.1.2)", "ecdsa (>=0.16.1)", "eth-abi (>=4.0.0)", "flatbuffers (>=22.12.6)", "hkdf (>=0.0.3)", "jinja2 (>=2.11.3)", "mnemonic (>=0.19)", "msgpack (>=1.0.2) ; platform_python_implementation == \"CPython\"", "passlib (>=1.7.4)", "py-ecc (>=5.1.0)", "py-eth-sig-utils (>=0.4.0)", "py-multihash (>=2.0.1)", "py-ubjson (>=0.16.1)", "pynacl (>=1.4.0)", "pyopenssl (>=20.0.1)", "python-snappy (>=0.6.0)", "pytrie (>=0.4.0)", "qrcode (>=7.3.1)", "rlp (>=2.0.1)", "service-identity (>=18.1.0)", "spake2 (>=0.8)", "twisted (>=20.3.0)", "twisted (>=24.3.0)", "u-msgpack-python (>=2.1) ; platform_python_implementation != \"CPython\"", "ujson (>=4.0.2) ; platform_python_implementation == \"CPython\"", "web3[ipfs] (>=6.0.0)", "xbr (>=21.2.1)", "yapf (==0.29.0)", "zlmdb (>=21.2.1)", "zope.interface (>=5.2.0)"] compress = ["python-snappy (>=0.6.0)"] dev = ["backports.tempfile (>=1.0)", "build (>=1.2.1)", "bumpversion (>=0.5.3)", "codecov (>=2.0.15)", "flake8 (<5)", "humanize (>=0.5.1)", "mypy (>=0.610) ; python_version >= \"3.4\" and platform_python_implementation != \"PyPy\"", "passlib", "pep8-naming (>=0.3.3)", "pip (>=9.0.1)", "pyenchant (>=1.6.6)", "pyflakes (>=1.0.0)", "pyinstaller (>=4.2)", "pylint (>=1.9.2)", "pytest (>=3.4.2)", "pytest-aiohttp", "pytest-asyncio (>=0.14.0)", "pytest-runner (>=2.11.1)", "pyyaml (>=4.2b4)", "qualname", "sphinx (>=1.7.1)", "sphinx-autoapi (>=1.7.0)", "sphinx-rtd-theme (>=0.1.9)", "sphinxcontrib-images (>=0.9.1)", "tox (>=4.2.8)", "tox-gh-actions (>=2.2.0)", "twine (>=3.3.0)", "twisted (>=22.10.0)", "txaio (>=20.4.1)", "watchdog (>=0.8.3)", "wheel (>=0.36.2)", "yapf (==0.29.0)"] encryption = ["pynacl (>=1.4.0)", "pyopenssl (>=20.0.1)", "pytrie (>=0.4.0)", "qrcode (>=7.3.1)", "service-identity (>=18.1.0)"] nvx = ["cffi (>=1.14.5)"] scram = ["argon2-cffi (>=20.1.0)", "cffi (>=1.14.5)", "passlib (>=1.7.4)"] serialization = ["cbor2 (>=5.2.0)", "flatbuffers (>=22.12.6)", "msgpack (>=1.0.2) ; platform_python_implementation == \"CPython\"", "py-ubjson (>=0.16.1)", "u-msgpack-python (>=2.1) ; platform_python_implementation != \"CPython\"", "ujson (>=4.0.2) ; platform_python_implementation == \"CPython\""] twisted = ["attrs (>=20.3.0)", "twisted (>=24.3.0)", "zope.interface (>=5.2.0)"] ui = ["PyGObject (>=3.40.0)"] xbr = ["base58 (>=2.1.0)", "bitarray (>=2.7.5)", "cbor2 (>=5.2.0)", "click (>=8.1.2)", "ecdsa (>=0.16.1)", "eth-abi (>=4.0.0)", "hkdf (>=0.0.3)", "jinja2 (>=2.11.3)", "mnemonic (>=0.19)", "py-ecc (>=5.1.0)", "py-eth-sig-utils (>=0.4.0)", "py-multihash (>=2.0.1)", "rlp (>=2.0.1)", "spake2 (>=0.8)", "twisted (>=20.3.0)", "web3[ipfs] (>=6.0.0)", "xbr (>=21.2.1)", "yapf (==0.29.0)", "zlmdb (>=21.2.1)"] [[package]] name = "automat" version = "25.4.16" description = "Self-service finite-state machines for the programmer on the go." optional = false python-versions = ">=3.9" groups = ["integrations"] files = [ {file = "automat-25.4.16-py3-none-any.whl", hash = "sha256:04e9bce696a8d5671ee698005af6e5a9fa15354140a87f4870744604dcdd3ba1"}, {file = "automat-25.4.16.tar.gz", hash = "sha256:0017591a5477066e90d26b0e696ddc143baafd87b588cfac8100bc6be9634de0"}, ] [package.extras] visualize = ["Twisted (>=16.1.1)", "graphviz (>0.5.1)"] [[package]] name = "backoff" version = "2.2.1" description = "Function decoration for backoff and retry" optional = false python-versions = ">=3.7,<4.0" groups = ["dev"] files = [ {file = "backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8"}, {file = "backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba"}, ] [[package]] name = "backports-tarfile" version = "1.2.0" description = "Backport of CPython tarfile module" optional = false python-versions = ">=3.8" groups = ["dev"] markers = "python_version < \"3.12\"" files = [ {file = "backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34"}, {file = "backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991"}, ] [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] testing = ["jaraco.test", "pytest (!=8.0.*)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)"] [[package]] name = "black" version = "25.9.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "black-25.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ce41ed2614b706fd55fd0b4a6909d06b5bab344ffbfadc6ef34ae50adba3d4f7"}, {file = "black-25.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ab0ce111ef026790e9b13bd216fa7bc48edd934ffc4cbf78808b235793cbc92"}, {file = "black-25.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f96b6726d690c96c60ba682955199f8c39abc1ae0c3a494a9c62c0184049a713"}, {file = "black-25.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:d119957b37cc641596063cd7db2656c5be3752ac17877017b2ffcdb9dfc4d2b1"}, {file = "black-25.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:456386fe87bad41b806d53c062e2974615825c7a52159cde7ccaeb0695fa28fa"}, {file = "black-25.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a16b14a44c1af60a210d8da28e108e13e75a284bf21a9afa6b4571f96ab8bb9d"}, {file = "black-25.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aaf319612536d502fdd0e88ce52d8f1352b2c0a955cc2798f79eeca9d3af0608"}, {file = "black-25.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:c0372a93e16b3954208417bfe448e09b0de5cc721d521866cd9e0acac3c04a1f"}, {file = "black-25.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1b9dc70c21ef8b43248f1d86aedd2aaf75ae110b958a7909ad8463c4aa0880b0"}, {file = "black-25.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8e46eecf65a095fa62e53245ae2795c90bdecabd53b50c448d0a8bcd0d2e74c4"}, {file = "black-25.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9101ee58ddc2442199a25cb648d46ba22cd580b00ca4b44234a324e3ec7a0f7e"}, {file = "black-25.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:77e7060a00c5ec4b3367c55f39cf9b06e68965a4f2e61cecacd6d0d9b7ec945a"}, {file = "black-25.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0172a012f725b792c358d57fe7b6b6e8e67375dd157f64fa7a3097b3ed3e2175"}, {file = "black-25.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3bec74ee60f8dfef564b573a96b8930f7b6a538e846123d5ad77ba14a8d7a64f"}, {file = "black-25.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b756fc75871cb1bcac5499552d771822fd9db5a2bb8db2a7247936ca48f39831"}, {file = "black-25.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:846d58e3ce7879ec1ffe816bb9df6d006cd9590515ed5d17db14e17666b2b357"}, {file = "black-25.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ef69351df3c84485a8beb6f7b8f9721e2009e20ef80a8d619e2d1788b7816d47"}, {file = "black-25.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e3c1f4cd5e93842774d9ee4ef6cd8d17790e65f44f7cdbaab5f2cf8ccf22a823"}, {file = "black-25.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:154b06d618233fe468236ba1f0e40823d4eb08b26f5e9261526fde34916b9140"}, {file = "black-25.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:e593466de7b998374ea2585a471ba90553283fb9beefcfa430d84a2651ed5933"}, {file = "black-25.9.0-py3-none-any.whl", hash = "sha256:474b34c1342cdc157d307b56c4c65bce916480c4a8f6551fdc6bf9b486a7c4ae"}, {file = "black-25.9.0.tar.gz", hash = "sha256:0474bca9a0dd1b51791fcc507a4e02078a1c63f6d4e4ae5544b9848c7adfb619"}, ] [package.dependencies] click = ">=8.0.0" mypy-extensions = ">=0.4.3" packaging = ">=22.0" pathspec = ">=0.9.0" platformdirs = ">=2" pytokens = ">=0.1.10" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] d = ["aiohttp (>=3.10)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "blessed" version = "1.22.0" description = "Easy, practical library for making terminal apps, by providing an elegant, well-documented interface to Colors, Keyboard input, and screen Positioning capabilities." optional = false python-versions = ">=2.7" groups = ["main", "dev", "integrations"] files = [ {file = "blessed-1.22.0-py2.py3-none-any.whl", hash = "sha256:a1fed52d708a1aa26dfb8d3eaecf6f4714bff590e728baeefcb44f2c16c8de82"}, {file = "blessed-1.22.0.tar.gz", hash = "sha256:1818efb7c10015478286f21a412fcdd31a3d8b94a18f6d926e733827da7a844b"}, ] [package.dependencies] jinxed = {version = ">=1.1.0", markers = "platform_system == \"Windows\""} wcwidth = ">=0.1.4" [[package]] name = "blinker" version = "1.9.0" description = "Fast, simple object-to-object and broadcast signaling" optional = false python-versions = ">=3.9" groups = ["main", "integrations"] files = [ {file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"}, {file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"}, ] [[package]] name = "botocore" version = "1.40.46" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.9" groups = ["main", "integrations"] files = [ {file = "botocore-1.40.46-py3-none-any.whl", hash = "sha256:d2c8e0d9ba804d6fd9b942db0aa3e6cfbdd9aab86581b472ee97809b6e5103e0"}, {file = "botocore-1.40.46.tar.gz", hash = "sha256:4b0c0efdba788117ef365bf930c0be7300fa052e5e195ea3ed53ab278fc6d7b1"}, ] [package.dependencies] jmespath = ">=0.7.1,<2.0.0" python-dateutil = ">=2.1,<3.0.0" urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""} [package.extras] crt = ["awscrt (==0.27.6)"] [[package]] name = "build" version = "1.3.0" description = "A simple, correct Python build frontend" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "build-1.3.0-py3-none-any.whl", hash = "sha256:7145f0b5061ba90a1500d60bd1b13ca0a8a4cebdd0cc16ed8adf1c0e739f43b4"}, {file = "build-1.3.0.tar.gz", hash = "sha256:698edd0ea270bde950f53aed21f3a0135672206f3911e0176261a31e0e07b397"}, ] [package.dependencies] colorama = {version = "*", markers = "os_name == \"nt\""} importlib-metadata = {version = ">=4.6", markers = "python_full_version < \"3.10.2\""} packaging = ">=19.1" pyproject_hooks = "*" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} [package.extras] uv = ["uv (>=0.1.18)"] virtualenv = ["virtualenv (>=20.11) ; python_version < \"3.10\"", "virtualenv (>=20.17) ; python_version >= \"3.10\" and python_version < \"3.14\"", "virtualenv (>=20.31) ; python_version >= \"3.14\""] [[package]] name = "cachecontrol" version = "0.14.3" description = "httplib2 caching for requests" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "cachecontrol-0.14.3-py3-none-any.whl", hash = "sha256:b35e44a3113f17d2a31c1e6b27b9de6d4405f84ae51baa8c1d3cc5b633010cae"}, {file = "cachecontrol-0.14.3.tar.gz", hash = "sha256:73e7efec4b06b20d9267b441c1f733664f989fb8688391b670ca812d70795d11"}, ] [package.dependencies] filelock = {version = ">=3.8.0", optional = true, markers = "extra == \"filecache\""} msgpack = ">=0.5.2,<2.0.0" requests = ">=2.16.0" [package.extras] dev = ["CacheControl[filecache,redis]", "build", "cherrypy", "codespell[tomli]", "furo", "mypy", "pytest", "pytest-cov", "ruff", "sphinx", "sphinx-copybutton", "tox", "types-redis", "types-requests"] filecache = ["filelock (>=3.8.0)"] redis = ["redis (>=2.10.5)"] [[package]] name = "cattrs" version = "25.3.0" description = "Composable complex class support for attrs and dataclasses." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "cattrs-25.3.0-py3-none-any.whl", hash = "sha256:9896e84e0a5bf723bc7b4b68f4481785367ce07a8a02e7e9ee6eb2819bc306ff"}, {file = "cattrs-25.3.0.tar.gz", hash = "sha256:1ac88d9e5eda10436c4517e390a4142d88638fe682c436c93db7ce4a277b884a"}, ] [package.dependencies] attrs = ">=25.4.0" exceptiongroup = {version = ">=1.1.1", markers = "python_version < \"3.11\""} typing-extensions = ">=4.14.0" [package.extras] bson = ["pymongo (>=4.4.0)"] cbor2 = ["cbor2 (>=5.4.6)"] msgpack = ["msgpack (>=1.0.5)"] msgspec = ["msgspec (>=0.19.0) ; implementation_name == \"cpython\""] orjson = ["orjson (>=3.11.3) ; implementation_name == \"cpython\""] pyyaml = ["pyyaml (>=6.0)"] tomlkit = ["tomlkit (>=0.11.8)"] ujson = ["ujson (>=5.10.0)"] [[package]] name = "certifi" version = "2025.10.5" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" groups = ["main", "dev", "integrations"] files = [ {file = "certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de"}, {file = "certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43"}, ] [[package]] name = "cffi" version = "2.0.0" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.9" groups = ["dev", "integrations"] files = [ {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}, {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}, {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}, {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}, {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}, {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}, {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}, {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}, {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}, {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}, {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}, {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}, {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}, {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}, {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}, {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}, {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}, {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}, {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}, {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}, {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}, {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}, {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}, {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}, {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}, {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}, {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}, {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}, {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}, {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}, {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}, {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}, {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}, {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}, {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}, {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}, {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}, {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}, {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}, {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}, {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}, {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}, {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}, {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}, {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}, {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}, {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}, {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}, {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}, {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}, {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}, {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}, {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}, {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}, {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}, {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}, {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}, {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}, {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}, {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}, {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}, {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}, {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}, {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}, {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}, {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}, {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}, {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}, {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}, {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"}, {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"}, {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"}, {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"}, {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"}, {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"}, {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"}, {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"}, {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"}, {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"}, {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"}, {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, ] markers = {integrations = "platform_python_implementation != \"PyPy\""} [package.dependencies] pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} [[package]] name = "cfgv" version = "3.4.0" description = "Validate configuration and produce human readable error messages." optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, ] [[package]] name = "chalice" version = "1.32.0" description = "Microframework" optional = false python-versions = "*" groups = ["main", "integrations"] files = [ {file = "chalice-1.32.0-py3-none-any.whl", hash = "sha256:671fdf45b8fe9315a29acb63a0accfdff60dfc582ea4faf54f0d463323930542"}, {file = "chalice-1.32.0.tar.gz", hash = "sha256:c1d469316747ef8850b4b286c60bcf8c53da3bab1a2042d7551284aa8be06af2"}, ] [package.dependencies] botocore = ">=1.14.0,<2.0.0" click = ">=7,<9.0" inquirer = ">=3.0.0,<4.0.0" jmespath = ">=0.9.3,<2.0.0" pip = ">=9,<25.1" pyyaml = ">=5.3.1,<7.0.0" setuptools = "*" six = ">=1.10.0,<2.0.0" wheel = "*" [package.extras] cdk = ["aws_cdk.aws-s3-assets (>=1.85.0,<2.0)", "aws_cdk.aws_iam (>=1.85.0,<2.0)", "aws_cdk.cloudformation-include (>=1.85.0,<2.0)", "aws_cdk.core (>=1.85.0,<2.0)"] cdkv2 = ["aws-cdk-lib (>2.0,<3.0)"] event-file-poller = ["watchdog (==2.3.1)"] [[package]] name = "channels" version = "4.3.1" description = "Brings async, event-driven capabilities to Django." optional = false python-versions = ">=3.9" groups = ["main", "integrations"] files = [ {file = "channels-4.3.1-py3-none-any.whl", hash = "sha256:b091d4b26f91d807de3e84aead7ba785314f27eaf5bac31dd51b1c956b883859"}, {file = "channels-4.3.1.tar.gz", hash = "sha256:97413ffd674542db08e16a9ef09cd86ec0113e5f8125fbd33cf0854adcf27cdb"}, ] [package.dependencies] asgiref = ">=3.9.0,<4" Django = ">=4.2" [package.extras] daphne = ["daphne (>=4.0.0)"] tests = ["async-timeout", "coverage (>=4.5,<5.0)", "pytest", "pytest-asyncio", "pytest-django", "selenium"] [[package]] name = "charset-normalizer" version = "3.4.3" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ {file = "charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72"}, {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe"}, {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601"}, {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c"}, {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2"}, {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0"}, {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0"}, {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0"}, {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a"}, {file = "charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f"}, {file = "charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669"}, {file = "charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b"}, {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64"}, {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91"}, {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f"}, {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07"}, {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30"}, {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14"}, {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c"}, {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae"}, {file = "charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849"}, {file = "charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c"}, {file = "charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1"}, {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884"}, {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018"}, {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392"}, {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f"}, {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154"}, {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491"}, {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93"}, {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f"}, {file = "charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37"}, {file = "charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc"}, {file = "charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe"}, {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8"}, {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9"}, {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31"}, {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f"}, {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927"}, {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9"}, {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5"}, {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc"}, {file = "charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce"}, {file = "charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef"}, {file = "charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15"}, {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db"}, {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d"}, {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096"}, {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa"}, {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049"}, {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0"}, {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92"}, {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16"}, {file = "charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce"}, {file = "charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c"}, {file = "charset_normalizer-3.4.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c"}, {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b"}, {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4"}, {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b"}, {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9"}, {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb"}, {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a"}, {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942"}, {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b"}, {file = "charset_normalizer-3.4.3-cp38-cp38-win32.whl", hash = "sha256:ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557"}, {file = "charset_normalizer-3.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40"}, {file = "charset_normalizer-3.4.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05"}, {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e"}, {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99"}, {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7"}, {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7"}, {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19"}, {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312"}, {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc"}, {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34"}, {file = "charset_normalizer-3.4.3-cp39-cp39-win32.whl", hash = "sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432"}, {file = "charset_normalizer-3.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca"}, {file = "charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a"}, {file = "charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14"}, ] [[package]] name = "cleo" version = "2.1.0" description = "Cleo allows you to create beautiful and testable command-line interfaces." optional = false python-versions = ">=3.7,<4.0" groups = ["dev"] files = [ {file = "cleo-2.1.0-py3-none-any.whl", hash = "sha256:4a31bd4dd45695a64ee3c4758f583f134267c2bc518d8ae9a29cf237d009b07e"}, {file = "cleo-2.1.0.tar.gz", hash = "sha256:0b2c880b5d13660a7ea651001fb4acb527696c01f15c9ee650f377aa543fd523"}, ] [package.dependencies] crashtest = ">=0.4.1,<0.5.0" rapidfuzz = ">=3.0.0,<4.0.0" [[package]] name = "click" version = "8.3.0" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" groups = ["main", "dev", "integrations"] files = [ {file = "click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc"}, {file = "click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4"}, ] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "codeflash" version = "0.17.2" description = "Client for codeflash.ai - automatic code performance optimization, powered by AI" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "codeflash-0.17.2-py3-none-any.whl", hash = "sha256:51b1e9d63dc9008caeaade6cfb7a9cdd0667aa550ab9239c5b4d7a012feb6348"}, {file = "codeflash-0.17.2.tar.gz", hash = "sha256:0d626eec3946c4381d876862402685f8d924edcf03abc8474a050ee2fb280095"}, ] [package.dependencies] click = ">=8.1.0" codeflash-benchmark = "*" coverage = ">=7.6.4" crosshair-tool = ">=0.0.78" dill = ">=0.3.8" filelock = "*" gitpython = ">=3.1.31" humanize = ">=4.0.0" inquirer = ">=3.0.0" isort = ">=5.11.0" jedi = ">=0.19.1" junitparser = ">=3.1.0" libcst = ">=1.0.1" line-profiler = ">=4.2.0" lxml = ">=5.3.0" parameterized = ">=0.9.0" platformdirs = ">=4.3.7" posthog = ">=3.0.0" pydantic = ">=1.10.1" pygls = ">=1.3.1" pytest = ">=7.0.0,<8.3.4 || >8.3.4" pytest-timeout = ">=2.1.0" rich = ">=13.8.1" sentry-sdk = ">=1.40.6,<3.0.0" timeout-decorator = ">=0.5.0" tomlkit = ">=0.11.7" unidiff = ">=0.7.4" unittest-xml-reporting = ">=3.2.0" [package.extras] asyncio = ["pytest-asyncio (>=1.2.0)"] [[package]] name = "codeflash-benchmark" version = "0.2.0" description = "Pytest benchmarking plugin for codeflash.ai - automatic code performance optimization" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "codeflash_benchmark-0.2.0-py3-none-any.whl", hash = "sha256:996e8bf79e6ab6e3c6b5dacd7370a4414d37bea085b69c691bb3c5e07c879c39"}, {file = "codeflash_benchmark-0.2.0.tar.gz", hash = "sha256:05ffbc948c9e3896b15d57aee18dec96f2db6159157b63aa109973c4d41c0595"}, ] [package.dependencies] pytest = ">=7.0.0,<8.3.4 || >8.3.4" [[package]] name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" groups = ["main", "dev", "integrations"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] markers = {main = "platform_system == \"Windows\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\" or os_name == \"nt\"", integrations = "platform_system == \"Windows\" or sys_platform == \"win32\""} [[package]] name = "colorlog" version = "6.9.0" description = "Add colours to the output of Python's logging module." optional = false python-versions = ">=3.6" groups = ["dev"] files = [ {file = "colorlog-6.9.0-py3-none-any.whl", hash = "sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff"}, {file = "colorlog-6.9.0.tar.gz", hash = "sha256:bfba54a1b93b94f54e1f4fe48395725a3d92fd2a4af702f6bd70946bdc0c6ac2"}, ] [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} [package.extras] development = ["black", "flake8", "mypy", "pytest", "types-colorama"] [[package]] name = "constantly" version = "23.10.4" description = "Symbolic constants in Python" optional = false python-versions = ">=3.8" groups = ["integrations"] files = [ {file = "constantly-23.10.4-py3-none-any.whl", hash = "sha256:3fd9b4d1c3dc1ec9757f3c52aef7e53ad9323dbe39f51dfd4c43853b68dfa3f9"}, {file = "constantly-23.10.4.tar.gz", hash = "sha256:aa92b70a33e2ac0bb33cd745eb61776594dc48764b06c35e0efd050b7f1c7cbd"}, ] [[package]] name = "coverage" version = "7.10.7" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a"}, {file = "coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5"}, {file = "coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17"}, {file = "coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b"}, {file = "coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87"}, {file = "coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e"}, {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e"}, {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df"}, {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0"}, {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13"}, {file = "coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b"}, {file = "coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807"}, {file = "coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59"}, {file = "coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a"}, {file = "coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699"}, {file = "coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d"}, {file = "coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e"}, {file = "coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23"}, {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab"}, {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82"}, {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2"}, {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61"}, {file = "coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14"}, {file = "coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2"}, {file = "coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a"}, {file = "coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417"}, {file = "coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973"}, {file = "coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c"}, {file = "coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7"}, {file = "coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6"}, {file = "coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59"}, {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b"}, {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a"}, {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb"}, {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1"}, {file = "coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256"}, {file = "coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba"}, {file = "coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf"}, {file = "coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d"}, {file = "coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b"}, {file = "coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e"}, {file = "coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b"}, {file = "coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49"}, {file = "coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911"}, {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0"}, {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f"}, {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c"}, {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f"}, {file = "coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698"}, {file = "coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843"}, {file = "coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546"}, {file = "coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c"}, {file = "coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15"}, {file = "coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4"}, {file = "coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0"}, {file = "coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0"}, {file = "coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65"}, {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541"}, {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6"}, {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999"}, {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2"}, {file = "coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a"}, {file = "coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb"}, {file = "coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb"}, {file = "coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520"}, {file = "coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32"}, {file = "coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f"}, {file = "coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a"}, {file = "coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360"}, {file = "coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69"}, {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14"}, {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe"}, {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e"}, {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd"}, {file = "coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2"}, {file = "coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681"}, {file = "coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880"}, {file = "coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63"}, {file = "coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2"}, {file = "coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d"}, {file = "coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0"}, {file = "coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699"}, {file = "coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9"}, {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f"}, {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1"}, {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0"}, {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399"}, {file = "coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235"}, {file = "coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d"}, {file = "coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a"}, {file = "coverage-7.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3"}, {file = "coverage-7.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c"}, {file = "coverage-7.10.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396"}, {file = "coverage-7.10.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40"}, {file = "coverage-7.10.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594"}, {file = "coverage-7.10.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a"}, {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b"}, {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3"}, {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0"}, {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f"}, {file = "coverage-7.10.7-cp39-cp39-win32.whl", hash = "sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431"}, {file = "coverage-7.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07"}, {file = "coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260"}, {file = "coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239"}, ] [package.dependencies] tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} [package.extras] toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "crashtest" version = "0.4.1" description = "Manage Python errors with ease" optional = false python-versions = ">=3.7,<4.0" groups = ["dev"] files = [ {file = "crashtest-0.4.1-py3-none-any.whl", hash = "sha256:8d23eac5fa660409f57472e3851dab7ac18aba459a8d19cbbba86d3d5aecd2a5"}, {file = "crashtest-0.4.1.tar.gz", hash = "sha256:80d7b1f316ebfbd429f648076d6275c877ba30ba48979de4191714a75266f0ce"}, ] [[package]] name = "crosshair-tool" version = "0.0.97" description = "Analyze Python code for correctness using symbolic execution." optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "crosshair_tool-0.0.97-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7752f1fc531836ca54bc87975836ad6964e1407a0b7ae43d3291a90d979a6f1e"}, {file = "crosshair_tool-0.0.97-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e84ca96dde383a63a75a5df2d987a653eeb876dd20cf1c717c03a8304f9c1def"}, {file = "crosshair_tool-0.0.97-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8d6bd6ec2a07955ce19be4e314e45371dc02195ece63a5ef9874ab1d37d49e7e"}, {file = "crosshair_tool-0.0.97-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:288f263b9297f36486ba3eb1bc5a7d1eebab5c3330e22c613c52a0c2bf8227e6"}, {file = "crosshair_tool-0.0.97-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b1d037fa45cd2ac3af8927591dbfbd3cc47fa7d23883de9cba0dfc6a78915363"}, {file = "crosshair_tool-0.0.97-cp310-cp310-win32.whl", hash = "sha256:4d979f0892742b288e4537fba925a04c27a220b270a0d5fd11dce8726e9c6430"}, {file = "crosshair_tool-0.0.97-cp310-cp310-win_amd64.whl", hash = "sha256:d0d06ad9952ff8fe026441942789403c0fd563893cc93c9fa1a2742001de9e7c"}, {file = "crosshair_tool-0.0.97-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b3d04f4bf14034ed4e9f22a2ac4be9e2bc1a0bb4438a96bd3b8065d46bc0e9f7"}, {file = "crosshair_tool-0.0.97-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b09ce7501a698c009c342fda6a8c38cfd465b31326db551eb479a76efd57df6"}, {file = "crosshair_tool-0.0.97-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3095d7d2ff4c1e600fc99c429396abade0e3fff0cc3d5eb9f51302d5e6078d39"}, {file = "crosshair_tool-0.0.97-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3c511c88c15ff9f27481e776b2d8204444a02288e21ca2123b0c417d151a1c94"}, {file = "crosshair_tool-0.0.97-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:15499c49c496e7cea3375ac68271f495c06353ba9fa4a5687c68094f99512032"}, {file = "crosshair_tool-0.0.97-cp311-cp311-win32.whl", hash = "sha256:92ad0d15bde85d278eedf0ea25181844df2da364f3de9f0ff3ff2d02697b829b"}, {file = "crosshair_tool-0.0.97-cp311-cp311-win_amd64.whl", hash = "sha256:d41049d14aab08ab0b014fb7dca9c7628dcc6ddd112077c9fabb175143b5c862"}, {file = "crosshair_tool-0.0.97-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f78443b25b3063e2c671d58bde4c70e55672d2d3fa12759979ce4e484123132f"}, {file = "crosshair_tool-0.0.97-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3950febbf03c845149815047bce4fad533124975e8688b1aecd7d4174ec20086"}, {file = "crosshair_tool-0.0.97-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1daa7840e6099f87ca2127769318fcaef4f0456aa9501834955ab2ed68090a74"}, {file = "crosshair_tool-0.0.97-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:18fedd97f207f1d6a34504cbd210af310e842253cdd53a76157fcf944ac9e5b1"}, {file = "crosshair_tool-0.0.97-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a7d8183163d53dc7c3a449793cc51c26f24fec7655e920ea721326c70163b1bc"}, {file = "crosshair_tool-0.0.97-cp312-cp312-win32.whl", hash = "sha256:1b08b694a05568574cf45ee9efc78d8b78d3f99066998bf916a3fc5b2c8e5be5"}, {file = "crosshair_tool-0.0.97-cp312-cp312-win_amd64.whl", hash = "sha256:74376021f35961362cfbf00a93ce882361c6a5d7017fdabe65bfe5f59a1fa4d2"}, {file = "crosshair_tool-0.0.97-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ccbc51d6278e312378052395ad028eee92987042772663c59f5f6b9ba2ed6e97"}, {file = "crosshair_tool-0.0.97-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:90cfc3e7f0e1742cfdff6e4505c2e56adaf0bb97fc280f1277e16e37f5540667"}, {file = "crosshair_tool-0.0.97-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:858e5e07155e7ba80ce69ec1c258af1124aa69851506472dabd1bf0f7c074ff8"}, {file = "crosshair_tool-0.0.97-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5b86a705a702a2a8cc3c655449e55021c4c143a47cf890e74d0fb6b74936070d"}, {file = "crosshair_tool-0.0.97-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e921653ef8a51bd72b163a97ba42f958d401d8a1fbea42e131fd990ae5f6495f"}, {file = "crosshair_tool-0.0.97-cp313-cp313-win32.whl", hash = "sha256:c17ca9c7953cc995dbfa4a9bc3249fa484f22b8cd2a3ce4ec2f94dfec6bc1a7c"}, {file = "crosshair_tool-0.0.97-cp313-cp313-win_amd64.whl", hash = "sha256:31eecf292ed6a36781adf5fbc631d0c5473bf914094dbcbc9d0b0903c191084f"}, {file = "crosshair_tool-0.0.97-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bdaf67a81d9dcda1685f4c04005dbf51e23bff82a9a581004308b81511d69dc"}, {file = "crosshair_tool-0.0.97-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b9af71c3d83fe6192c8b8a559e7a89ddb1b3e4e45e26335fc3598e95d328eb8e"}, {file = "crosshair_tool-0.0.97-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0df8568bb861215f989639c73baa16663f72524ac42971f5542b8a0f7798a845"}, {file = "crosshair_tool-0.0.97-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:feeba99d9e3e62623f0ef0dc2113ba5f406251c0f04c40320f0f35d85c6bd35e"}, {file = "crosshair_tool-0.0.97-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0d478ef8a8f6dd69881a225e450df253185c3ec3088205bc512db1254ead5de9"}, {file = "crosshair_tool-0.0.97-cp314-cp314-win32.whl", hash = "sha256:5fffeaa82cfb4fe3638698de270d242d4c683fa62743f133e60ec77991baac9d"}, {file = "crosshair_tool-0.0.97-cp314-cp314-win_amd64.whl", hash = "sha256:41442ca083add3293b91986842d6cbaf1b6c819dc7dbb04b0422db4ab0c36729"}, {file = "crosshair_tool-0.0.97-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8226e50aeb20a741d84aa2feea96b18ca3cfe576bbd47e9aa61196759a4dfa45"}, {file = "crosshair_tool-0.0.97-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6ddc75268768a86b27b109c4d861fe2728c9e661ffb67fd46b4c0c320a6cc80b"}, {file = "crosshair_tool-0.0.97-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2039d8886d8aee3b5e3bbb5bdd36a183256440e3558f9f8fab807cb011d3faca"}, {file = "crosshair_tool-0.0.97-cp38-cp38-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:06d49b25b686f157ad0a0fd523c5e8388915984ccb24002febca13cb08c68a41"}, {file = "crosshair_tool-0.0.97-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:37a62b41142d79cf4d075946d39c96ce9ea3a113b8fc8ba9fe1f7b89356916fa"}, {file = "crosshair_tool-0.0.97-cp38-cp38-win32.whl", hash = "sha256:8137dcf83e30876e71500dfab133e8b42393d32cab8f5b12025304f0b9a89efd"}, {file = "crosshair_tool-0.0.97-cp38-cp38-win_amd64.whl", hash = "sha256:aca645e4f21a63c064a0d27cc8faf8756024430736448c51c58096b25bc2a7b0"}, {file = "crosshair_tool-0.0.97-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:94dd701a3e07505fd23aba7fbfa87200d1de9daaa170757043286e2eec17714c"}, {file = "crosshair_tool-0.0.97-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6a177990c5d17de97a388bf89178e8a4a264cca699473491a65cf69a603bfce4"}, {file = "crosshair_tool-0.0.97-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0e28bce6fa1f1a2be527f2fd337ecc6a14fa313262f257b4c51ff031821bbd20"}, {file = "crosshair_tool-0.0.97-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9cd7eec7fc3221a62896072d2844fd6e0b81dfcb1c73a87dd6b1678ddf4acb06"}, {file = "crosshair_tool-0.0.97-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b3a81f1b42034daaf850c431765105b4635cc70816cde13d97c198ee489fbaf3"}, {file = "crosshair_tool-0.0.97-cp39-cp39-win32.whl", hash = "sha256:86cca308c5469f8a9d0c76208bbee8d5065a3ddf6a1e021c4381d715c9e60c00"}, {file = "crosshair_tool-0.0.97-cp39-cp39-win_amd64.whl", hash = "sha256:4f6f3b928b52ecb341a75f4da7e97c459f170690a07c51f177b2284b559ab57a"}, {file = "crosshair_tool-0.0.97.tar.gz", hash = "sha256:7e121bbbd2a11710f3d3bd62ae18245d8bd7959dc2c8b262bd523aafa165509b"}, ] [package.dependencies] importlib_metadata = ">=4.0.0" packaging = "*" pygls = ">=1.0.0" typeshed-client = ">=2.0.5" typing_extensions = ">=3.10.0" typing-inspect = ">=0.7.1" z3-solver = ">=4.13.0.0" [package.extras] dev = ["autodocsumm (>=0.2.2,<1)", "black (==25.9.0)", "deal (>=4.13.0)", "icontract (>=2.4.0)", "isort (==5.11.5)", "mypy (==1.18.1)", "numpy (==1.23.4) ; python_version < \"3.12\"", "numpy (==1.26.0) ; python_version == \"3.12\"", "numpy (==2.3.3) ; python_version >= \"3.13\"", "pre-commit (>=2.20,<3.0)", "pytest", "pytest-xdist", "rst2pdf (>=0.102)", "setuptools", "sphinx (>=3.4.3)", "sphinx-rtd-theme (>=0.5.1)", "z3-solver (==4.14.1.0)"] [[package]] name = "cryptography" version = "46.0.2" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = "!=3.9.0,!=3.9.1,>=3.8" groups = ["dev", "integrations"] files = [ {file = "cryptography-46.0.2-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:f3e32ab7dd1b1ef67b9232c4cf5e2ee4cd517d4316ea910acaaa9c5712a1c663"}, {file = "cryptography-46.0.2-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1fd1a69086926b623ef8126b4c33d5399ce9e2f3fac07c9c734c2a4ec38b6d02"}, {file = "cryptography-46.0.2-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb7fb9cd44c2582aa5990cf61a4183e6f54eea3172e54963787ba47287edd135"}, {file = "cryptography-46.0.2-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9066cfd7f146f291869a9898b01df1c9b0e314bfa182cef432043f13fc462c92"}, {file = "cryptography-46.0.2-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:97e83bf4f2f2c084d8dd792d13841d0a9b241643151686010866bbd076b19659"}, {file = "cryptography-46.0.2-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:4a766d2a5d8127364fd936572c6e6757682fc5dfcbdba1632d4554943199f2fa"}, {file = "cryptography-46.0.2-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:fab8f805e9675e61ed8538f192aad70500fa6afb33a8803932999b1049363a08"}, {file = "cryptography-46.0.2-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:1e3b6428a3d56043bff0bb85b41c535734204e599c1c0977e1d0f261b02f3ad5"}, {file = "cryptography-46.0.2-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:1a88634851d9b8de8bb53726f4300ab191d3b2f42595e2581a54b26aba71b7cc"}, {file = "cryptography-46.0.2-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:be939b99d4e091eec9a2bcf41aaf8f351f312cd19ff74b5c83480f08a8a43e0b"}, {file = "cryptography-46.0.2-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f13b040649bc18e7eb37936009b24fd31ca095a5c647be8bb6aaf1761142bd1"}, {file = "cryptography-46.0.2-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bdc25e4e01b261a8fda4e98618f1c9515febcecebc9566ddf4a70c63967043b"}, {file = "cryptography-46.0.2-cp311-abi3-win32.whl", hash = "sha256:8b9bf67b11ef9e28f4d78ff88b04ed0929fcd0e4f70bb0f704cfc32a5c6311ee"}, {file = "cryptography-46.0.2-cp311-abi3-win_amd64.whl", hash = "sha256:758cfc7f4c38c5c5274b55a57ef1910107436f4ae842478c4989abbd24bd5acb"}, {file = "cryptography-46.0.2-cp311-abi3-win_arm64.whl", hash = "sha256:218abd64a2e72f8472c2102febb596793347a3e65fafbb4ad50519969da44470"}, {file = "cryptography-46.0.2-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:bda55e8dbe8533937956c996beaa20266a8eca3570402e52ae52ed60de1faca8"}, {file = "cryptography-46.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e7155c0b004e936d381b15425273aee1cebc94f879c0ce82b0d7fecbf755d53a"}, {file = "cryptography-46.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a61c154cc5488272a6c4b86e8d5beff4639cdb173d75325ce464d723cda0052b"}, {file = "cryptography-46.0.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:9ec3f2e2173f36a9679d3b06d3d01121ab9b57c979de1e6a244b98d51fea1b20"}, {file = "cryptography-46.0.2-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2fafb6aa24e702bbf74de4cb23bfa2c3beb7ab7683a299062b69724c92e0fa73"}, {file = "cryptography-46.0.2-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:0c7ffe8c9b1fcbb07a26d7c9fa5e857c2fe80d72d7b9e0353dcf1d2180ae60ee"}, {file = "cryptography-46.0.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:5840f05518caa86b09d23f8b9405a7b6d5400085aa14a72a98fdf5cf1568c0d2"}, {file = "cryptography-46.0.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:27c53b4f6a682a1b645fbf1cd5058c72cf2f5aeba7d74314c36838c7cbc06e0f"}, {file = "cryptography-46.0.2-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:512c0250065e0a6b286b2db4bbcc2e67d810acd53eb81733e71314340366279e"}, {file = "cryptography-46.0.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:07c0eb6657c0e9cca5891f4e35081dbf985c8131825e21d99b4f440a8f496f36"}, {file = "cryptography-46.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:48b983089378f50cba258f7f7aa28198c3f6e13e607eaf10472c26320332ca9a"}, {file = "cryptography-46.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e6f6775eaaa08c0eec73e301f7592f4367ccde5e4e4df8e58320f2ebf161ea2c"}, {file = "cryptography-46.0.2-cp314-cp314t-win32.whl", hash = "sha256:e8633996579961f9b5a3008683344c2558d38420029d3c0bc7ff77c17949a4e1"}, {file = "cryptography-46.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:48c01988ecbb32979bb98731f5c2b2f79042a6c58cc9a319c8c2f9987c7f68f9"}, {file = "cryptography-46.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:8e2ad4d1a5899b7caa3a450e33ee2734be7cc0689010964703a7c4bcc8dd4fd0"}, {file = "cryptography-46.0.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a08e7401a94c002e79dc3bc5231b6558cd4b2280ee525c4673f650a37e2c7685"}, {file = "cryptography-46.0.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d30bc11d35743bf4ddf76674a0a369ec8a21f87aaa09b0661b04c5f6c46e8d7b"}, {file = "cryptography-46.0.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bca3f0ce67e5a2a2cf524e86f44697c4323a86e0fd7ba857de1c30d52c11ede1"}, {file = "cryptography-46.0.2-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ff798ad7a957a5021dcbab78dfff681f0cf15744d0e6af62bd6746984d9c9e9c"}, {file = "cryptography-46.0.2-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cb5e8daac840e8879407acbe689a174f5ebaf344a062f8918e526824eb5d97af"}, {file = "cryptography-46.0.2-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:3f37aa12b2d91e157827d90ce78f6180f0c02319468a0aea86ab5a9566da644b"}, {file = "cryptography-46.0.2-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:5e38f203160a48b93010b07493c15f2babb4e0f2319bbd001885adb3f3696d21"}, {file = "cryptography-46.0.2-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d19f5f48883752b5ab34cff9e2f7e4a7f216296f33714e77d1beb03d108632b6"}, {file = "cryptography-46.0.2-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:04911b149eae142ccd8c9a68892a70c21613864afb47aba92d8c7ed9cc001023"}, {file = "cryptography-46.0.2-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:8b16c1ede6a937c291d41176934268e4ccac2c6521c69d3f5961c5a1e11e039e"}, {file = "cryptography-46.0.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:747b6f4a4a23d5a215aadd1d0b12233b4119c4313df83ab4137631d43672cc90"}, {file = "cryptography-46.0.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6b275e398ab3a7905e168c036aad54b5969d63d3d9099a0a66cc147a3cc983be"}, {file = "cryptography-46.0.2-cp38-abi3-win32.whl", hash = "sha256:0b507c8e033307e37af61cb9f7159b416173bdf5b41d11c4df2e499a1d8e007c"}, {file = "cryptography-46.0.2-cp38-abi3-win_amd64.whl", hash = "sha256:f9b2dc7668418fb6f221e4bf701f716e05e8eadb4f1988a2487b11aedf8abe62"}, {file = "cryptography-46.0.2-cp38-abi3-win_arm64.whl", hash = "sha256:91447f2b17e83c9e0c89f133119d83f94ce6e0fb55dd47da0a959316e6e9cfa1"}, {file = "cryptography-46.0.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f25a41f5b34b371a06dad3f01799706631331adc7d6c05253f5bca22068c7a34"}, {file = "cryptography-46.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e12b61e0b86611e3f4c1756686d9086c1d36e6fd15326f5658112ad1f1cc8807"}, {file = "cryptography-46.0.2-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1d3b3edd145953832e09607986f2bd86f85d1dc9c48ced41808b18009d9f30e5"}, {file = "cryptography-46.0.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fe245cf4a73c20592f0f48da39748b3513db114465be78f0a36da847221bd1b4"}, {file = "cryptography-46.0.2-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2b9cad9cf71d0c45566624ff76654e9bae5f8a25970c250a26ccfc73f8553e2d"}, {file = "cryptography-46.0.2-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9bd26f2f75a925fdf5e0a446c0de2714f17819bf560b44b7480e4dd632ad6c46"}, {file = "cryptography-46.0.2-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:7282d8f092b5be7172d6472f29b0631f39f18512a3642aefe52c3c0e0ccfad5a"}, {file = "cryptography-46.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c4b93af7920cdf80f71650769464ccf1fb49a4b56ae0024173c24c48eb6b1612"}, {file = "cryptography-46.0.2.tar.gz", hash = "sha256:21b6fc8c71a3f9a604f028a329e5560009cc4a3a828bfea5fcba8eb7647d88fe"}, ] markers = {dev = "sys_platform == \"linux\""} [package.dependencies] cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""} typing-extensions = {version = ">=4.13.2", markers = "python_full_version < \"3.11.0\""} [package.extras] docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"] docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] nox = ["nox[uv] (>=2024.4.15)"] pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"] sdist = ["build (>=1.0.0)"] ssh = ["bcrypt (>=3.1.5)"] test = ["certifi (>=2024)", "cryptography-vectors (==46.0.2)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] test-randomorder = ["pytest-randomly"] [[package]] name = "daphne" version = "4.2.1" description = "Django ASGI (HTTP/WebSocket) server" optional = false python-versions = ">=3.9" groups = ["integrations"] files = [ {file = "daphne-4.2.1-py3-none-any.whl", hash = "sha256:881e96b387b95b35ad85acd855f229d7f5b79073d6649089c8a33f661885e055"}, {file = "daphne-4.2.1.tar.gz", hash = "sha256:5f898e700a1fda7addf1541d7c328606415e96a7bd768405f0463c312fcb31b3"}, ] [package.dependencies] asgiref = ">=3.5.2,<4" autobahn = ">=22.4.2" twisted = {version = ">=22.4", extras = ["tls"]} [package.extras] tests = ["black", "django", "flake8", "flake8-bugbear", "hypothesis", "mypy", "pytest", "pytest-asyncio", "pytest-cov", "tox"] [[package]] name = "dill" version = "0.4.0" description = "serialize all of Python" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049"}, {file = "dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0"}, ] [package.extras] graph = ["objgraph (>=1.7.2)"] profile = ["gprof2dot (>=2022.7.29)"] [[package]] name = "distlib" version = "0.4.0" description = "Distribution utilities" optional = false python-versions = "*" groups = ["dev"] files = [ {file = "distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16"}, {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}, ] [[package]] name = "distro" version = "1.9.0" description = "Distro - an OS platform information API" optional = false python-versions = ">=3.6" groups = ["dev"] files = [ {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"}, {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, ] [[package]] name = "django" version = "5.2.7" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.10" groups = ["main", "integrations"] files = [ {file = "django-5.2.7-py3-none-any.whl", hash = "sha256:59a13a6515f787dec9d97a0438cd2efac78c8aca1c80025244b0fe507fe0754b"}, {file = "django-5.2.7.tar.gz", hash = "sha256:e0f6f12e2551b1716a95a63a1366ca91bbcd7be059862c1b18f989b1da356cdd"}, ] [package.dependencies] asgiref = ">=3.8.1" sqlparse = ">=0.3.1" tzdata = {version = "*", markers = "sys_platform == \"win32\""} [package.extras] argon2 = ["argon2-cffi (>=19.1.0)"] bcrypt = ["bcrypt"] [[package]] name = "dnspython" version = "2.8.0" description = "DNS toolkit" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ {file = "dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af"}, {file = "dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f"}, ] [package.extras] dev = ["black (>=25.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "hypercorn (>=0.17.0)", "mypy (>=1.17)", "pylint (>=3)", "pytest (>=8.4)", "pytest-cov (>=6.2.0)", "quart-trio (>=0.12.0)", "sphinx (>=8.2.0)", "sphinx-rtd-theme (>=3.0.0)", "twine (>=6.1.0)", "wheel (>=0.45.0)"] dnssec = ["cryptography (>=45)"] doh = ["h2 (>=4.2.0)", "httpcore (>=1.0.0)", "httpx (>=0.28.0)"] doq = ["aioquic (>=1.2.0)"] idna = ["idna (>=3.10)"] trio = ["trio (>=0.30)"] wmi = ["wmi (>=1.5.1) ; platform_system == \"Windows\""] [[package]] name = "dulwich" version = "0.24.1" description = "Python Git Library" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "dulwich-0.24.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2169c36b7955b40e1ec9b0543301a3dd536718c3b7840959ca70e7ed17397c25"}, {file = "dulwich-0.24.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:d16507ca6d6c2d29d7d942da4cc50fa589d58ab066030992dfa3932de6695062"}, {file = "dulwich-0.24.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:e893b800c72499e21d0160169bac574292626193532c336ffce7617fe02d97db"}, {file = "dulwich-0.24.1-cp310-cp310-win32.whl", hash = "sha256:d7144febcad9e8510ed870a141073b07071390421691285a81cea5b9fa38d888"}, {file = "dulwich-0.24.1-cp310-cp310-win_amd64.whl", hash = "sha256:1d8226ca444c4347e5820b4a0a3a8f91753c0e39e335eee1eaf59d9672356a9c"}, {file = "dulwich-0.24.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:31ad6637322aaafeecc4c884f396ac2d963aadf201deb6422134fa0f8ac9a87a"}, {file = "dulwich-0.24.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:358e4b688f6c1fa5346a8394a2c1ab79ff7126be576b20ffd0f38085ead0df54"}, {file = "dulwich-0.24.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:50f981edd5307475f6f862ccdbe39e8dd01afc17f2ed8ee0e452c3878389b48c"}, {file = "dulwich-0.24.1-cp311-cp311-win32.whl", hash = "sha256:741417a6a029a3230c46ad4725a50440cac852f165706824303d9939cf83770c"}, {file = "dulwich-0.24.1-cp311-cp311-win_amd64.whl", hash = "sha256:816ec4abd152ebd11d05bf25b5d37a4a88c18af59857067ee85d32e43af12b5f"}, {file = "dulwich-0.24.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d227cebcb2082801ab429e196d973315dbe3818904b5c13a22d80a16f5692c9"}, {file = "dulwich-0.24.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5e3c01b8109169aa361842af4987bca672087e3faf38d528ff9f631d1071f523"}, {file = "dulwich-0.24.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ee80d8e9199124974b486d2c83a7e2d4db17ae59682909fa198111d8bb416f50"}, {file = "dulwich-0.24.1-cp312-cp312-win32.whl", hash = "sha256:bef4dccba44edd6d18015f67c9e0d6216f978840cdbe703930e1679e2872c595"}, {file = "dulwich-0.24.1-cp312-cp312-win_amd64.whl", hash = "sha256:83b2fb17bac190cfc6c91e7a94a1d806aa8ce8903aca0e3e54cecb2c3f547a55"}, {file = "dulwich-0.24.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a11ec69fc6604228804ddfc32c85b22bc627eca4cf4ff3f27dbe822e6f29477"}, {file = "dulwich-0.24.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a9800df7238b586b4c38c00432776781bc889cf02d756dcfb8dc0ecb8fc47a33"}, {file = "dulwich-0.24.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3baab4a01aff890e2e6551ccbd33eb2a44173c897f0f027ad3aeab0fb057ec44"}, {file = "dulwich-0.24.1-cp313-cp313-win32.whl", hash = "sha256:b39689aa4d143ba1fb0a687a4eb93d2e630d2c8f940aaa6c6911e9c8dca16e6a"}, {file = "dulwich-0.24.1-cp313-cp313-win_amd64.whl", hash = "sha256:8fca9b863b939b52c5f759d292499f0d21a7bf7f8cbb9fdeb8cdd9511c5bc973"}, {file = "dulwich-0.24.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:62e3ed32e48e2a7e37c5a97071beac43040cd700d0cab7867514a91335916c83"}, {file = "dulwich-0.24.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6d87cc4770a69d547ebbbac535c83af8a5b762d9e5b1c886c40cb457a1b5c2c1"}, {file = "dulwich-0.24.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:10882685a8b03d0de321f277f6c8e6672bb849a378ad9823d482c42bb1ee8ee4"}, {file = "dulwich-0.24.1-cp39-cp39-win32.whl", hash = "sha256:3df7ed03062e47f50675c83a7a1b73e48a95c9b413c2c8fca329b3e9f2700c04"}, {file = "dulwich-0.24.1-cp39-cp39-win_amd64.whl", hash = "sha256:38000f553593e183189e06c6ed51377f106d28a2d98942d81eab9a10daef4663"}, {file = "dulwich-0.24.1-py3-none-any.whl", hash = "sha256:57cc0dc5a21059698ffa4ed9a7272f1040ec48535193df84b0ee6b16bf615676"}, {file = "dulwich-0.24.1.tar.gz", hash = "sha256:e19fd864f10f02bb834bb86167d92dcca1c228451b04458761fc13dabd447758"}, ] [package.dependencies] typing_extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} urllib3 = ">=1.25" [package.extras] colordiff = ["rich"] dev = ["dissolve (>=0.1.1)", "mypy (==1.17.0)", "ruff (==0.12.4)"] fastimport = ["fastimport"] fuzzing = ["atheris"] https = ["urllib3 (>=1.24.1)"] merge = ["merge3"] paramiko = ["paramiko"] pgp = ["gpg"] [[package]] name = "editor" version = "1.6.6" description = "🖋 Open the default text editor 🖋" optional = false python-versions = ">=3.8" groups = ["main", "dev", "integrations"] files = [ {file = "editor-1.6.6-py3-none-any.whl", hash = "sha256:e818e6913f26c2a81eadef503a2741d7cca7f235d20e217274a009ecd5a74abf"}, {file = "editor-1.6.6.tar.gz", hash = "sha256:bb6989e872638cd119db9a4fce284cd8e13c553886a1c044c6b8d8a160c871f8"}, ] [package.dependencies] runs = "*" xmod = "*" [[package]] name = "email-validator" version = "2.3.0" description = "A robust email address syntax and deliverability validation library." optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4"}, {file = "email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426"}, ] [package.dependencies] dnspython = ">=2.0.0" idna = ">=2.0.0" [[package]] name = "exceptiongroup" version = "1.3.0" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" groups = ["main", "dev", "integrations"] markers = "python_version == \"3.10\"" files = [ {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, ] [package.dependencies] typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} [package.extras] test = ["pytest (>=6)"] [[package]] name = "execnet" version = "2.1.1" description = "execnet: rapid multi-Python deployment" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc"}, {file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"}, ] [package.extras] testing = ["hatch", "pre-commit", "pytest", "tox"] [[package]] name = "executing" version = "2.2.1" description = "Get the currently executing AST node of a frame, and other information" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017"}, {file = "executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4"}, ] [package.extras] tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich ; python_version >= \"3.11\""] [[package]] name = "faker" version = "37.11.0" description = "Faker is a Python package that generates fake data for you." optional = false python-versions = ">=3.9" groups = ["main", "integrations"] files = [ {file = "faker-37.11.0-py3-none-any.whl", hash = "sha256:1508d2da94dfd1e0087b36f386126d84f8583b3de19ac18e392a2831a6676c57"}, {file = "faker-37.11.0.tar.gz", hash = "sha256:22969803849ba0618be8eee2dd01d0d9e2cd3b75e6ff1a291fa9abcdb34da5e6"}, ] [package.dependencies] tzdata = "*" [[package]] name = "fastapi" version = "0.118.0" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.8" groups = ["main", "integrations"] files = [ {file = "fastapi-0.118.0-py3-none-any.whl", hash = "sha256:705137a61e2ef71019d2445b123aa8845bd97273c395b744d5a7dfe559056855"}, {file = "fastapi-0.118.0.tar.gz", hash = "sha256:5e81654d98c4d2f53790a7d32d25a7353b30c81441be7d0958a26b5d761fa1c8"}, ] [package.dependencies] pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" starlette = ">=0.40.0,<0.49.0" typing-extensions = ">=4.8.0" [package.extras] all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] [[package]] name = "fastjsonschema" version = "2.21.2" description = "Fastest Python implementation of JSON schema" optional = false python-versions = "*" groups = ["dev"] files = [ {file = "fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463"}, {file = "fastjsonschema-2.21.2.tar.gz", hash = "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de"}, ] [package.extras] devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benchmark", "pytest-cache", "validictory"] [[package]] name = "filelock" version = "3.19.1" description = "A platform independent file lock." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d"}, {file = "filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58"}, ] [[package]] name = "findpython" version = "0.7.0" description = "A utility to find python versions on your system" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "findpython-0.7.0-py3-none-any.whl", hash = "sha256:f53cfcc29536f5b83c962cf922bba8ff6d6a3c2a05fda6a45aa58a47d005d8fc"}, {file = "findpython-0.7.0.tar.gz", hash = "sha256:8b31647c76352779a3c1a0806699b68e6a7bdc0b5c2ddd9af2a07a0d40c673dc"}, ] [package.dependencies] packaging = ">=20" platformdirs = ">=4.3.6" [[package]] name = "flask" version = "3.1.2" description = "A simple framework for building complex web applications." optional = false python-versions = ">=3.9" groups = ["main", "integrations"] files = [ {file = "flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c"}, {file = "flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87"}, ] [package.dependencies] blinker = ">=1.9.0" click = ">=8.1.3" itsdangerous = ">=2.2.0" jinja2 = ">=3.1.2" markupsafe = ">=2.1.1" werkzeug = ">=3.1.0" [package.extras] async = ["asgiref (>=3.2)"] dotenv = ["python-dotenv"] [[package]] name = "freezegun" version = "1.5.5" description = "Let your Python tests travel through time" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "freezegun-1.5.5-py3-none-any.whl", hash = "sha256:cd557f4a75cf074e84bc374249b9dd491eaeacd61376b9eb3c423282211619d2"}, {file = "freezegun-1.5.5.tar.gz", hash = "sha256:ac7742a6cc6c25a2c35e9292dfd554b897b517d2dec26891a2e8debf205cb94a"}, ] [package.dependencies] python-dateutil = ">=2.7" [[package]] name = "frozenlist" version = "1.8.0" description = "A list-like structure which implements collections.abc.MutableSequence" optional = false python-versions = ">=3.9" groups = ["main", "integrations"] files = [ {file = "frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011"}, {file = "frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565"}, {file = "frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad"}, {file = "frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2"}, {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186"}, {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e"}, {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450"}, {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef"}, {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4"}, {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff"}, {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c"}, {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f"}, {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7"}, {file = "frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a"}, {file = "frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6"}, {file = "frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e"}, {file = "frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84"}, {file = "frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9"}, {file = "frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93"}, {file = "frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f"}, {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695"}, {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52"}, {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581"}, {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567"}, {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b"}, {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92"}, {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d"}, {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd"}, {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967"}, {file = "frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25"}, {file = "frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b"}, {file = "frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a"}, {file = "frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1"}, {file = "frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b"}, {file = "frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4"}, {file = "frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383"}, {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4"}, {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8"}, {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b"}, {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52"}, {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29"}, {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3"}, {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143"}, {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608"}, {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa"}, {file = "frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf"}, {file = "frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746"}, {file = "frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd"}, {file = "frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a"}, {file = "frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7"}, {file = "frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40"}, {file = "frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027"}, {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822"}, {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121"}, {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5"}, {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e"}, {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11"}, {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1"}, {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1"}, {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8"}, {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed"}, {file = "frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496"}, {file = "frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231"}, {file = "frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62"}, {file = "frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94"}, {file = "frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c"}, {file = "frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52"}, {file = "frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51"}, {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65"}, {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82"}, {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714"}, {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d"}, {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506"}, {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51"}, {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e"}, {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0"}, {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41"}, {file = "frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b"}, {file = "frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888"}, {file = "frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042"}, {file = "frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0"}, {file = "frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f"}, {file = "frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c"}, {file = "frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2"}, {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8"}, {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686"}, {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e"}, {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a"}, {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128"}, {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f"}, {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7"}, {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30"}, {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7"}, {file = "frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806"}, {file = "frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0"}, {file = "frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b"}, {file = "frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d"}, {file = "frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed"}, {file = "frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930"}, {file = "frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c"}, {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24"}, {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37"}, {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a"}, {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2"}, {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef"}, {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe"}, {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8"}, {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a"}, {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e"}, {file = "frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df"}, {file = "frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd"}, {file = "frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79"}, {file = "frozenlist-1.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d8b7138e5cd0647e4523d6685b0eac5d4be9a184ae9634492f25c6eb38c12a47"}, {file = "frozenlist-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a6483e309ca809f1efd154b4d37dc6d9f61037d6c6a81c2dc7a15cb22c8c5dca"}, {file = "frozenlist-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1b9290cf81e95e93fdf90548ce9d3c1211cf574b8e3f4b3b7cb0537cf2227068"}, {file = "frozenlist-1.8.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59a6a5876ca59d1b63af8cd5e7ffffb024c3dc1e9cf9301b21a2e76286505c95"}, {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6dc4126390929823e2d2d9dc79ab4046ed74680360fc5f38b585c12c66cdf459"}, {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:332db6b2563333c5671fecacd085141b5800cb866be16d5e3eb15a2086476675"}, {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ff15928d62a0b80bb875655c39bf517938c7d589554cbd2669be42d97c2cb61"}, {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7bf6cdf8e07c8151fba6fe85735441240ec7f619f935a5205953d58009aef8c6"}, {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:48e6d3f4ec5c7273dfe83ff27c91083c6c9065af655dc2684d2c200c94308bb5"}, {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:1a7607e17ad33361677adcd1443edf6f5da0ce5e5377b798fba20fae194825f3"}, {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3a935c3a4e89c733303a2d5a7c257ea44af3a56c8202df486b7f5de40f37e1"}, {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:940d4a017dbfed9daf46a3b086e1d2167e7012ee297fef9e1c545c4d022f5178"}, {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b9be22a69a014bc47e78072d0ecae716f5eb56c15238acca0f43d6eb8e4a5bda"}, {file = "frozenlist-1.8.0-cp39-cp39-win32.whl", hash = "sha256:1aa77cb5697069af47472e39612976ed05343ff2e84a3dcf15437b232cbfd087"}, {file = "frozenlist-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:7398c222d1d405e796970320036b1b563892b65809d9e5261487bb2c7f7b5c6a"}, {file = "frozenlist-1.8.0-cp39-cp39-win_arm64.whl", hash = "sha256:b4f3b365f31c6cd4af24545ca0a244a53688cad8834e32f56831c4923b50a103"}, {file = "frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d"}, {file = "frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad"}, ] [[package]] name = "gitdb" version = "4.0.12" description = "Git Object Database" optional = false python-versions = ">=3.7" groups = ["dev"] files = [ {file = "gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf"}, {file = "gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571"}, ] [package.dependencies] smmap = ">=3.0.1,<6" [[package]] name = "gitpython" version = "3.1.45" description = "GitPython is a Python library used to interact with Git repositories" optional = false python-versions = ">=3.7" groups = ["dev"] files = [ {file = "gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77"}, {file = "gitpython-3.1.45.tar.gz", hash = "sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c"}, ] [package.dependencies] gitdb = ">=4.0.1,<5" [package.extras] doc = ["sphinx (>=7.1.2,<7.2)", "sphinx-autodoc-typehints", "sphinx_rtd_theme"] test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock ; python_version < \"3.8\"", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions ; python_version < \"3.11\""] [[package]] name = "graphql-core" version = "3.2.6" description = "GraphQL implementation for Python, a port of GraphQL.js, the JavaScript reference implementation for GraphQL." optional = false python-versions = "<4,>=3.6" groups = ["main"] files = [ {file = "graphql_core-3.2.6-py3-none-any.whl", hash = "sha256:78b016718c161a6fb20a7d97bbf107f331cd1afe53e45566c59f776ed7f0b45f"}, {file = "graphql_core-3.2.6.tar.gz", hash = "sha256:c08eec22f9e40f0bd61d805907e3b3b1b9a320bc606e23dc145eebca07c8fbab"}, ] [[package]] name = "h11" version = "0.16.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.8" groups = ["main", "dev", "integrations"] files = [ {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, ] [[package]] name = "h2" version = "4.3.0" description = "Pure-Python HTTP/2 protocol implementation" optional = false python-versions = ">=3.9" groups = ["main", "integrations"] files = [ {file = "h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd"}, {file = "h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1"}, ] [package.dependencies] hpack = ">=4.1,<5" hyperframe = ">=6.1,<7" [[package]] name = "hpack" version = "4.1.0" description = "Pure-Python HPACK header encoding" optional = false python-versions = ">=3.9" groups = ["main", "integrations"] files = [ {file = "hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496"}, {file = "hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca"}, ] [[package]] name = "html5tagger" version = "1.3.0" description = "Pythonic HTML generation/templating (no template files)" optional = false python-versions = ">=3.7" groups = ["main", "integrations"] files = [ {file = "html5tagger-1.3.0-py3-none-any.whl", hash = "sha256:ce14313515edffec8ed8a36c5890d023922641171b4e6e5774ad1a74998f5351"}, {file = "html5tagger-1.3.0.tar.gz", hash = "sha256:84fa3dfb49e5c83b79bbd856ab7b1de8e2311c3bb46a8be925f119e3880a8da9"}, ] [[package]] name = "httpcore" version = "1.0.9" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" groups = ["main", "dev", "integrations"] files = [ {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, ] [package.dependencies] certifi = "*" h11 = ">=0.16" [package.extras] asyncio = ["anyio (>=4.0,<5.0)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] trio = ["trio (>=0.22.0,<1.0)"] [[package]] name = "httptools" version = "0.6.4" description = "A collection of framework independent HTTP protocol utils." optional = false python-versions = ">=3.8.0" groups = ["main", "integrations"] files = [ {file = "httptools-0.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0"}, {file = "httptools-0.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da"}, {file = "httptools-0.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deee0e3343f98ee8047e9f4c5bc7cedbf69f5734454a94c38ee829fb2d5fa3c1"}, {file = "httptools-0.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca80b7485c76f768a3bc83ea58373f8db7b015551117375e4918e2aa77ea9b50"}, {file = "httptools-0.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:90d96a385fa941283ebd231464045187a31ad932ebfa541be8edf5b3c2328959"}, {file = "httptools-0.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:59e724f8b332319e2875efd360e61ac07f33b492889284a3e05e6d13746876f4"}, {file = "httptools-0.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:c26f313951f6e26147833fc923f78f95604bbec812a43e5ee37f26dc9e5a686c"}, {file = "httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069"}, {file = "httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a"}, {file = "httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975"}, {file = "httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636"}, {file = "httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721"}, {file = "httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988"}, {file = "httptools-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17"}, {file = "httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2"}, {file = "httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44"}, {file = "httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1"}, {file = "httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2"}, {file = "httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81"}, {file = "httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f"}, {file = "httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970"}, {file = "httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660"}, {file = "httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083"}, {file = "httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3"}, {file = "httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071"}, {file = "httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5"}, {file = "httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0"}, {file = "httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8"}, {file = "httptools-0.6.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d3f0d369e7ffbe59c4b6116a44d6a8eb4783aae027f2c0b366cf0aa964185dba"}, {file = "httptools-0.6.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:94978a49b8f4569ad607cd4946b759d90b285e39c0d4640c6b36ca7a3ddf2efc"}, {file = "httptools-0.6.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40dc6a8e399e15ea525305a2ddba998b0af5caa2566bcd79dcbe8948181eeaff"}, {file = "httptools-0.6.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab9ba8dcf59de5181f6be44a77458e45a578fc99c31510b8c65b7d5acc3cf490"}, {file = "httptools-0.6.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:fc411e1c0a7dcd2f902c7c48cf079947a7e65b5485dea9decb82b9105ca71a43"}, {file = "httptools-0.6.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:d54efd20338ac52ba31e7da78e4a72570cf729fac82bc31ff9199bedf1dc7440"}, {file = "httptools-0.6.4-cp38-cp38-win_amd64.whl", hash = "sha256:df959752a0c2748a65ab5387d08287abf6779ae9165916fe053e68ae1fbdc47f"}, {file = "httptools-0.6.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:85797e37e8eeaa5439d33e556662cc370e474445d5fab24dcadc65a8ffb04003"}, {file = "httptools-0.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:db353d22843cf1028f43c3651581e4bb49374d85692a85f95f7b9a130e1b2cab"}, {file = "httptools-0.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1ffd262a73d7c28424252381a5b854c19d9de5f56f075445d33919a637e3547"}, {file = "httptools-0.6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:703c346571fa50d2e9856a37d7cd9435a25e7fd15e236c397bf224afaa355fe9"}, {file = "httptools-0.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aafe0f1918ed07b67c1e838f950b1c1fabc683030477e60b335649b8020e1076"}, {file = "httptools-0.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0e563e54979e97b6d13f1bbc05a96109923e76b901f786a5eae36e99c01237bd"}, {file = "httptools-0.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:b799de31416ecc589ad79dd85a0b2657a8fe39327944998dea368c1d4c9e55e6"}, {file = "httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c"}, ] [package.extras] test = ["Cython (>=0.29.24)"] [[package]] name = "httpx" version = "0.28.1" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" groups = ["main", "dev", "integrations"] files = [ {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, ] [package.dependencies] anyio = "*" certifi = "*" httpcore = "==1.*" idna = "*" [package.extras] brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] zstd = ["zstandard (>=0.18.0)"] [[package]] name = "humanize" version = "4.13.0" description = "Python humanize utilities" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "humanize-4.13.0-py3-none-any.whl", hash = "sha256:b810820b31891813b1673e8fec7f1ed3312061eab2f26e3fa192c393d11ed25f"}, {file = "humanize-4.13.0.tar.gz", hash = "sha256:78f79e68f76f0b04d711c4e55d32bebef5be387148862cb1ef83d2b58e7935a0"}, ] [package.extras] tests = ["freezegun", "pytest", "pytest-cov"] [[package]] name = "hypercorn" version = "0.17.3" description = "A ASGI Server based on Hyper libraries and inspired by Gunicorn" optional = false python-versions = ">=3.8" groups = ["main", "integrations"] files = [ {file = "hypercorn-0.17.3-py3-none-any.whl", hash = "sha256:059215dec34537f9d40a69258d323f56344805efb462959e727152b0aa504547"}, {file = "hypercorn-0.17.3.tar.gz", hash = "sha256:1b37802ee3ac52d2d85270700d565787ab16cf19e1462ccfa9f089ca17574165"}, ] [package.dependencies] exceptiongroup = {version = ">=1.1.0", markers = "python_version < \"3.11\""} h11 = "*" h2 = ">=3.1.0" priority = "*" taskgroup = {version = "*", markers = "python_version < \"3.11\""} tomli = {version = "*", markers = "python_version < \"3.11\""} typing_extensions = {version = "*", markers = "python_version < \"3.11\""} wsproto = ">=0.14.0" [package.extras] docs = ["pydata_sphinx_theme", "sphinxcontrib_mermaid"] h3 = ["aioquic (>=0.9.0,<1.0)"] trio = ["trio (>=0.22.0)"] uvloop = ["uvloop (>=0.18) ; platform_system != \"Windows\""] [[package]] name = "hyperframe" version = "6.1.0" description = "Pure-Python HTTP/2 framing" optional = false python-versions = ">=3.9" groups = ["main", "integrations"] files = [ {file = "hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5"}, {file = "hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08"}, ] [[package]] name = "hyperlink" version = "21.0.0" description = "A featureful, immutable, and correct URL for Python." optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" groups = ["integrations"] files = [ {file = "hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4"}, {file = "hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b"}, ] [package.dependencies] idna = ">=2.5" [[package]] name = "identify" version = "2.6.15" description = "File identification library for Python" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757"}, {file = "identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf"}, ] [package.extras] license = ["ukkonen"] [[package]] name = "idna" version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" groups = ["main", "dev", "integrations"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, ] [package.extras] all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] [[package]] name = "importlib-metadata" version = "8.7.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ {file = "importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd"}, {file = "importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000"}, ] [package.dependencies] zipp = ">=3.20" [package.extras] check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] perf = ["ipython"] test = ["flufl.flake8", "importlib_resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] type = ["pytest-mypy"] [[package]] name = "importlib-resources" version = "6.5.2" description = "Read resources from Python packages" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec"}, {file = "importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c"}, ] [package.extras] check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] test = ["jaraco.test (>=5.4)", "pytest (>=6,!=8.1.*)", "zipp (>=3.17)"] type = ["pytest-mypy"] [[package]] name = "incremental" version = "24.7.2" description = "A small library that versions your Python projects." optional = false python-versions = ">=3.8" groups = ["integrations"] files = [ {file = "incremental-24.7.2-py3-none-any.whl", hash = "sha256:8cb2c3431530bec48ad70513931a760f446ad6c25e8333ca5d95e24b0ed7b8fe"}, {file = "incremental-24.7.2.tar.gz", hash = "sha256:fb4f1d47ee60efe87d4f6f0ebb5f70b9760db2b2574c59c8e8912be4ebd464c9"}, ] [package.dependencies] setuptools = ">=61.0" tomli = {version = "*", markers = "python_version < \"3.11\""} [package.extras] scripts = ["click (>=6.0)"] [[package]] name = "iniconfig" version = "2.1.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.8" groups = ["dev", "integrations"] files = [ {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, ] [[package]] name = "inline-snapshot" version = "0.10.2" description = "golden master/snapshot/approval testing library which puts the values right into your source code" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "inline_snapshot-0.10.2-py3-none-any.whl", hash = "sha256:f61d42f0d4bddd2a3efae041f5b168e94ac2df566cbf2c67a26d03d5f090835a"}, {file = "inline_snapshot-0.10.2.tar.gz", hash = "sha256:fb3c1410a08c9700ca838a269f70117760b024d99d6193661a8b47f8302b09cd"}, ] [package.dependencies] asttokens = ">=2.0.5" black = ">=23.3.0" click = ">=8.1.4" executing = ">=2.0.0" rich = ">=13.7.1" toml = ">=0.10.2" types-toml = ">=0.10.8.7" [[package]] name = "inquirer" version = "3.4.1" description = "Collection of common interactive command line user interfaces, based on Inquirer.js" optional = false python-versions = ">=3.9.2" groups = ["main", "dev", "integrations"] files = [ {file = "inquirer-3.4.1-py3-none-any.whl", hash = "sha256:717bf146d547b595d2495e7285fd55545cff85e5ce01decc7487d2ec6a605412"}, {file = "inquirer-3.4.1.tar.gz", hash = "sha256:60d169fddffe297e2f8ad54ab33698249ccfc3fc377dafb1e5cf01a0efb9cbe5"}, ] [package.dependencies] blessed = ">=1.19.0" editor = ">=1.6.0" readchar = ">=4.2.0" [[package]] name = "installer" version = "0.7.0" description = "A library for installing Python wheels." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ {file = "installer-0.7.0-py3-none-any.whl", hash = "sha256:05d1933f0a5ba7d8d6296bb6d5018e7c94fa473ceb10cf198a92ccea19c27b53"}, {file = "installer-0.7.0.tar.gz", hash = "sha256:a26d3e3116289bb08216e0d0f7d925fcef0b0194eedfa0c944bcaaa106c4b631"}, ] [[package]] name = "isort" version = "6.1.0" description = "A Python utility / library to sort Python imports." optional = false python-versions = ">=3.9.0" groups = ["dev"] files = [ {file = "isort-6.1.0-py3-none-any.whl", hash = "sha256:58d8927ecce74e5087aef019f778d4081a3b6c98f15a80ba35782ca8a2097784"}, {file = "isort-6.1.0.tar.gz", hash = "sha256:9b8f96a14cfee0677e78e941ff62f03769a06d412aabb9e2a90487b3b7e8d481"}, ] [package.extras] colors = ["colorama"] plugins = ["setuptools"] [[package]] name = "itsdangerous" version = "2.2.0" description = "Safely pass data to untrusted environments and back." optional = false python-versions = ">=3.8" groups = ["main", "integrations"] files = [ {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, ] [[package]] name = "jaraco-classes" version = "3.4.0" description = "Utility functions for Python class constructs" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790"}, {file = "jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd"}, ] [package.dependencies] more-itertools = "*" [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [[package]] name = "jaraco-context" version = "6.0.1" description = "Useful decorators and context managers" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4"}, {file = "jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3"}, ] [package.dependencies] "backports.tarfile" = {version = "*", markers = "python_version < \"3.12\""} [package.extras] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] test = ["portend", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] [[package]] name = "jaraco-functools" version = "4.3.0" description = "Functools like those found in stdlib" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "jaraco_functools-4.3.0-py3-none-any.whl", hash = "sha256:227ff8ed6f7b8f62c56deff101545fa7543cf2c8e7b82a7c2116e672f29c26e8"}, {file = "jaraco_functools-4.3.0.tar.gz", hash = "sha256:cfd13ad0dd2c47a3600b439ef72d8615d482cedcff1632930d6f28924d92f294"}, ] [package.dependencies] more_itertools = "*" [package.extras] check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] test = ["jaraco.classes", "pytest (>=6,!=8.1.*)"] type = ["pytest-mypy"] [[package]] name = "jedi" version = "0.19.2" description = "An autocompletion tool for Python that can be used for text editors." optional = false python-versions = ">=3.6" groups = ["dev"] files = [ {file = "jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9"}, {file = "jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0"}, ] [package.dependencies] parso = ">=0.8.4,<0.9.0" [package.extras] docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] testing = ["Django", "attrs", "colorama", "docopt", "pytest (<9.0.0)"] [[package]] name = "jeepney" version = "0.9.0" description = "Low-level, pure Python DBus protocol wrapper." optional = false python-versions = ">=3.7" groups = ["dev"] markers = "sys_platform == \"linux\"" files = [ {file = "jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683"}, {file = "jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732"}, ] [package.extras] test = ["async-timeout ; python_version < \"3.11\"", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] trio = ["trio"] [[package]] name = "jinja2" version = "3.1.6" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" groups = ["main", "integrations"] files = [ {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, ] [package.dependencies] MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] [[package]] name = "jinxed" version = "1.3.0" description = "Jinxed Terminal Library" optional = false python-versions = "*" groups = ["main", "dev", "integrations"] markers = "platform_system == \"Windows\"" files = [ {file = "jinxed-1.3.0-py2.py3-none-any.whl", hash = "sha256:b993189f39dc2d7504d802152671535b06d380b26d78070559551cbf92df4fc5"}, {file = "jinxed-1.3.0.tar.gz", hash = "sha256:1593124b18a41b7a3da3b078471442e51dbad3d77b4d4f2b0c26ab6f7d660dbf"}, ] [package.dependencies] ansicon = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "jmespath" version = "1.0.1" description = "JSON Matching Expressions" optional = false python-versions = ">=3.7" groups = ["main", "integrations"] files = [ {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, ] [[package]] name = "junitparser" version = "4.0.2" description = "Manipulates JUnit/xUnit Result XML files" optional = false python-versions = ">=3.6" groups = ["dev"] files = [ {file = "junitparser-4.0.2-py3-none-any.whl", hash = "sha256:94c3570e41fcaedc64cc3c634ca99457fe41a84dd1aa8ff74e9e12e66223a155"}, {file = "junitparser-4.0.2.tar.gz", hash = "sha256:d5d07cece6d4a600ff3b7b96c8db5ffa45a91eed695cb86c45c3db113c1ca0f8"}, ] [[package]] name = "keyring" version = "25.6.0" description = "Store and access your passwords safely." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd"}, {file = "keyring-25.6.0.tar.gz", hash = "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66"}, ] [package.dependencies] importlib_metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""} "jaraco.classes" = "*" "jaraco.context" = "*" "jaraco.functools" = "*" jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} [package.extras] check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] completion = ["shtab (>=1.1.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] test = ["pyfakefs", "pytest (>=6,!=8.1.*)"] type = ["pygobject-stubs", "pytest-mypy", "shtab", "types-pywin32"] [[package]] name = "lia-web" version = "0.2.3" description = "A library for working with web frameworks" optional = false python-versions = ">=3.9" groups = ["main"] files = [ {file = "lia_web-0.2.3-py3-none-any.whl", hash = "sha256:237c779c943cd4341527fc0adfcc3d8068f992ee051f4ef059b8474ee087f641"}, {file = "lia_web-0.2.3.tar.gz", hash = "sha256:ccc9d24cdc200806ea96a20b22fb68f4759e6becdb901bd36024df7921e848d7"}, ] [package.dependencies] typing-extensions = ">=4.14.0" [[package]] name = "libcst" version = "1.8.5" description = "A concrete syntax tree with AST-like properties for Python 3.0 through 3.13 programs." optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ {file = "libcst-1.8.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:373011a1a995c6201cf76c72ab598cedc27de9a5d665428620610f599bfc5f20"}, {file = "libcst-1.8.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:774df1b40d338d245bb2d4e368ed99feb72a4642984125a5db62a3f4013a6e87"}, {file = "libcst-1.8.5-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:08762c19eaf3d72162150ac0f0e1aa70378a10182ee539b8ecdf55c7f83b7f82"}, {file = "libcst-1.8.5-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:54a50034c29d477fd3ceed2bcc02e17142b354e4039831246c32fde59281d116"}, {file = "libcst-1.8.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:667ec0b245b8fa1e4afaa69ab4640ff124d4f5e7a480196fedde705db69b8c56"}, {file = "libcst-1.8.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3b7e5142768418094fb8f673e107f01cfdfa70b72d6c97749f3619e2e8beacb1"}, {file = "libcst-1.8.5-cp310-cp310-win_amd64.whl", hash = "sha256:4ad060e43bd3ba54b4fefcc5f619fc2480fd5a7dbec6768b598bfe0eb46e3da9"}, {file = "libcst-1.8.5-cp310-cp310-win_arm64.whl", hash = "sha256:985303bbc3c748c8fb71f994b56cc2806385b423acd53f5dd1cc191b3c2df6d3"}, {file = "libcst-1.8.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:dd5a292ce2b6410bc100aeac2b18ba3554fd8a8f6aa0ee6a9238bb4031c521ca"}, {file = "libcst-1.8.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4f43915cd523a6967ba1dfe137627ed3804892005330c3bf53674a2ab4ff3dad"}, {file = "libcst-1.8.5-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9a756bd314b87b87dec9f0f900672c37719645b1c8bb2b53fe37b5b5fe7ee2c2"}, {file = "libcst-1.8.5-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:26e9d5e756447873eeda78441fa7d1fe640c0b526e5be2b6b7ee0c8f03c4665f"}, {file = "libcst-1.8.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b5b33ec61f62ff6122dc9c5bf1401bc8a9f9a2f0663ca15661d21d14d9dc4de0"}, {file = "libcst-1.8.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a80e14836ecbdf5374c2c82cd5cd290abaa7290ecfafe9259d0615a1ebccb30c"}, {file = "libcst-1.8.5-cp311-cp311-win_amd64.whl", hash = "sha256:588acde1588544b3bfe06069c118ee731e6712f323f26a026733f0ec4512596e"}, {file = "libcst-1.8.5-cp311-cp311-win_arm64.whl", hash = "sha256:a8146f945f1eb46406fab676f86de3b7f88aca9e5d421f6366f7a63c8a950254"}, {file = "libcst-1.8.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c7733aba7b43239157661207b1e3a9f3711a7fc061a0eca6a33f0716fdfd21"}, {file = "libcst-1.8.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b8c3cfbbf6049e3c587713652e4b3c88cfbf7df7878b2eeefaa8dd20a48dc607"}, {file = "libcst-1.8.5-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:31d86025d8997c853f85c4b5d494f04a157fb962e24f187b4af70c7755c9b27d"}, {file = "libcst-1.8.5-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ff9c535cfe99f0be79ac3024772b288570751fc69fc472b44fca12d1912d1561"}, {file = "libcst-1.8.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e8204607504563d3606bbaea2b9b04e0cef2b3bdc14c89171a702c1e09b9318a"}, {file = "libcst-1.8.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5e6cd3df72d47701b205fa3349ba8899566df82cef248c2fdf5f575d640419c4"}, {file = "libcst-1.8.5-cp312-cp312-win_amd64.whl", hash = "sha256:197c2f86dd0ca5c6464184ddef7f6440d64c8da39b78d16fc053da6701ed1209"}, {file = "libcst-1.8.5-cp312-cp312-win_arm64.whl", hash = "sha256:c5ca109c9a81dff3d947dceba635a08f9c3dfeb7f61b0b824a175ef0a98ea69b"}, {file = "libcst-1.8.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9e9563dcd754b65557ba9cdff9a5af32cfa5f007be0db982429580db45bfe"}, {file = "libcst-1.8.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:61d56839d237e9bf3310e6479ffaf6659f298940f0e0d2460ce71ee67a5375df"}, {file = "libcst-1.8.5-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:b084769dcda2036265fc426eec5894c658af8d4b0e0d0255ab6bb78c8c9d6eb4"}, {file = "libcst-1.8.5-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:c20384b8a4a7801b4416ef96173f1fbb7fafad7529edfdf151811ef70423118a"}, {file = "libcst-1.8.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:271b0b363972ff7d2b8116add13977e7c3b2668c7a424095851d548d222dab18"}, {file = "libcst-1.8.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0ba728c7aee73b330f49f2df0f0b56b74c95302eeb78860f8d5ff0e0fc52c887"}, {file = "libcst-1.8.5-cp313-cp313-win_amd64.whl", hash = "sha256:0abf0e87570cd3b06a8cafbb5378a9d1cbf12e4583dc35e0fff2255100da55a1"}, {file = "libcst-1.8.5-cp313-cp313-win_arm64.whl", hash = "sha256:757390c3cf0b45d7ae1d1d4070c839b082926e762e65eab144f37a63ad33b939"}, {file = "libcst-1.8.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f8934763389cd21ce3ed229b63b994b79dac8be7e84a9da144823f46bc1ffc5c"}, {file = "libcst-1.8.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b873caf04862b6649a2a961fce847f7515ba882be02376a924732cf82c160861"}, {file = "libcst-1.8.5-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:50e095d18c4f76da0e03f25c50b52a2999acbcbe4598a3cf41842ee3c13b54f1"}, {file = "libcst-1.8.5-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:3a3c967725cc3e8fa5c7251188d57d48eec8835f44c6b53f7523992bec595fa0"}, {file = "libcst-1.8.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eed454ab77f4b18100c41d8973b57069e503943ea4e5e5bbb660404976a0fe7a"}, {file = "libcst-1.8.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:39130e59868b8fa49f6eeedd46f008d3456fc13ded57e1c85b211636eb6425f3"}, {file = "libcst-1.8.5-cp313-cp313t-win_amd64.whl", hash = "sha256:a7b1cc3abfdba5ce36907f94f07e079528d4be52c07dfffa26f0e68eb1d25d45"}, {file = "libcst-1.8.5-cp313-cp313t-win_arm64.whl", hash = "sha256:20354c4217e87afea936e9ea90c57fe0b2c5651f41b3ee59f5df8a53ab417746"}, {file = "libcst-1.8.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f350ff2867b3075ba97a022de694f2747c469c25099216cef47b58caaee96314"}, {file = "libcst-1.8.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b95db09d04d125619a63f191c9534853656c4c76c303b8b4c5f950c8e610fba"}, {file = "libcst-1.8.5-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:60e62e966b45b7dee6f0ec0fd7687704d29be18ae670c5bc6c9c61a12ccf589f"}, {file = "libcst-1.8.5-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:7cbb330a352dde570059c73af7b7bbfaa84ae121f54d2ce46c5530351f57419d"}, {file = "libcst-1.8.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:71b2b1ef2305cba051252342a1a4f8e94e6b8e95d7693a7c15a00ce8849ef722"}, {file = "libcst-1.8.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0f504d06dfba909d1ba6a4acf60bfe3f22275444d6e0d07e472a5da4a209b0be"}, {file = "libcst-1.8.5-cp314-cp314-win_amd64.whl", hash = "sha256:c69d2b39e360dea5490ccb5dcf5957dcbb1067d27dc1f3f0787d4e287f7744e2"}, {file = "libcst-1.8.5-cp314-cp314-win_arm64.whl", hash = "sha256:63405cb548b2d7b78531535a7819231e633b13d3dee3eb672d58f0f3322892ca"}, {file = "libcst-1.8.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8a5921105610f35921cc4db6fa5e68e941c6da20ce7f9f93b41b6c66b5481353"}, {file = "libcst-1.8.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:abded10e8d92462fa982d19b064c6f24ed7ead81cf3c3b71011e9764cb12923d"}, {file = "libcst-1.8.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dd7bdb14545c4b77a6c0eb39c86a76441fe833da800f6ca63e917e1273621029"}, {file = "libcst-1.8.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6dc28d33ab8750a84c28b5625f7916846ecbecefd89bf75a5292a35644b6efbd"}, {file = "libcst-1.8.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:970b7164a71c65e13c961965f9677bbbbeb21ce2e7e6655294f7f774156391c4"}, {file = "libcst-1.8.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd74c543770e6a61dcb8846c9689dfcce2ad686658896f77f3e21b6ce94bcb2e"}, {file = "libcst-1.8.5-cp314-cp314t-win_amd64.whl", hash = "sha256:3d8e80cd1ed6577166f0bab77357f819f12564c2ed82307612e2bcc93e684d72"}, {file = "libcst-1.8.5-cp314-cp314t-win_arm64.whl", hash = "sha256:a026aaa19cb2acd8a4d9e2a215598b0a7e2c194bf4482eb9dec4d781ec6e10b2"}, {file = "libcst-1.8.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:62d19557e9ca8c4d4969e4139f6678ee36beacce5a1dddbdb8f891e7fb867e84"}, {file = "libcst-1.8.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cd389a8a1da7cd48f47e72606153548de1a4aae7914c6af6302bcd3095bc592d"}, {file = "libcst-1.8.5-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:b7de38b9b6c24825d028be70ec12745d268a763d2fb89344f65db749be13733f"}, {file = "libcst-1.8.5-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:395aa10f34b91c952098eb69fc461f17fcda4e1dc4ac462c3bdff2d4dfbb92e7"}, {file = "libcst-1.8.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9e431d331f4296090325dc22bc4e9e4a32aff08d51ee31053b7efff16faf87fc"}, {file = "libcst-1.8.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3e409c0784d0950b16555799bfa108199209b7df159d84ebe443fe08aa0ba8f6"}, {file = "libcst-1.8.5-cp39-cp39-win_amd64.whl", hash = "sha256:aaad71a6079eb9ebe84f982bb0ccebd4f5010f5f18c6324690b73efc4427b3fa"}, {file = "libcst-1.8.5-cp39-cp39-win_arm64.whl", hash = "sha256:0ade64fbbeae77b5f2cf0b4fd62afa51c56f51fa026eb1f1627e65ec6d2e38d7"}, {file = "libcst-1.8.5.tar.gz", hash = "sha256:e72e1816eed63f530668e93a4c22ff1cf8b91ddce0ec53e597d3f6c53e103ec7"}, ] [package.dependencies] pyyaml = {version = ">=5.2", markers = "python_version < \"3.13\""} pyyaml-ft = {version = ">=8.0.0", markers = "python_version >= \"3.13\""} [[package]] name = "line-profiler" version = "5.0.0" description = "Line-by-line profiler" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "line_profiler-5.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5cd1621ff77e1f3f423dcc2611ef6fba462e791ce01fb41c95dce6d519c48ec8"}, {file = "line_profiler-5.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:17a44491d16309bc39fc6197b376a120ebc52adc3f50b0b6f9baf99af3124406"}, {file = "line_profiler-5.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a36a9a5ea5e37b0969a451f922b4dbb109350981187317f708694b3b5ceac3a5"}, {file = "line_profiler-5.0.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b67e6e292efaf85d9678fe29295b46efd72c0d363b38e6b424df39b6553c49b3"}, {file = "line_profiler-5.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9c92c28ee16bf3ba99966854407e4bc927473a925c1629489c8ebc01f8a640"}, {file = "line_profiler-5.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:51609cc264df6315cd9b9fa76d822a7b73a4f278dcab90ba907e32dc939ab1c2"}, {file = "line_profiler-5.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:67f9721281655dc2b6763728a63928e3b8a35dfd6160c628a3c599afd0814a71"}, {file = "line_profiler-5.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:c2c27ac0c30d35ca1de5aeebe97e1d9c0d582e3d2c4146c572a648bec8efcfac"}, {file = "line_profiler-5.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f32d536c056393b7ca703e459632edc327ff9e0fc320c7b0e0ed14b84d342b7f"}, {file = "line_profiler-5.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a7da04ffc5a0a1f6653f43b13ad2e7ebf66f1d757174b7e660dfa0cbe74c4fc6"}, {file = "line_profiler-5.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d2746f6b13c19ca4847efd500402d53a5ebb2fe31644ce8af74fbeac5ea4c54c"}, {file = "line_profiler-5.0.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b4290319a59730c04cbd03755472d10524130065a20a695dc10dd66ffd92172"}, {file = "line_profiler-5.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cd168a8af0032e8e3cb2fbb9ffc7694cdcecd47ec356ae863134df07becb3a2"}, {file = "line_profiler-5.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:cbe7b095865d00dda0f53d7d4556c2b1b5d13f723173a85edb206a78779ee07a"}, {file = "line_profiler-5.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ff176045ea8a9e33900856db31b0b979357c337862ae4837140c98bd3161c3c7"}, {file = "line_profiler-5.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:474e0962d02123f1190a804073b308a67ef5f9c3b8379184483d5016844a00df"}, {file = "line_profiler-5.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:729b18c0ac66b3368ade61203459219c202609f76b34190cbb2508b8e13998c8"}, {file = "line_profiler-5.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:438ed24278c428119473b61a473c8fe468ace7c97c94b005cb001137bc624547"}, {file = "line_profiler-5.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:920b0076dca726caadbf29f0bfcce0cbcb4d9ff034cd9445a7308f9d556b4b3a"}, {file = "line_profiler-5.0.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53326eaad2d807487dcd45d2e385feaaed81aaf72b9ecd4f53c1a225d658006f"}, {file = "line_profiler-5.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e3995a989cdea022f0ede5db19a6ab527f818c59ffcebf4e5f7a8be4eb8e880"}, {file = "line_profiler-5.0.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8bf57892a1d3a42273652506746ba9f620c505773ada804367c42e5b4146d6b6"}, {file = "line_profiler-5.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43672085f149f5fbf3f08bba072ad7014dd485282e8665827b26941ea97d2d76"}, {file = "line_profiler-5.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:446bd4f04e4bd9e979d68fdd916103df89a9d419e25bfb92b31af13c33808ee0"}, {file = "line_profiler-5.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9873fabbae1587778a551176758a70a5f6c89d8d070a1aca7a689677d41a1348"}, {file = "line_profiler-5.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2cd6cdb5a4d3b4ced607104dbed73ec820a69018decd1a90904854380536ed32"}, {file = "line_profiler-5.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:34d6172a3bd14167b3ea2e629d71b08683b17b3bc6eb6a4936d74e3669f875b6"}, {file = "line_profiler-5.0.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5edd859be322aa8252253e940ac1c60cca4c385760d90a402072f8f35e4b967"}, {file = "line_profiler-5.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d4f97b223105eed6e525994f5653061bd981e04838ee5d14e01d17c26185094"}, {file = "line_profiler-5.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4758007e491bee3be40ebcca460596e0e28e7f39b735264694a9cafec729dfa9"}, {file = "line_profiler-5.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:213b19c4b65942db5d477e603c18c76126e3811a39d8bab251d930d8ce82ffba"}, {file = "line_profiler-5.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:84c91fdc813e41c7d07ff3d1630a8b9efd54646c144432178f8603424ab06f81"}, {file = "line_profiler-5.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ebaf17814431f429d76166b7c0e57c6e84925f7b57e348f8edfd8e96968f0d73"}, {file = "line_profiler-5.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:412efd162a9ad75d80410e58ba80368f587af854c6b373a152a4f858e15f6102"}, {file = "line_profiler-5.0.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3b05c9177201f02b18a70039e72bcf5a75288abb362e97e17a83f0db334e368"}, {file = "line_profiler-5.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c4d3147aa07caa44e05f44db4e27ca4f5392187c0934f887bdb81d7dc1884c9"}, {file = "line_profiler-5.0.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:6cec60f39d0e72548173bfcd419566221e2c0c6168ecca46678f427a0e21b732"}, {file = "line_profiler-5.0.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:7d14141fe4376510cc192cd828f357bf276b8297fcda00ebac5adbc9235732f4"}, {file = "line_profiler-5.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:64b4ce2506d1dac22f05f51692970ecb89741cb6a15bcb4c00212b2c39610ff1"}, {file = "line_profiler-5.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7ba2142d35a3401d348cb743611bac52ba9db9cf026f8aa82c34d13effb98a71"}, {file = "line_profiler-5.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17724b2dff0edb3a4ac402bef6381060a4c424fbaa170e651306495f7c95bba8"}, {file = "line_profiler-5.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d2315baca21a9be299b5a0a89f2ce4ed5cfd12ba039a82784a298dd106d3621d"}, {file = "line_profiler-5.0.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:febbfc59502984e2cb0deb27cd163ed71847e36bbb82763f2bf3c9432cc440ab"}, {file = "line_profiler-5.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:213dc34b1abdcafff944c13e62f2f1d254fc1cb30740ac0257e4567c8bea9a03"}, {file = "line_profiler-5.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:011ac8167855513cac266d698b34b8ded9c673640d105a715c989fd5f27a298c"}, {file = "line_profiler-5.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4646907f588439845d7739d6a5f10ab08a2f8952d65f61145eeb705e8bb4797e"}, {file = "line_profiler-5.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cb6dced51bf906ddf2a8d75eda3523cee4cfb0102f54610e8f849630341a281"}, {file = "line_profiler-5.0.0.tar.gz", hash = "sha256:a80f0afb05ba0d275d9dddc5ff97eab637471167ff3e66dcc7d135755059398c"}, ] [package.dependencies] tomli = {version = "*", markers = "python_version < \"3.11\""} [package.extras] all = ["Cython (>=3.0.3)", "IPython (>=8.12.2) ; python_version < \"3.9.0\" and python_version >= \"3.8.0\"", "IPython (>=8.14.0) ; python_version < \"4.0.0\" and python_version >= \"3.9.0\"", "cibuildwheel (>=2.11.2) ; python_version < \"4.0\" and python_version >= \"3.11\"", "cibuildwheel (>=2.11.2) ; python_version == \"3.10\"", "cibuildwheel (>=2.11.2) ; python_version == \"3.8\"", "cibuildwheel (>=2.11.2) ; python_version == \"3.9\"", "cmake (>=3.21.2)", "coverage[toml] (>=6.5.0) ; python_version < \"3.12\" and python_version >= \"3.10\"", "coverage[toml] (>=6.5.0) ; python_version == \"3.8\"", "coverage[toml] (>=6.5.0) ; python_version == \"3.9\"", "coverage[toml] (>=7.3.0) ; python_version < \"4.0\" and python_version >= \"3.12\"", "ninja (>=1.10.2)", "pytest (>=7.4.4) ; python_version < \"4.0\" and python_version >= \"3.13\"", "pytest (>=7.4.4) ; python_version == \"3.10\"", "pytest (>=7.4.4) ; python_version == \"3.11\"", "pytest (>=7.4.4) ; python_version == \"3.12\"", "pytest (>=7.4.4) ; python_version == \"3.8\"", "pytest (>=7.4.4) ; python_version == \"3.9\"", "pytest-cov (>=3.0.0)", "rich (>=12.3.0)", "scikit-build (>=0.11.1)", "setuptools (>=68.2.2) ; python_version < \"4.0\" and python_version >= \"3.8\"", "tomli ; python_version < \"3.11\"", "ubelt (>=1.3.4)", "xdoctest (>=1.1.3)"] all-strict = ["Cython (==3.0.3)", "IPython (==8.12.2) ; python_version < \"3.9.0\" and python_version >= \"3.8.0\"", "IPython (==8.14.0) ; python_version < \"4.0.0\" and python_version >= \"3.9.0\"", "cibuildwheel (==2.11.2) ; python_version < \"4.0\" and python_version >= \"3.11\"", "cibuildwheel (==2.11.2) ; python_version == \"3.10\"", "cibuildwheel (==2.11.2) ; python_version == \"3.8\"", "cibuildwheel (==2.11.2) ; python_version == \"3.9\"", "cmake (==3.21.2)", "coverage[toml] (==6.5.0) ; python_version < \"3.12\" and python_version >= \"3.10\"", "coverage[toml] (==6.5.0) ; python_version == \"3.8\"", "coverage[toml] (==6.5.0) ; python_version == \"3.9\"", "coverage[toml] (==7.3.0) ; python_version < \"4.0\" and python_version >= \"3.12\"", "ninja (==1.10.2)", "pytest (==7.4.4) ; python_version < \"4.0\" and python_version >= \"3.13\"", "pytest (==7.4.4) ; python_version == \"3.10\"", "pytest (==7.4.4) ; python_version == \"3.11\"", "pytest (==7.4.4) ; python_version == \"3.12\"", "pytest (==7.4.4) ; python_version == \"3.8\"", "pytest (==7.4.4) ; python_version == \"3.9\"", "pytest-cov (==3.0.0)", "rich (==12.3.0)", "scikit-build (==0.11.1)", "setuptools (==68.2.2) ; python_version < \"4.0\" and python_version >= \"3.8\"", "tomli ; python_version < \"3.11\"", "ubelt (==1.3.4)", "xdoctest (==1.1.3)"] ipython = ["IPython (>=8.12.2) ; python_version < \"3.9.0\" and python_version >= \"3.8.0\"", "IPython (>=8.14.0) ; python_version < \"4.0.0\" and python_version >= \"3.9.0\""] ipython-strict = ["IPython (==8.12.2) ; python_version < \"3.9.0\" and python_version >= \"3.8.0\"", "IPython (==8.14.0) ; python_version < \"4.0.0\" and python_version >= \"3.9.0\""] optional = ["IPython (>=8.12.2) ; python_version < \"3.9.0\" and python_version >= \"3.8.0\"", "IPython (>=8.14.0) ; python_version < \"4.0.0\" and python_version >= \"3.9.0\"", "rich (>=12.3.0)"] optional-strict = ["IPython (==8.12.2) ; python_version < \"3.9.0\" and python_version >= \"3.8.0\"", "IPython (==8.14.0) ; python_version < \"4.0.0\" and python_version >= \"3.9.0\"", "rich (==12.3.0)"] runtime-strict = ["tomli ; python_version < \"3.11\""] tests = ["coverage[toml] (>=6.5.0) ; python_version < \"3.12\" and python_version >= \"3.10\"", "coverage[toml] (>=6.5.0) ; python_version == \"3.8\"", "coverage[toml] (>=6.5.0) ; python_version == \"3.9\"", "coverage[toml] (>=7.3.0) ; python_version < \"4.0\" and python_version >= \"3.12\"", "pytest (>=7.4.4) ; python_version < \"4.0\" and python_version >= \"3.13\"", "pytest (>=7.4.4) ; python_version == \"3.10\"", "pytest (>=7.4.4) ; python_version == \"3.11\"", "pytest (>=7.4.4) ; python_version == \"3.12\"", "pytest (>=7.4.4) ; python_version == \"3.8\"", "pytest (>=7.4.4) ; python_version == \"3.9\"", "pytest-cov (>=3.0.0)", "ubelt (>=1.3.4)", "xdoctest (>=1.1.3)"] tests-strict = ["coverage[toml] (==6.5.0) ; python_version < \"3.12\" and python_version >= \"3.10\"", "coverage[toml] (==6.5.0) ; python_version == \"3.8\"", "coverage[toml] (==6.5.0) ; python_version == \"3.9\"", "coverage[toml] (==7.3.0) ; python_version < \"4.0\" and python_version >= \"3.12\"", "pytest (==7.4.4) ; python_version < \"4.0\" and python_version >= \"3.13\"", "pytest (==7.4.4) ; python_version == \"3.10\"", "pytest (==7.4.4) ; python_version == \"3.11\"", "pytest (==7.4.4) ; python_version == \"3.12\"", "pytest (==7.4.4) ; python_version == \"3.8\"", "pytest (==7.4.4) ; python_version == \"3.9\"", "pytest-cov (==3.0.0)", "ubelt (==1.3.4)", "xdoctest (==1.1.3)"] [[package]] name = "litestar" version = "2.18.0" description = "Litestar - A production-ready, highly performant, extensible ASGI API Framework" optional = false python-versions = "<4.0,>=3.8" groups = ["main", "integrations"] files = [ {file = "litestar-2.18.0-py3-none-any.whl", hash = "sha256:459ec993bafe47245c981d802a0a0c73f47c98313b3c4e47923eebe978f0e511"}, {file = "litestar-2.18.0.tar.gz", hash = "sha256:be8f91813854722b7a2f37cbb57d76977050a96b2427d3c4455d406f0f4fcd50"}, ] [package.dependencies] anyio = ">=3" click = "*" exceptiongroup = {version = ">=1.2.2", markers = "python_version < \"3.11\""} httpx = ">=0.22" litestar-htmx = ">=0.4.0" msgspec = ">=0.18.2" multidict = ">=6.0.2" multipart = ">=1.2.0" polyfactory = ">=2.6.3" pyyaml = "*" rich = ">=13.0.0" rich-click = "*" typing-extensions = "*" [package.extras] annotated-types = ["annotated-types"] attrs = ["attrs"] brotli = ["brotli"] cli = ["jsbeautifier", "uvicorn[standard]", "uvloop (>=0.18.0) ; sys_platform != \"win32\""] cryptography = ["cryptography"] full = ["advanced-alchemy (>=0.2.2)", "annotated-types", "attrs", "brotli", "cryptography", "email-validator", "fast-query-parsers (>=1.0.2)", "jinja2", "jinja2 (>=3.1.2)", "jsbeautifier", "mako (>=1.2.4)", "minijinja (>=1.0.0)", "opentelemetry-instrumentation-asgi", "piccolo", "picologging ; python_version < \"3.13\"", "prometheus-client", "pydantic", "pydantic-extra-types (!=2.9.0) ; python_version < \"3.9\"", "pydantic-extra-types ; python_version >= \"3.9\"", "pyjwt (>=2.9.0)", "redis[hiredis] (>=4.4.4,<5.3)", "structlog", "uvicorn[standard]", "uvloop (>=0.18.0) ; sys_platform != \"win32\"", "valkey[libvalkey] (>=6.0.2)"] jinja = ["jinja2 (>=3.1.2)"] jwt = ["cryptography", "pyjwt (>=2.9.0)"] mako = ["mako (>=1.2.4)"] minijinja = ["minijinja (>=1.0.0)"] opentelemetry = ["opentelemetry-instrumentation-asgi"] piccolo = ["piccolo"] picologging = ["picologging ; python_version < \"3.13\""] prometheus = ["prometheus-client"] pydantic = ["email-validator", "pydantic", "pydantic-extra-types (!=2.9.0) ; python_version < \"3.9\"", "pydantic-extra-types ; python_version >= \"3.9\""] redis = ["redis[hiredis] (>=4.4.4,<5.3)"] sqlalchemy = ["advanced-alchemy (>=0.2.2)"] standard = ["fast-query-parsers (>=1.0.2)", "jinja2", "jsbeautifier", "uvicorn[standard]", "uvloop (>=0.18.0) ; sys_platform != \"win32\""] structlog = ["structlog"] valkey = ["valkey[libvalkey] (>=6.0.2)"] [[package]] name = "litestar-htmx" version = "0.5.0" description = "HTMX Integration for Litestar" optional = false python-versions = "<4.0,>=3.9" groups = ["main", "integrations"] files = [ {file = "litestar_htmx-0.5.0-py3-none-any.whl", hash = "sha256:92833aa47e0d0e868d2a7dbfab75261f124f4b83d4f9ad12b57b9a68f86c50e6"}, {file = "litestar_htmx-0.5.0.tar.gz", hash = "sha256:e02d1a3a92172c874835fa3e6749d65ae9fc626d0df46719490a16293e2146fb"}, ] [[package]] name = "lsprotocol" version = "2023.0.1" description = "Python implementation of the Language Server Protocol." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ {file = "lsprotocol-2023.0.1-py3-none-any.whl", hash = "sha256:c75223c9e4af2f24272b14c6375787438279369236cd568f596d4951052a60f2"}, {file = "lsprotocol-2023.0.1.tar.gz", hash = "sha256:cc5c15130d2403c18b734304339e51242d3018a05c4f7d0f198ad6e0cd21861d"}, ] [package.dependencies] attrs = ">=21.3.0" cattrs = "!=23.2.1" [[package]] name = "lxml" version = "6.0.2" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "lxml-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e77dd455b9a16bbd2a5036a63ddbd479c19572af81b624e79ef422f929eef388"}, {file = "lxml-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d444858b9f07cefff6455b983aea9a67f7462ba1f6cbe4a21e8bf6791bf2153"}, {file = "lxml-6.0.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f952dacaa552f3bb8834908dddd500ba7d508e6ea6eb8c52eb2d28f48ca06a31"}, {file = "lxml-6.0.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:71695772df6acea9f3c0e59e44ba8ac50c4f125217e84aab21074a1a55e7e5c9"}, {file = "lxml-6.0.2-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17f68764f35fd78d7c4cc4ef209a184c38b65440378013d24b8aecd327c3e0c8"}, {file = "lxml-6.0.2-cp310-cp310-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:058027e261afed589eddcfe530fcc6f3402d7fd7e89bfd0532df82ebc1563dba"}, {file = "lxml-6.0.2-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8ffaeec5dfea5881d4c9d8913a32d10cfe3923495386106e4a24d45300ef79c"}, {file = "lxml-6.0.2-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:f2e3b1a6bb38de0bc713edd4d612969dd250ca8b724be8d460001a387507021c"}, {file = "lxml-6.0.2-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d6690ec5ec1cce0385cb20896b16be35247ac8c2046e493d03232f1c2414d321"}, {file = "lxml-6.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2a50c3c1d11cad0ebebbac357a97b26aa79d2bcaf46f256551152aa85d3a4d1"}, {file = "lxml-6.0.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3efe1b21c7801ffa29a1112fab3b0f643628c30472d507f39544fd48e9549e34"}, {file = "lxml-6.0.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:59c45e125140b2c4b33920d21d83681940ca29f0b83f8629ea1a2196dc8cfe6a"}, {file = "lxml-6.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:452b899faa64f1805943ec1c0c9ebeaece01a1af83e130b69cdefeda180bb42c"}, {file = "lxml-6.0.2-cp310-cp310-win32.whl", hash = "sha256:1e786a464c191ca43b133906c6903a7e4d56bef376b75d97ccbb8ec5cf1f0a4b"}, {file = "lxml-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:dacf3c64ef3f7440e3167aa4b49aa9e0fb99e0aa4f9ff03795640bf94531bcb0"}, {file = "lxml-6.0.2-cp310-cp310-win_arm64.whl", hash = "sha256:45f93e6f75123f88d7f0cfd90f2d05f441b808562bf0bc01070a00f53f5028b5"}, {file = "lxml-6.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:13e35cbc684aadf05d8711a5d1b5857c92e5e580efa9a0d2be197199c8def607"}, {file = "lxml-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b1675e096e17c6fe9c0e8c81434f5736c0739ff9ac6123c87c2d452f48fc938"}, {file = "lxml-6.0.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac6e5811ae2870953390452e3476694196f98d447573234592d30488147404d"}, {file = "lxml-6.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5aa0fc67ae19d7a64c3fe725dc9a1bb11f80e01f78289d05c6f62545affec438"}, {file = "lxml-6.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de496365750cc472b4e7902a485d3f152ecf57bd3ba03ddd5578ed8ceb4c5964"}, {file = "lxml-6.0.2-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:200069a593c5e40b8f6fc0d84d86d970ba43138c3e68619ffa234bc9bb806a4d"}, {file = "lxml-6.0.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d2de809c2ee3b888b59f995625385f74629707c9355e0ff856445cdcae682b7"}, {file = "lxml-6.0.2-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:b2c3da8d93cf5db60e8858c17684c47d01fee6405e554fb55018dd85fc23b178"}, {file = "lxml-6.0.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:442de7530296ef5e188373a1ea5789a46ce90c4847e597856570439621d9c553"}, {file = "lxml-6.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2593c77efde7bfea7f6389f1ab249b15ed4aa5bc5cb5131faa3b843c429fbedb"}, {file = "lxml-6.0.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3e3cb08855967a20f553ff32d147e14329b3ae70ced6edc2f282b94afbc74b2a"}, {file = "lxml-6.0.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ed6c667fcbb8c19c6791bbf40b7268ef8ddf5a96940ba9404b9f9a304832f6c"}, {file = "lxml-6.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b8f18914faec94132e5b91e69d76a5c1d7b0c73e2489ea8929c4aaa10b76bbf7"}, {file = "lxml-6.0.2-cp311-cp311-win32.whl", hash = "sha256:6605c604e6daa9e0d7f0a2137bdc47a2e93b59c60a65466353e37f8272f47c46"}, {file = "lxml-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e5867f2651016a3afd8dd2c8238baa66f1e2802f44bc17e236f547ace6647078"}, {file = "lxml-6.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:4197fb2534ee05fd3e7afaab5d8bfd6c2e186f65ea7f9cd6a82809c887bd1285"}, {file = "lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456"}, {file = "lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924"}, {file = "lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f"}, {file = "lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534"}, {file = "lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564"}, {file = "lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f"}, {file = "lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0"}, {file = "lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192"}, {file = "lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0"}, {file = "lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092"}, {file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f"}, {file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8"}, {file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f"}, {file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6"}, {file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322"}, {file = "lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849"}, {file = "lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f"}, {file = "lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6"}, {file = "lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77"}, {file = "lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f"}, {file = "lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452"}, {file = "lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048"}, {file = "lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df"}, {file = "lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1"}, {file = "lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916"}, {file = "lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd"}, {file = "lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6"}, {file = "lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a"}, {file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679"}, {file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659"}, {file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484"}, {file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2"}, {file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314"}, {file = "lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2"}, {file = "lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7"}, {file = "lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf"}, {file = "lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe"}, {file = "lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d"}, {file = "lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d"}, {file = "lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5"}, {file = "lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0"}, {file = "lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba"}, {file = "lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0"}, {file = "lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d"}, {file = "lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37"}, {file = "lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9"}, {file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917"}, {file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f"}, {file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8"}, {file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a"}, {file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c"}, {file = "lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b"}, {file = "lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed"}, {file = "lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8"}, {file = "lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d"}, {file = "lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba"}, {file = "lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601"}, {file = "lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed"}, {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37"}, {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338"}, {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9"}, {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd"}, {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d"}, {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9"}, {file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e"}, {file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d"}, {file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec"}, {file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272"}, {file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f"}, {file = "lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312"}, {file = "lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca"}, {file = "lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c"}, {file = "lxml-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a656ca105115f6b766bba324f23a67914d9c728dafec57638e2b92a9dcd76c62"}, {file = "lxml-6.0.2-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c54d83a2188a10ebdba573f16bd97135d06c9ef60c3dc495315c7a28c80a263f"}, {file = "lxml-6.0.2-cp38-cp38-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:1ea99340b3c729beea786f78c38f60f4795622f36e305d9c9be402201efdc3b7"}, {file = "lxml-6.0.2-cp38-cp38-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:af85529ae8d2a453feee4c780d9406a5e3b17cee0dd75c18bd31adcd584debc3"}, {file = "lxml-6.0.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:fe659f6b5d10fb5a17f00a50eb903eb277a71ee35df4615db573c069bcf967ac"}, {file = "lxml-6.0.2-cp38-cp38-win32.whl", hash = "sha256:5921d924aa5468c939d95c9814fa9f9b5935a6ff4e679e26aaf2951f74043512"}, {file = "lxml-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:0aa7070978f893954008ab73bb9e3c24a7c56c054e00566a21b553dc18105fca"}, {file = "lxml-6.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2c8458c2cdd29589a8367c09c8f030f1d202be673f0ca224ec18590b3b9fb694"}, {file = "lxml-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3fee0851639d06276e6b387f1c190eb9d7f06f7f53514e966b26bae46481ec90"}, {file = "lxml-6.0.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b2142a376b40b6736dfc214fd2902409e9e3857eff554fed2d3c60f097e62a62"}, {file = "lxml-6.0.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6b5b39cc7e2998f968f05309e666103b53e2edd01df8dc51b90d734c0825444"}, {file = "lxml-6.0.2-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4aec24d6b72ee457ec665344a29acb2d35937d5192faebe429ea02633151aad"}, {file = "lxml-6.0.2-cp39-cp39-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:b42f4d86b451c2f9d06ffb4f8bbc776e04df3ba070b9fe2657804b1b40277c48"}, {file = "lxml-6.0.2-cp39-cp39-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cdaefac66e8b8f30e37a9b4768a391e1f8a16a7526d5bc77a7928408ef68e93"}, {file = "lxml-6.0.2-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:b738f7e648735714bbb82bdfd030203360cfeab7f6e8a34772b3c8c8b820568c"}, {file = "lxml-6.0.2-cp39-cp39-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daf42de090d59db025af61ce6bdb2521f0f102ea0e6ea310f13c17610a97da4c"}, {file = "lxml-6.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:66328dabea70b5ba7e53d94aa774b733cf66686535f3bc9250a7aab53a91caaf"}, {file = "lxml-6.0.2-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:e237b807d68a61fc3b1e845407e27e5eb8ef69bc93fe8505337c1acb4ee300b6"}, {file = "lxml-6.0.2-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:ac02dc29fd397608f8eb15ac1610ae2f2f0154b03f631e6d724d9e2ad4ee2c84"}, {file = "lxml-6.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:817ef43a0c0b4a77bd166dc9a09a555394105ff3374777ad41f453526e37f9cb"}, {file = "lxml-6.0.2-cp39-cp39-win32.whl", hash = "sha256:bc532422ff26b304cfb62b328826bd995c96154ffd2bac4544f37dbb95ecaa8f"}, {file = "lxml-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:995e783eb0374c120f528f807443ad5a83a656a8624c467ea73781fc5f8a8304"}, {file = "lxml-6.0.2-cp39-cp39-win_arm64.whl", hash = "sha256:08b9d5e803c2e4725ae9e8559ee880e5328ed61aa0935244e0515d7d9dbec0aa"}, {file = "lxml-6.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e748d4cf8fef2526bb2a589a417eba0c8674e29ffcb570ce2ceca44f1e567bf6"}, {file = "lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4ddb1049fa0579d0cbd00503ad8c58b9ab34d1254c77bc6a5576d96ec7853dba"}, {file = "lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cb233f9c95f83707dae461b12b720c1af9c28c2d19208e1be03387222151daf5"}, {file = "lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc456d04db0515ce3320d714a1eac7a97774ff0849e7718b492d957da4631dd4"}, {file = "lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2613e67de13d619fd283d58bda40bff0ee07739f624ffee8b13b631abf33083d"}, {file = "lxml-6.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:24a8e756c982c001ca8d59e87c80c4d9dcd4d9b44a4cbeb8d9be4482c514d41d"}, {file = "lxml-6.0.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1c06035eafa8404b5cf475bb37a9f6088b0aca288d4ccc9d69389750d5543700"}, {file = "lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c7d13103045de1bdd6fe5d61802565f1a3537d70cd3abf596aa0af62761921ee"}, {file = "lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a3c150a95fbe5ac91de323aa756219ef9cf7fde5a3f00e2281e30f33fa5fa4f"}, {file = "lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60fa43be34f78bebb27812ed90f1925ec99560b0fa1decdb7d12b84d857d31e9"}, {file = "lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21c73b476d3cfe836be731225ec3421fa2f048d84f6df6a8e70433dff1376d5a"}, {file = "lxml-6.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:27220da5be049e936c3aca06f174e8827ca6445a4353a1995584311487fc4e3e"}, {file = "lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62"}, ] [package.extras] cssselect = ["cssselect (>=0.7)"] html-clean = ["lxml_html_clean"] html5 = ["html5lib"] htmlsoup = ["BeautifulSoup4"] [[package]] name = "markdown-it-py" version = "4.0.0" description = "Python port of markdown-it. Markdown parsing, done right!" optional = false python-versions = ">=3.10" groups = ["main", "dev", "integrations"] files = [ {file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"}, {file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"}, ] [package.dependencies] mdurl = ">=0.1,<1.0" [package.extras] benchmarking = ["psutil", "pytest", "pytest-benchmark"] compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "markdown-it-pyrs", "mistletoe (>=1.0,<2.0)", "mistune (>=3.0,<4.0)", "panflute (>=2.3,<3.0)"] linkify = ["linkify-it-py (>=1,<3)"] plugins = ["mdit-py-plugins (>=0.5.0)"] profiling = ["gprof2dot"] rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"] testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "requests"] [[package]] name = "markupsafe" version = "2.1.3" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.7" groups = ["main", "dev", "integrations"] files = [ {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, ] [[package]] name = "mdurl" version = "0.1.2" description = "Markdown URL utilities" optional = false python-versions = ">=3.7" groups = ["main", "dev", "integrations"] files = [ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] [[package]] name = "more-itertools" version = "10.8.0" description = "More routines for operating on iterables, beyond itertools" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b"}, {file = "more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd"}, ] [[package]] name = "msgpack" version = "1.1.1" description = "MessagePack serializer" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "msgpack-1.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:353b6fc0c36fde68b661a12949d7d49f8f51ff5fa019c1e47c87c4ff34b080ed"}, {file = "msgpack-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:79c408fcf76a958491b4e3b103d1c417044544b68e96d06432a189b43d1215c8"}, {file = "msgpack-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78426096939c2c7482bf31ef15ca219a9e24460289c00dd0b94411040bb73ad2"}, {file = "msgpack-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b17ba27727a36cb73aabacaa44b13090feb88a01d012c0f4be70c00f75048b4"}, {file = "msgpack-1.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a17ac1ea6ec3c7687d70201cfda3b1e8061466f28f686c24f627cae4ea8efd0"}, {file = "msgpack-1.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:88d1e966c9235c1d4e2afac21ca83933ba59537e2e2727a999bf3f515ca2af26"}, {file = "msgpack-1.1.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f6d58656842e1b2ddbe07f43f56b10a60f2ba5826164910968f5933e5178af75"}, {file = "msgpack-1.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:96decdfc4adcbc087f5ea7ebdcfd3dee9a13358cae6e81d54be962efc38f6338"}, {file = "msgpack-1.1.1-cp310-cp310-win32.whl", hash = "sha256:6640fd979ca9a212e4bcdf6eb74051ade2c690b862b679bfcb60ae46e6dc4bfd"}, {file = "msgpack-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:8b65b53204fe1bd037c40c4148d00ef918eb2108d24c9aaa20bc31f9810ce0a8"}, {file = "msgpack-1.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:71ef05c1726884e44f8b1d1773604ab5d4d17729d8491403a705e649116c9558"}, {file = "msgpack-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:36043272c6aede309d29d56851f8841ba907a1a3d04435e43e8a19928e243c1d"}, {file = "msgpack-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a32747b1b39c3ac27d0670122b57e6e57f28eefb725e0b625618d1b59bf9d1e0"}, {file = "msgpack-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a8b10fdb84a43e50d38057b06901ec9da52baac6983d3f709d8507f3889d43f"}, {file = "msgpack-1.1.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba0c325c3f485dc54ec298d8b024e134acf07c10d494ffa24373bea729acf704"}, {file = "msgpack-1.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:88daaf7d146e48ec71212ce21109b66e06a98e5e44dca47d853cbfe171d6c8d2"}, {file = "msgpack-1.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8b55ea20dc59b181d3f47103f113e6f28a5e1c89fd5b67b9140edb442ab67f2"}, {file = "msgpack-1.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4a28e8072ae9779f20427af07f53bbb8b4aa81151054e882aee333b158da8752"}, {file = "msgpack-1.1.1-cp311-cp311-win32.whl", hash = "sha256:7da8831f9a0fdb526621ba09a281fadc58ea12701bc709e7b8cbc362feabc295"}, {file = "msgpack-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:5fd1b58e1431008a57247d6e7cc4faa41c3607e8e7d4aaf81f7c29ea013cb458"}, {file = "msgpack-1.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ae497b11f4c21558d95de9f64fff7053544f4d1a17731c866143ed6bb4591238"}, {file = "msgpack-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:33be9ab121df9b6b461ff91baac6f2731f83d9b27ed948c5b9d1978ae28bf157"}, {file = "msgpack-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f64ae8fe7ffba251fecb8408540c34ee9df1c26674c50c4544d72dbf792e5ce"}, {file = "msgpack-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a494554874691720ba5891c9b0b39474ba43ffb1aaf32a5dac874effb1619e1a"}, {file = "msgpack-1.1.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb643284ab0ed26f6957d969fe0dd8bb17beb567beb8998140b5e38a90974f6c"}, {file = "msgpack-1.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d275a9e3c81b1093c060c3837e580c37f47c51eca031f7b5fb76f7b8470f5f9b"}, {file = "msgpack-1.1.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4fd6b577e4541676e0cc9ddc1709d25014d3ad9a66caa19962c4f5de30fc09ef"}, {file = "msgpack-1.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb29aaa613c0a1c40d1af111abf025f1732cab333f96f285d6a93b934738a68a"}, {file = "msgpack-1.1.1-cp312-cp312-win32.whl", hash = "sha256:870b9a626280c86cff9c576ec0d9cbcc54a1e5ebda9cd26dab12baf41fee218c"}, {file = "msgpack-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:5692095123007180dca3e788bb4c399cc26626da51629a31d40207cb262e67f4"}, {file = "msgpack-1.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3765afa6bd4832fc11c3749be4ba4b69a0e8d7b728f78e68120a157a4c5d41f0"}, {file = "msgpack-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8ddb2bcfd1a8b9e431c8d6f4f7db0773084e107730ecf3472f1dfe9ad583f3d9"}, {file = "msgpack-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:196a736f0526a03653d829d7d4c5500a97eea3648aebfd4b6743875f28aa2af8"}, {file = "msgpack-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d592d06e3cc2f537ceeeb23d38799c6ad83255289bb84c2e5792e5a8dea268a"}, {file = "msgpack-1.1.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4df2311b0ce24f06ba253fda361f938dfecd7b961576f9be3f3fbd60e87130ac"}, {file = "msgpack-1.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e4141c5a32b5e37905b5940aacbc59739f036930367d7acce7a64e4dec1f5e0b"}, {file = "msgpack-1.1.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b1ce7f41670c5a69e1389420436f41385b1aa2504c3b0c30620764b15dded2e7"}, {file = "msgpack-1.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4147151acabb9caed4e474c3344181e91ff7a388b888f1e19ea04f7e73dc7ad5"}, {file = "msgpack-1.1.1-cp313-cp313-win32.whl", hash = "sha256:500e85823a27d6d9bba1d057c871b4210c1dd6fb01fbb764e37e4e8847376323"}, {file = "msgpack-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:6d489fba546295983abd142812bda76b57e33d0b9f5d5b71c09a583285506f69"}, {file = "msgpack-1.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bba1be28247e68994355e028dcd668316db30c1f758d3241a7b903ac78dcd285"}, {file = "msgpack-1.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8f93dcddb243159c9e4109c9750ba5b335ab8d48d9522c5308cd05d7e3ce600"}, {file = "msgpack-1.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fbbc0b906a24038c9958a1ba7ae0918ad35b06cb449d398b76a7d08470b0ed9"}, {file = "msgpack-1.1.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:61e35a55a546a1690d9d09effaa436c25ae6130573b6ee9829c37ef0f18d5e78"}, {file = "msgpack-1.1.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:1abfc6e949b352dadf4bce0eb78023212ec5ac42f6abfd469ce91d783c149c2a"}, {file = "msgpack-1.1.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:996f2609ddf0142daba4cefd767d6db26958aac8439ee41db9cc0db9f4c4c3a6"}, {file = "msgpack-1.1.1-cp38-cp38-win32.whl", hash = "sha256:4d3237b224b930d58e9d83c81c0dba7aacc20fcc2f89c1e5423aa0529a4cd142"}, {file = "msgpack-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:da8f41e602574ece93dbbda1fab24650d6bf2a24089f9e9dbb4f5730ec1e58ad"}, {file = "msgpack-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5be6b6bc52fad84d010cb45433720327ce886009d862f46b26d4d154001994b"}, {file = "msgpack-1.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3a89cd8c087ea67e64844287ea52888239cbd2940884eafd2dcd25754fb72232"}, {file = "msgpack-1.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d75f3807a9900a7d575d8d6674a3a47e9f227e8716256f35bc6f03fc597ffbf"}, {file = "msgpack-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d182dac0221eb8faef2e6f44701812b467c02674a322c739355c39e94730cdbf"}, {file = "msgpack-1.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b13fe0fb4aac1aa5320cd693b297fe6fdef0e7bea5518cbc2dd5299f873ae90"}, {file = "msgpack-1.1.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:435807eeb1bc791ceb3247d13c79868deb22184e1fc4224808750f0d7d1affc1"}, {file = "msgpack-1.1.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4835d17af722609a45e16037bb1d4d78b7bdf19d6c0128116d178956618c4e88"}, {file = "msgpack-1.1.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a8ef6e342c137888ebbfb233e02b8fbd689bb5b5fcc59b34711ac47ebd504478"}, {file = "msgpack-1.1.1-cp39-cp39-win32.whl", hash = "sha256:61abccf9de335d9efd149e2fff97ed5974f2481b3353772e8e2dd3402ba2bd57"}, {file = "msgpack-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:40eae974c873b2992fd36424a5d9407f93e97656d999f43fca9d29f820899084"}, {file = "msgpack-1.1.1.tar.gz", hash = "sha256:77b79ce34a2bdab2594f490c8e80dd62a02d650b91a75159a63ec413b8d104cd"}, ] [[package]] name = "msgspec" version = "0.19.0" description = "A fast serialization and validation library, with builtin support for JSON, MessagePack, YAML, and TOML." optional = false python-versions = ">=3.9" groups = ["main", "integrations"] files = [ {file = "msgspec-0.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d8dd848ee7ca7c8153462557655570156c2be94e79acec3561cf379581343259"}, {file = "msgspec-0.19.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0553bbc77662e5708fe66aa75e7bd3e4b0f209709c48b299afd791d711a93c36"}, {file = "msgspec-0.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe2c4bf29bf4e89790b3117470dea2c20b59932772483082c468b990d45fb947"}, {file = "msgspec-0.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e87ecfa9795ee5214861eab8326b0e75475c2e68a384002aa135ea2a27d909"}, {file = "msgspec-0.19.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3c4ec642689da44618f68c90855a10edbc6ac3ff7c1d94395446c65a776e712a"}, {file = "msgspec-0.19.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2719647625320b60e2d8af06b35f5b12d4f4d281db30a15a1df22adb2295f633"}, {file = "msgspec-0.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:695b832d0091edd86eeb535cd39e45f3919f48d997685f7ac31acb15e0a2ed90"}, {file = "msgspec-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa77046904db764b0462036bc63ef71f02b75b8f72e9c9dd4c447d6da1ed8f8e"}, {file = "msgspec-0.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:047cfa8675eb3bad68722cfe95c60e7afabf84d1bd8938979dd2b92e9e4a9551"}, {file = "msgspec-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e78f46ff39a427e10b4a61614a2777ad69559cc8d603a7c05681f5a595ea98f7"}, {file = "msgspec-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c7adf191e4bd3be0e9231c3b6dc20cf1199ada2af523885efc2ed218eafd011"}, {file = "msgspec-0.19.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f04cad4385e20be7c7176bb8ae3dca54a08e9756cfc97bcdb4f18560c3042063"}, {file = "msgspec-0.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45c8fb410670b3b7eb884d44a75589377c341ec1392b778311acdbfa55187716"}, {file = "msgspec-0.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:70eaef4934b87193a27d802534dc466778ad8d536e296ae2f9334e182ac27b6c"}, {file = "msgspec-0.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f98bd8962ad549c27d63845b50af3f53ec468b6318400c9f1adfe8b092d7b62f"}, {file = "msgspec-0.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:43bbb237feab761b815ed9df43b266114203f53596f9b6e6f00ebd79d178cdf2"}, {file = "msgspec-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cfc033c02c3e0aec52b71710d7f84cb3ca5eb407ab2ad23d75631153fdb1f12"}, {file = "msgspec-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d911c442571605e17658ca2b416fd8579c5050ac9adc5e00c2cb3126c97f73bc"}, {file = "msgspec-0.19.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:757b501fa57e24896cf40a831442b19a864f56d253679f34f260dcb002524a6c"}, {file = "msgspec-0.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5f0f65f29b45e2816d8bded36e6b837a4bf5fb60ec4bc3c625fa2c6da4124537"}, {file = "msgspec-0.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:067f0de1c33cfa0b6a8206562efdf6be5985b988b53dd244a8e06f993f27c8c0"}, {file = "msgspec-0.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f12d30dd6266557aaaf0aa0f9580a9a8fbeadfa83699c487713e355ec5f0bd86"}, {file = "msgspec-0.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82b2c42c1b9ebc89e822e7e13bbe9d17ede0c23c187469fdd9505afd5a481314"}, {file = "msgspec-0.19.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19746b50be214a54239aab822964f2ac81e38b0055cca94808359d779338c10e"}, {file = "msgspec-0.19.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60ef4bdb0ec8e4ad62e5a1f95230c08efb1f64f32e6e8dd2ced685bcc73858b5"}, {file = "msgspec-0.19.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac7f7c377c122b649f7545810c6cd1b47586e3aa3059126ce3516ac7ccc6a6a9"}, {file = "msgspec-0.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5bc1472223a643f5ffb5bf46ccdede7f9795078194f14edd69e3aab7020d327"}, {file = "msgspec-0.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:317050bc0f7739cb30d257ff09152ca309bf5a369854bbf1e57dffc310c1f20f"}, {file = "msgspec-0.19.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15c1e86fff77184c20a2932cd9742bf33fe23125fa3fcf332df9ad2f7d483044"}, {file = "msgspec-0.19.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3b5541b2b3294e5ffabe31a09d604e23a88533ace36ac288fa32a420aa38d229"}, {file = "msgspec-0.19.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f5c043ace7962ef188746e83b99faaa9e3e699ab857ca3f367b309c8e2c6b12"}, {file = "msgspec-0.19.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca06aa08e39bf57e39a258e1996474f84d0dd8130d486c00bec26d797b8c5446"}, {file = "msgspec-0.19.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e695dad6897896e9384cf5e2687d9ae9feaef50e802f93602d35458e20d1fb19"}, {file = "msgspec-0.19.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3be5c02e1fee57b54130316a08fe40cca53af92999a302a6054cd451700ea7db"}, {file = "msgspec-0.19.0-cp39-cp39-win_amd64.whl", hash = "sha256:0684573a821be3c749912acf5848cce78af4298345cb2d7a8b8948a0a5a27cfe"}, {file = "msgspec-0.19.0.tar.gz", hash = "sha256:604037e7cd475345848116e89c553aa9a233259733ab51986ac924ab1b976f8e"}, ] [package.extras] dev = ["attrs", "coverage", "eval-type-backport ; python_version < \"3.10\"", "furo", "ipython", "msgpack", "mypy", "pre-commit", "pyright", "pytest", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "tomli ; python_version < \"3.11\"", "tomli_w"] doc = ["furo", "ipython", "sphinx", "sphinx-copybutton", "sphinx-design"] test = ["attrs", "eval-type-backport ; python_version < \"3.10\"", "msgpack", "pytest", "pyyaml", "tomli ; python_version < \"3.11\"", "tomli_w"] toml = ["tomli ; python_version < \"3.11\"", "tomli_w"] yaml = ["pyyaml"] [[package]] name = "multidict" version = "6.7.0" description = "multidict implementation" optional = false python-versions = ">=3.9" groups = ["main", "integrations"] files = [ {file = "multidict-6.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9f474ad5acda359c8758c8accc22032c6abe6dc87a8be2440d097785e27a9349"}, {file = "multidict-6.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b7a9db5a870f780220e931d0002bbfd88fb53aceb6293251e2c839415c1b20e"}, {file = "multidict-6.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03ca744319864e92721195fa28c7a3b2bc7b686246b35e4078c1e4d0eb5466d3"}, {file = "multidict-6.7.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f0e77e3c0008bc9316e662624535b88d360c3a5d3f81e15cf12c139a75250046"}, {file = "multidict-6.7.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08325c9e5367aa379a3496aa9a022fe8837ff22e00b94db256d3a1378c76ab32"}, {file = "multidict-6.7.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e2862408c99f84aa571ab462d25236ef9cb12a602ea959ba9c9009a54902fc73"}, {file = "multidict-6.7.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d72a9a2d885f5c208b0cb91ff2ed43636bb7e345ec839ff64708e04f69a13cc"}, {file = "multidict-6.7.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:478cc36476687bac1514d651cbbaa94b86b0732fb6855c60c673794c7dd2da62"}, {file = "multidict-6.7.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6843b28b0364dc605f21481c90fadb5f60d9123b442eb8a726bb74feef588a84"}, {file = "multidict-6.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:23bfeee5316266e5ee2d625df2d2c602b829435fc3a235c2ba2131495706e4a0"}, {file = "multidict-6.7.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:680878b9f3d45c31e1f730eef731f9b0bc1da456155688c6745ee84eb818e90e"}, {file = "multidict-6.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:eb866162ef2f45063acc7a53a88ef6fe8bf121d45c30ea3c9cd87ce7e191a8d4"}, {file = "multidict-6.7.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:df0e3bf7993bdbeca5ac25aa859cf40d39019e015c9c91809ba7093967f7a648"}, {file = "multidict-6.7.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:661709cdcd919a2ece2234f9bae7174e5220c80b034585d7d8a755632d3e2111"}, {file = "multidict-6.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:096f52730c3fb8ed419db2d44391932b63891b2c5ed14850a7e215c0ba9ade36"}, {file = "multidict-6.7.0-cp310-cp310-win32.whl", hash = "sha256:afa8a2978ec65d2336305550535c9c4ff50ee527914328c8677b3973ade52b85"}, {file = "multidict-6.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:b15b3afff74f707b9275d5ba6a91ae8f6429c3ffb29bbfd216b0b375a56f13d7"}, {file = "multidict-6.7.0-cp310-cp310-win_arm64.whl", hash = "sha256:4b73189894398d59131a66ff157837b1fafea9974be486d036bb3d32331fdbf0"}, {file = "multidict-6.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc"}, {file = "multidict-6.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721"}, {file = "multidict-6.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6"}, {file = "multidict-6.7.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c"}, {file = "multidict-6.7.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7"}, {file = "multidict-6.7.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7"}, {file = "multidict-6.7.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9"}, {file = "multidict-6.7.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8"}, {file = "multidict-6.7.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd"}, {file = "multidict-6.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb"}, {file = "multidict-6.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6"}, {file = "multidict-6.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2"}, {file = "multidict-6.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff"}, {file = "multidict-6.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b"}, {file = "multidict-6.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34"}, {file = "multidict-6.7.0-cp311-cp311-win32.whl", hash = "sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff"}, {file = "multidict-6.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81"}, {file = "multidict-6.7.0-cp311-cp311-win_arm64.whl", hash = "sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912"}, {file = "multidict-6.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184"}, {file = "multidict-6.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45"}, {file = "multidict-6.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa"}, {file = "multidict-6.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7"}, {file = "multidict-6.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e"}, {file = "multidict-6.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546"}, {file = "multidict-6.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4"}, {file = "multidict-6.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1"}, {file = "multidict-6.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d"}, {file = "multidict-6.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304"}, {file = "multidict-6.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12"}, {file = "multidict-6.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62"}, {file = "multidict-6.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0"}, {file = "multidict-6.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a"}, {file = "multidict-6.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8"}, {file = "multidict-6.7.0-cp312-cp312-win32.whl", hash = "sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4"}, {file = "multidict-6.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b"}, {file = "multidict-6.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec"}, {file = "multidict-6.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6"}, {file = "multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159"}, {file = "multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca"}, {file = "multidict-6.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8"}, {file = "multidict-6.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60"}, {file = "multidict-6.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4"}, {file = "multidict-6.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f"}, {file = "multidict-6.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf"}, {file = "multidict-6.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32"}, {file = "multidict-6.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036"}, {file = "multidict-6.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec"}, {file = "multidict-6.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e"}, {file = "multidict-6.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64"}, {file = "multidict-6.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd"}, {file = "multidict-6.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288"}, {file = "multidict-6.7.0-cp313-cp313-win32.whl", hash = "sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17"}, {file = "multidict-6.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390"}, {file = "multidict-6.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e"}, {file = "multidict-6.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00"}, {file = "multidict-6.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb"}, {file = "multidict-6.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b"}, {file = "multidict-6.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c"}, {file = "multidict-6.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1"}, {file = "multidict-6.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b"}, {file = "multidict-6.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5"}, {file = "multidict-6.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad"}, {file = "multidict-6.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c"}, {file = "multidict-6.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5"}, {file = "multidict-6.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10"}, {file = "multidict-6.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754"}, {file = "multidict-6.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c"}, {file = "multidict-6.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762"}, {file = "multidict-6.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6"}, {file = "multidict-6.7.0-cp313-cp313t-win32.whl", hash = "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d"}, {file = "multidict-6.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6"}, {file = "multidict-6.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792"}, {file = "multidict-6.7.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842"}, {file = "multidict-6.7.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b"}, {file = "multidict-6.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38"}, {file = "multidict-6.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128"}, {file = "multidict-6.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34"}, {file = "multidict-6.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99"}, {file = "multidict-6.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202"}, {file = "multidict-6.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1"}, {file = "multidict-6.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3"}, {file = "multidict-6.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d"}, {file = "multidict-6.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6"}, {file = "multidict-6.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7"}, {file = "multidict-6.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb"}, {file = "multidict-6.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f"}, {file = "multidict-6.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f"}, {file = "multidict-6.7.0-cp314-cp314-win32.whl", hash = "sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885"}, {file = "multidict-6.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c"}, {file = "multidict-6.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000"}, {file = "multidict-6.7.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63"}, {file = "multidict-6.7.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718"}, {file = "multidict-6.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2"}, {file = "multidict-6.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e"}, {file = "multidict-6.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064"}, {file = "multidict-6.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e"}, {file = "multidict-6.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd"}, {file = "multidict-6.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a"}, {file = "multidict-6.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96"}, {file = "multidict-6.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e"}, {file = "multidict-6.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599"}, {file = "multidict-6.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394"}, {file = "multidict-6.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38"}, {file = "multidict-6.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9"}, {file = "multidict-6.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0"}, {file = "multidict-6.7.0-cp314-cp314t-win32.whl", hash = "sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13"}, {file = "multidict-6.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd"}, {file = "multidict-6.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827"}, {file = "multidict-6.7.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:363eb68a0a59bd2303216d2346e6c441ba10d36d1f9969fcb6f1ba700de7bb5c"}, {file = "multidict-6.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d874eb056410ca05fed180b6642e680373688efafc7f077b2a2f61811e873a40"}, {file = "multidict-6.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b55d5497b51afdfde55925e04a022f1de14d4f4f25cdfd4f5d9b0aa96166851"}, {file = "multidict-6.7.0-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f8e5c0031b90ca9ce555e2e8fd5c3b02a25f14989cbc310701823832c99eb687"}, {file = "multidict-6.7.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cf41880c991716f3c7cec48e2f19ae4045fc9db5fc9cff27347ada24d710bb5"}, {file = "multidict-6.7.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8cfc12a8630a29d601f48d47787bd7eb730e475e83edb5d6c5084317463373eb"}, {file = "multidict-6.7.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3996b50c3237c4aec17459217c1e7bbdead9a22a0fcd3c365564fbd16439dde6"}, {file = "multidict-6.7.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7f5170993a0dd3ab871c74f45c0a21a4e2c37a2f2b01b5f722a2ad9c6650469e"}, {file = "multidict-6.7.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ec81878ddf0e98817def1e77d4f50dae5ef5b0e4fe796fae3bd674304172416e"}, {file = "multidict-6.7.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9281bf5b34f59afbc6b1e477a372e9526b66ca446f4bf62592839c195a718b32"}, {file = "multidict-6.7.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:68af405971779d8b37198726f2b6fe3955db846fee42db7a4286fc542203934c"}, {file = "multidict-6.7.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3ba3ef510467abb0667421a286dc906e30eb08569365f5cdb131d7aff7c2dd84"}, {file = "multidict-6.7.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b61189b29081a20c7e4e0b49b44d5d44bb0dc92be3c6d06a11cc043f81bf9329"}, {file = "multidict-6.7.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:fb287618b9c7aa3bf8d825f02d9201b2f13078a5ed3b293c8f4d953917d84d5e"}, {file = "multidict-6.7.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:521f33e377ff64b96c4c556b81c55d0cfffb96a11c194fd0c3f1e56f3d8dd5a4"}, {file = "multidict-6.7.0-cp39-cp39-win32.whl", hash = "sha256:ce8fdc2dca699f8dbf055a61d73eaa10482569ad20ee3c36ef9641f69afa8c91"}, {file = "multidict-6.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:7e73299c99939f089dd9b2120a04a516b95cdf8c1cd2b18c53ebf0de80b1f18f"}, {file = "multidict-6.7.0-cp39-cp39-win_arm64.whl", hash = "sha256:6bdce131e14b04fd34a809b6380dbfd826065c3e2fe8a50dbae659fa0c390546"}, {file = "multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3"}, {file = "multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5"}, ] [package.dependencies] typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} [[package]] name = "multipart" version = "1.3.0" description = "Parser for multipart/form-data" optional = false python-versions = ">=3.8" groups = ["main", "integrations"] files = [ {file = "multipart-1.3.0-py3-none-any.whl", hash = "sha256:439bf4b00fd7cb2dbff08ae13f49f4f49798931ecd8d496372c63537fa19f304"}, {file = "multipart-1.3.0.tar.gz", hash = "sha256:a46bd6b0eb4c1ba865beb88ddd886012a3da709b6e7b86084fc37e99087e5cf1"}, ] [package.extras] dev = ["build", "pytest", "pytest-cov", "tox", "tox-uv", "twine"] docs = ["sphinx (>=8,<9)", "sphinx-autobuild"] [[package]] name = "mypy" version = "1.18.2" description = "Optional static typing for Python" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "mypy-1.18.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c"}, {file = "mypy-1.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e"}, {file = "mypy-1.18.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448acd386266989ef11662ce3c8011fd2a7b632e0ec7d61a98edd8e27472225b"}, {file = "mypy-1.18.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9e171c465ad3901dc652643ee4bffa8e9fef4d7d0eece23b428908c77a76a66"}, {file = "mypy-1.18.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:592ec214750bc00741af1f80cbf96b5013d81486b7bb24cb052382c19e40b428"}, {file = "mypy-1.18.2-cp310-cp310-win_amd64.whl", hash = "sha256:7fb95f97199ea11769ebe3638c29b550b5221e997c63b14ef93d2e971606ebed"}, {file = "mypy-1.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f"}, {file = "mypy-1.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341"}, {file = "mypy-1.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d"}, {file = "mypy-1.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86"}, {file = "mypy-1.18.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37"}, {file = "mypy-1.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8"}, {file = "mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34"}, {file = "mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764"}, {file = "mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893"}, {file = "mypy-1.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914"}, {file = "mypy-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8"}, {file = "mypy-1.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074"}, {file = "mypy-1.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc"}, {file = "mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e"}, {file = "mypy-1.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986"}, {file = "mypy-1.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d"}, {file = "mypy-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba"}, {file = "mypy-1.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544"}, {file = "mypy-1.18.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce"}, {file = "mypy-1.18.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d"}, {file = "mypy-1.18.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c"}, {file = "mypy-1.18.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb"}, {file = "mypy-1.18.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075"}, {file = "mypy-1.18.2-cp314-cp314-win_amd64.whl", hash = "sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf"}, {file = "mypy-1.18.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:25a9c8fb67b00599f839cf472713f54249a62efd53a54b565eb61956a7e3296b"}, {file = "mypy-1.18.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2b9c7e284ee20e7598d6f42e13ca40b4928e6957ed6813d1ab6348aa3f47133"}, {file = "mypy-1.18.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6985ed057513e344e43a26cc1cd815c7a94602fb6a3130a34798625bc2f07b6"}, {file = "mypy-1.18.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22f27105f1525ec024b5c630c0b9f36d5c1cc4d447d61fe51ff4bd60633f47ac"}, {file = "mypy-1.18.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:030c52d0ea8144e721e49b1f68391e39553d7451f0c3f8a7565b59e19fcb608b"}, {file = "mypy-1.18.2-cp39-cp39-win_amd64.whl", hash = "sha256:aa5e07ac1a60a253445797e42b8b2963c9675563a94f11291ab40718b016a7a0"}, {file = "mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e"}, {file = "mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b"}, ] [package.dependencies] mypy_extensions = ">=1.0.0" pathspec = ">=0.9.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} typing_extensions = ">=4.6.0" [package.extras] dmypy = ["psutil (>=4.0)"] faster-cache = ["orjson"] install-types = ["pip"] mypyc = ["setuptools (>=50)"] reports = ["lxml"] [[package]] name = "mypy-extensions" version = "1.1.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, ] [[package]] name = "nodeenv" version = "1.9.1" description = "Node.js virtual environment builder" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" groups = ["dev"] files = [ {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, ] [[package]] name = "nox" version = "2024.10.9" description = "Flexible test automation." optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "nox-2024.10.9-py3-none-any.whl", hash = "sha256:1d36f309a0a2a853e9bccb76bbef6bb118ba92fa92674d15604ca99adeb29eab"}, {file = "nox-2024.10.9.tar.gz", hash = "sha256:7aa9dc8d1c27e9f45ab046ffd1c3b2c4f7c91755304769df231308849ebded95"}, ] [package.dependencies] argcomplete = ">=1.9.4,<4" colorlog = ">=2.6.1,<7" packaging = ">=20.9" tomli = {version = ">=1", markers = "python_version < \"3.11\""} virtualenv = ">=20.14.1" [package.extras] tox-to-nox = ["jinja2", "tox"] uv = ["uv (>=0.1.6)"] [[package]] name = "nox-poetry" version = "1.2.0" description = "nox-poetry" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "nox_poetry-1.2.0-py3-none-any.whl", hash = "sha256:266eea7a0ab3cad7f4121ecc05b76945036db3b67e6e347557f05010a18e2682"}, {file = "nox_poetry-1.2.0.tar.gz", hash = "sha256:2531a404e3a21eb73fc1a587a548506a8e2c4c1e6e7ef0c1d0d8d6453b7e5d26"}, ] [package.dependencies] build = ">=1.2" nox = ">=2020.8.22" packaging = ">=20.9" tomlkit = ">=0.7" [[package]] name = "opentelemetry-api" version = "1.37.0" description = "OpenTelemetry Python API" optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ {file = "opentelemetry_api-1.37.0-py3-none-any.whl", hash = "sha256:accf2024d3e89faec14302213bc39550ec0f4095d1cf5ca688e1bfb1c8612f47"}, {file = "opentelemetry_api-1.37.0.tar.gz", hash = "sha256:540735b120355bd5112738ea53621f8d5edb35ebcd6fe21ada3ab1c61d1cd9a7"}, ] [package.dependencies] importlib-metadata = ">=6.0,<8.8.0" typing-extensions = ">=4.5.0" [[package]] name = "opentelemetry-sdk" version = "1.37.0" description = "OpenTelemetry Python SDK" optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ {file = "opentelemetry_sdk-1.37.0-py3-none-any.whl", hash = "sha256:8f3c3c22063e52475c5dbced7209495c2c16723d016d39287dfc215d1771257c"}, {file = "opentelemetry_sdk-1.37.0.tar.gz", hash = "sha256:cc8e089c10953ded765b5ab5669b198bbe0af1b3f89f1007d19acd32dc46dda5"}, ] [package.dependencies] opentelemetry-api = "1.37.0" opentelemetry-semantic-conventions = "0.58b0" typing-extensions = ">=4.5.0" [[package]] name = "opentelemetry-semantic-conventions" version = "0.58b0" description = "OpenTelemetry Semantic Conventions" optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ {file = "opentelemetry_semantic_conventions-0.58b0-py3-none-any.whl", hash = "sha256:5564905ab1458b96684db1340232729fce3b5375a06e140e8904c78e4f815b28"}, {file = "opentelemetry_semantic_conventions-0.58b0.tar.gz", hash = "sha256:6bd46f51264279c433755767bb44ad00f1c9e2367e1b42af563372c5a6fa0c25"}, ] [package.dependencies] opentelemetry-api = "1.37.0" typing-extensions = ">=4.5.0" [[package]] name = "packaging" version = "25.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" groups = ["main", "dev", "integrations"] files = [ {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, ] [[package]] name = "parameterized" version = "0.9.0" description = "Parameterized testing with any Python test framework" optional = false python-versions = ">=3.7" groups = ["dev"] files = [ {file = "parameterized-0.9.0-py2.py3-none-any.whl", hash = "sha256:4e0758e3d41bea3bbd05ec14fc2c24736723f243b28d702081aef438c9372b1b"}, {file = "parameterized-0.9.0.tar.gz", hash = "sha256:7fc905272cefa4f364c1a3429cbbe9c0f98b793988efb5bf90aac80f08db09b1"}, ] [package.extras] dev = ["jinja2"] [[package]] name = "parso" version = "0.8.5" description = "A Python Parser" optional = false python-versions = ">=3.6" groups = ["dev"] files = [ {file = "parso-0.8.5-py2.py3-none-any.whl", hash = "sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887"}, {file = "parso-0.8.5.tar.gz", hash = "sha256:034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a"}, ] [package.extras] qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] testing = ["docopt", "pytest"] [[package]] name = "pathspec" version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] [[package]] name = "pbs-installer" version = "2025.9.18" description = "Installer for Python Build Standalone" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "pbs_installer-2025.9.18-py3-none-any.whl", hash = "sha256:8ef55d7675698747505c237015d14c81759bd66a0d4c8b20cec9a2dc96e8434c"}, {file = "pbs_installer-2025.9.18.tar.gz", hash = "sha256:c0a51a7c1e015723bd8396f02e15b5876e439f74b0f45bbac436b189f903219f"}, ] [package.dependencies] httpx = {version = ">=0.27.0,<1", optional = true, markers = "extra == \"download\""} zstandard = {version = ">=0.21.0", optional = true, markers = "extra == \"install\""} [package.extras] all = ["pbs-installer[download,install]"] download = ["httpx (>=0.27.0,<1)"] install = ["zstandard (>=0.21.0)"] [[package]] name = "pip" version = "25.0.1" description = "The PyPA recommended tool for installing Python packages." optional = false python-versions = ">=3.8" groups = ["main", "integrations"] files = [ {file = "pip-25.0.1-py3-none-any.whl", hash = "sha256:c46efd13b6aa8279f33f2864459c8ce587ea6a1a59ee20de055868d8f7688f7f"}, {file = "pip-25.0.1.tar.gz", hash = "sha256:88f96547ea48b940a3a385494e181e29fb8637898f88d88737c5049780f196ea"}, ] [[package]] name = "pkginfo" version = "1.12.1.2" description = "Query metadata from sdists / bdists / installed packages." optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "pkginfo-1.12.1.2-py3-none-any.whl", hash = "sha256:c783ac885519cab2c34927ccfa6bf64b5a704d7c69afaea583dd9b7afe969343"}, {file = "pkginfo-1.12.1.2.tar.gz", hash = "sha256:5cd957824ac36f140260964eba3c6be6442a8359b8c48f4adf90210f33a04b7b"}, ] [package.extras] testing = ["pytest", "pytest-cov", "wheel"] [[package]] name = "platformdirs" version = "4.4.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85"}, {file = "platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf"}, ] [package.extras] docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] type = ["mypy (>=1.14.1)"] [[package]] name = "pluggy" version = "1.6.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.9" groups = ["dev", "integrations"] files = [ {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, ] [package.extras] dev = ["pre-commit", "tox"] testing = ["coverage", "pytest", "pytest-benchmark"] [[package]] name = "poetry" version = "2.2.1" description = "Python dependency management and packaging made easy." optional = false python-versions = "<4.0,>=3.9" groups = ["dev"] files = [ {file = "poetry-2.2.1-py3-none-any.whl", hash = "sha256:f5958b908b96c5824e2acbb8b19cdef8a3351c62142d7ecff2d705396c8ca34c"}, {file = "poetry-2.2.1.tar.gz", hash = "sha256:bef9aa4bb00ce4c10b28b25e7bac724094802d6958190762c45df6c12749b37c"}, ] [package.dependencies] build = ">=1.2.1,<2.0.0" cachecontrol = {version = ">=0.14.0,<0.15.0", extras = ["filecache"]} cleo = ">=2.1.0,<3.0.0" dulwich = ">=0.24.0,<0.25.0" fastjsonschema = ">=2.18.0,<3.0.0" findpython = ">=0.6.2,<0.8.0" installer = ">=0.7.0,<0.8.0" keyring = ">=25.1.0,<26.0.0" packaging = ">=24.2" pbs-installer = {version = ">=2025.1.6,<2026.0.0", extras = ["download", "install"]} pkginfo = ">=1.12,<2.0" platformdirs = ">=3.0.0,<5" poetry-core = "2.2.1" pyproject-hooks = ">=1.0.0,<2.0.0" requests = ">=2.26,<3.0" requests-toolbelt = ">=1.0.0,<2.0.0" shellingham = ">=1.5,<2.0" tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""} tomlkit = ">=0.11.4,<1.0.0" trove-classifiers = ">=2022.5.19" virtualenv = ">=20.26.6" xattr = {version = ">=1.0.0,<2.0.0", markers = "sys_platform == \"darwin\""} [[package]] name = "poetry-core" version = "2.2.1" description = "Poetry PEP 517 Build Backend" optional = false python-versions = "<4.0,>=3.9" groups = ["dev"] files = [ {file = "poetry_core-2.2.1-py3-none-any.whl", hash = "sha256:bdfce710edc10bfcf9ab35041605c480829be4ab23f5bc01202cfe5db8f125ab"}, {file = "poetry_core-2.2.1.tar.gz", hash = "sha256:97e50d8593c8729d3f49364b428583e044087ee3def1e010c6496db76bd65ac5"}, ] [[package]] name = "poetry-plugin-export" version = "1.9.0" description = "Poetry plugin to export the dependencies to various formats" optional = false python-versions = "<4.0,>=3.9" groups = ["dev"] files = [ {file = "poetry_plugin_export-1.9.0-py3-none-any.whl", hash = "sha256:e2621dd8c260dd705a8227f076075246a7ff5c697e18ddb90ff68081f47ee642"}, {file = "poetry_plugin_export-1.9.0.tar.gz", hash = "sha256:6fc8755cfac93c74752f85510b171983e2e47d782d4ab5be4ffc4f6945be7967"}, ] [package.dependencies] poetry = ">=2.0.0,<3.0.0" poetry-core = ">=1.7.0,<3.0.0" [[package]] name = "polyfactory" version = "2.22.2" description = "Mock data generation factories" optional = false python-versions = "<4.0,>=3.8" groups = ["main", "integrations"] files = [ {file = "polyfactory-2.22.2-py3-none-any.whl", hash = "sha256:9bea58ac9a80375b4153cd60820f75e558b863e567e058794d28c6a52b84118a"}, {file = "polyfactory-2.22.2.tar.gz", hash = "sha256:a3297aa0b004f2b26341e903795565ae88507c4d86e68b132c2622969028587a"}, ] [package.dependencies] faker = ">=5.0.0" typing-extensions = ">=4.6.0" [package.extras] attrs = ["attrs (>=22.2.0)"] beanie = ["beanie", "pydantic[email]", "pymongo (<4.9)"] full = ["attrs", "beanie", "msgspec", "odmantic", "pydantic", "sqlalchemy"] msgspec = ["msgspec"] odmantic = ["odmantic (<1.0.0)", "pydantic[email]"] pydantic = ["pydantic[email] (>=1.10)"] sqlalchemy = ["sqlalchemy (>=1.4.29)"] [[package]] name = "posthog" version = "6.7.6" description = "Integrate PostHog into any python application." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "posthog-6.7.6-py3-none-any.whl", hash = "sha256:b09a7e65a042ec416c28874b397d3accae412a80a8b0ef3fa686fbffc99e4d4b"}, {file = "posthog-6.7.6.tar.gz", hash = "sha256:ee5c5ad04b857d96d9b7a4f715e23916a2f206bfcf25e5a9d328a3d27664b0d3"}, ] [package.dependencies] backoff = ">=1.10.0" distro = ">=1.5.0" python-dateutil = ">=2.2" requests = ">=2.7,<3.0" six = ">=1.5" typing-extensions = ">=4.2.0" [package.extras] dev = ["django-stubs", "lxml", "mypy", "mypy-baseline", "packaging", "pre-commit", "pydantic", "ruff", "setuptools", "tomli", "tomli_w", "twine", "types-mock", "types-python-dateutil", "types-requests", "types-setuptools", "types-six", "wheel"] langchain = ["langchain (>=0.2.0)"] test = ["anthropic", "coverage", "django", "freezegun (==1.5.1)", "google-genai", "langchain-anthropic (>=0.3.15)", "langchain-community (>=0.3.25)", "langchain-core (>=0.3.65)", "langchain-openai (>=0.3.22)", "langgraph (>=0.4.8)", "mock (>=2.0.0)", "openai", "parameterized (>=0.8.1)", "pydantic", "pytest", "pytest-asyncio", "pytest-timeout"] [[package]] name = "pre-commit" version = "4.3.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8"}, {file = "pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16"}, ] [package.dependencies] cfgv = ">=2.0.0" identify = ">=1.0.0" nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" [[package]] name = "priority" version = "2.0.0" description = "A pure-Python implementation of the HTTP/2 priority tree" optional = false python-versions = ">=3.6.1" groups = ["main", "integrations"] files = [ {file = "priority-2.0.0-py3-none-any.whl", hash = "sha256:6f8eefce5f3ad59baf2c080a664037bb4725cd0a790d53d59ab4059288faf6aa"}, {file = "priority-2.0.0.tar.gz", hash = "sha256:c965d54f1b8d0d0b19479db3924c7c36cf672dbf2aec92d43fbdaf4492ba18c0"}, ] [[package]] name = "propcache" version = "0.4.0" description = "Accelerated property cache" optional = false python-versions = ">=3.9" groups = ["main", "integrations"] files = [ {file = "propcache-0.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:779aaae64089e2f4992e993faea801925395d26bb5de4a47df7ef7f942c14f80"}, {file = "propcache-0.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:566552ed9b003030745e5bc7b402b83cf3cecae1bade95262d78543741786db5"}, {file = "propcache-0.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:944de70384c62d16d4a00c686b422aa75efbc67c4addaebefbb56475d1c16034"}, {file = "propcache-0.4.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e878553543ece1f8006d0ba4d096b40290580db173bfb18e16158045b9371335"}, {file = "propcache-0.4.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8659f995b19185179474b18de8755689e1f71e1334d05c14e1895caa4e409cf7"}, {file = "propcache-0.4.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7aa8cc5c94e682dce91cb4d12d7b81c01641f4ef5b3b3dc53325d43f0e3b9f2e"}, {file = "propcache-0.4.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da584d917a1a17f690fc726617fd2c3f3006ea959dae5bb07a5630f7b16f9f5f"}, {file = "propcache-0.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:892a072e5b19c3f324a4f8543c9f7e8fc2b0aa08579e46f69bdf0cfc1b440454"}, {file = "propcache-0.4.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c20d796210720455086ef3f85adc413d1e41d374742f9b439354f122bbc3b528"}, {file = "propcache-0.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:df7107a91126a495880576610ae989f19106e1900dd5218d08498391fa43b31d"}, {file = "propcache-0.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0b04ac2120c161416c866d0b6a4259e47e92231ff166b518cc0efb95777367c3"}, {file = "propcache-0.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1e7fa29c71ffa8d6a37324258737d09475f84715a6e8c350f67f0bc8e5e44993"}, {file = "propcache-0.4.0-cp310-cp310-win32.whl", hash = "sha256:01c0ebc172ca28e9d62876832befbf7f36080eee6ed9c9e00243de2a8089ad57"}, {file = "propcache-0.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:84f847e64f4d1a232e50460eebc1196642ee9b4c983612f41cd2d44fd2fe7c71"}, {file = "propcache-0.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:2166466a666a5bebc332cd209cad77d996fad925ca7e8a2a6310ba9e851ae641"}, {file = "propcache-0.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6a6a36b94c09711d6397d79006ca47901539fbc602c853d794c39abd6a326549"}, {file = "propcache-0.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:da47070e1340a1639aca6b1c18fe1f1f3d8d64d3a1f9ddc67b94475f44cd40f3"}, {file = "propcache-0.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:de536cf796abc5b58d11c0ad56580215d231d9554ea4bb6b8b1b3bed80aa3234"}, {file = "propcache-0.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5c82af8e329c3cdc3e717dd3c7b2ff1a218b6de611f6ce76ee34967570a9de9"}, {file = "propcache-0.4.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:abe04e7aa5ab2e4056fcf3255ebee2071e4a427681f76d4729519e292c46ecc1"}, {file = "propcache-0.4.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:075ca32384294434344760fdcb95f7833e1d7cf7c4e55f0e726358140179da35"}, {file = "propcache-0.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:626ec13592928b677f48ff5861040b604b635e93d8e2162fb638397ea83d07e8"}, {file = "propcache-0.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:02e071548b6a376e173b0102c3f55dc16e7d055b5307d487e844c320e38cacf2"}, {file = "propcache-0.4.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:2af6de831a26f42a3f94592964becd8d7f238551786d7525807f02e53defbd13"}, {file = "propcache-0.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bd6c6dba1a3b8949e08c4280071c86e38cb602f02e0ed6659234108c7a7cd710"}, {file = "propcache-0.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:783e91595cf9b66c2deda17f2e8748ae8591aa9f7c65dcab038872bfe83c5bb1"}, {file = "propcache-0.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c3f4b125285d354a627eb37f3ea7c13b8842c7c0d47783581d0df0e272dbf5f0"}, {file = "propcache-0.4.0-cp311-cp311-win32.whl", hash = "sha256:71c45f02ffbb8a21040ae816ceff7f6cd749ffac29fc0f9daa42dc1a9652d577"}, {file = "propcache-0.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:7d51f70f77950f8efafed4383865d3533eeee52d8a0dd1c35b65f24de41de4e0"}, {file = "propcache-0.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:858eaabd2191dd0da5272993ad08a748b5d3ae1aefabea8aee619b45c2af4a64"}, {file = "propcache-0.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:381c84a445efb8c9168f1393a5a7c566de22edc42bfe207a142fff919b37f5d9"}, {file = "propcache-0.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5a531d29d7b873b12730972237c48b1a4e5980b98cf21b3f09fa4710abd3a8c3"}, {file = "propcache-0.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cd6e22255ed73efeaaeb1765505a66a48a9ec9ebc919fce5ad490fe5e33b1555"}, {file = "propcache-0.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9a8d277dc218ddf04ec243a53ac309b1afcebe297c0526a8f82320139b56289"}, {file = "propcache-0.4.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:399c73201d88c856a994916200d7cba41d7687096f8eb5139eb68f02785dc3f7"}, {file = "propcache-0.4.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a1d5e474d43c238035b74ecf997f655afa67f979bae591ac838bb3fbe3076392"}, {file = "propcache-0.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22f589652ee38de96aa58dd219335604e09666092bc250c1d9c26a55bcef9932"}, {file = "propcache-0.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e5227da556b2939da6125cda1d5eecf9e412e58bc97b41e2f192605c3ccbb7c2"}, {file = "propcache-0.4.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:92bc43a1ab852310721ce856f40a3a352254aa6f5e26f0fad870b31be45bba2e"}, {file = "propcache-0.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:83ae2f5343f6f06f4c91ae530d95f56b415f768f9c401a5ee2a10459cf74370b"}, {file = "propcache-0.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:077a32977399dc05299b16e793210341a0b511eb0a86d1796873e83ce47334cc"}, {file = "propcache-0.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:94a278c45e6463031b5a8278e40a07edf2bcc3b5379510e22b6c1a6e6498c194"}, {file = "propcache-0.4.0-cp312-cp312-win32.whl", hash = "sha256:4c491462e1dc80f9deb93f428aad8d83bb286de212837f58eb48e75606e7726c"}, {file = "propcache-0.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:cdb0cecafb528ab15ed89cdfed183074d15912d046d3e304955513b50a34b907"}, {file = "propcache-0.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:b2f29697d1110e8cdf7a39cc630498df0082d7898b79b731c1c863f77c6e8cfc"}, {file = "propcache-0.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e2d01fd53e89cb3d71d20b8c225a8c70d84660f2d223afc7ed7851a4086afe6d"}, {file = "propcache-0.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7dfa60953169d2531dd8ae306e9c27c5d4e5efe7a2ba77049e8afdaece062937"}, {file = "propcache-0.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:227892597953611fce2601d49f1d1f39786a6aebc2f253c2de775407f725a3f6"}, {file = "propcache-0.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e0a5bc019014531308fb67d86066d235daa7551baf2e00e1ea7b00531f6ea85"}, {file = "propcache-0.4.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6ebc6e2e65c31356310ddb6519420eaa6bb8c30fbd809d0919129c89dcd70f4c"}, {file = "propcache-0.4.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1927b78dd75fc31a7fdc76cc7039e39f3170cb1d0d9a271e60f0566ecb25211a"}, {file = "propcache-0.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b113feeda47f908562d9a6d0e05798ad2f83d4473c0777dafa2bc7756473218"}, {file = "propcache-0.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4596c12aa7e3bb2abf158ea8f79eb0fb4851606695d04ab846b2bb386f5690a1"}, {file = "propcache-0.4.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6d1f67dad8cc36e8abc2207a77f3f952ac80be7404177830a7af4635a34cbc16"}, {file = "propcache-0.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e6229ad15366cd8b6d6b4185c55dd48debf9ca546f91416ba2e5921ad6e210a6"}, {file = "propcache-0.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2a4bf309d057327f1f227a22ac6baf34a66f9af75e08c613e47c4d775b06d6c7"}, {file = "propcache-0.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c2e274f3d1cbb2ddcc7a55ce3739af0f8510edc68a7f37981b2258fa1eedc833"}, {file = "propcache-0.4.0-cp313-cp313-win32.whl", hash = "sha256:f114a3e1f8034e2957d34043b7a317a8a05d97dfe8fddb36d9a2252c0117dbbc"}, {file = "propcache-0.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:9ba68c57cde9c667f6b65b98bc342dfa7240b1272ffb2c24b32172ee61b6d281"}, {file = "propcache-0.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:eb77a85253174bf73e52c968b689d64be62d71e8ac33cabef4ca77b03fb4ef92"}, {file = "propcache-0.4.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c0e1c218fff95a66ad9f2f83ad41a67cf4d0a3f527efe820f57bde5fda616de4"}, {file = "propcache-0.4.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:5710b1c01472542bb024366803812ca13e8774d21381bcfc1f7ae738eeb38acc"}, {file = "propcache-0.4.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d7f008799682e8826ce98f25e8bc43532d2cd26c187a1462499fa8d123ae054f"}, {file = "propcache-0.4.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0596d2ae99d74ca436553eb9ce11fe4163dc742fcf8724ebe07d7cb0db679bb1"}, {file = "propcache-0.4.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab9c1bd95ebd1689f0e24f2946c495808777e9e8df7bb3c1dfe3e9eb7f47fe0d"}, {file = "propcache-0.4.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a8ef2ea819549ae2e8698d2ec229ae948d7272feea1cb2878289f767b6c585a4"}, {file = "propcache-0.4.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:71a400b2f0b079438cc24f9a27f02eff24d8ef78f2943f949abc518b844ade3d"}, {file = "propcache-0.4.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4c2735d3305e6cecab6e53546909edf407ad3da5b9eeaf483f4cf80142bb21be"}, {file = "propcache-0.4.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:72b51340047ac43b3cf388eebd362d052632260c9f73a50882edbb66e589fd44"}, {file = "propcache-0.4.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:184c779363740d6664982ad05699f378f7694220e2041996f12b7c2a4acdcad0"}, {file = "propcache-0.4.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a60634a9de41f363923c6adfb83105d39e49f7a3058511563ed3de6748661af6"}, {file = "propcache-0.4.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c9b8119244d122241a9c4566bce49bb20408a6827044155856735cf14189a7da"}, {file = "propcache-0.4.0-cp313-cp313t-win32.whl", hash = "sha256:515b610a364c8cdd2b72c734cc97dece85c416892ea8d5c305624ac8734e81db"}, {file = "propcache-0.4.0-cp313-cp313t-win_amd64.whl", hash = "sha256:7ea86eb32e74f9902df57e8608e8ac66f1e1e1d24d1ed2ddeb849888413b924d"}, {file = "propcache-0.4.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c1443fa4bb306461a3a8a52b7de0932a2515b100ecb0ebc630cc3f87d451e0a9"}, {file = "propcache-0.4.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:de8e310d24b5a61de08812dd70d5234da1458d41b059038ee7895a9e4c8cae79"}, {file = "propcache-0.4.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:55a54de5266bc44aa274915cdf388584fa052db8748a869e5500ab5993bac3f4"}, {file = "propcache-0.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:88d50d662c917ec2c9d3858920aa7b9d5bfb74ab9c51424b775ccbe683cb1b4e"}, {file = "propcache-0.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae3adf88a66f5863cf79394bc359da523bb27a2ed6ba9898525a6a02b723bfc5"}, {file = "propcache-0.4.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7f088e21d15b3abdb9047e4b7b7a0acd79bf166893ac2b34a72ab1062feb219e"}, {file = "propcache-0.4.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a4efbaf10793fd574c76a5732c75452f19d93df6e0f758c67dd60552ebd8614b"}, {file = "propcache-0.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:681a168d06284602d56e97f09978057aa88bcc4177352b875b3d781df4efd4cb"}, {file = "propcache-0.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a7f06f077fc4ef37e8a37ca6bbb491b29e29db9fb28e29cf3896aad10dbd4137"}, {file = "propcache-0.4.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:082a643479f49a6778dcd68a80262fc324b14fd8e9b1a5380331fe41adde1738"}, {file = "propcache-0.4.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:26692850120241a99bb4a4eec675cd7b4fdc431144f0d15ef69f7f8599f6165f"}, {file = "propcache-0.4.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:33ad7d37b9a386f97582f5d042cc7b8d4b3591bb384cf50866b749a17e4dba90"}, {file = "propcache-0.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e7fd82d4a5b7583588f103b0771e43948532f1292105f13ee6f3b300933c4ca"}, {file = "propcache-0.4.0-cp314-cp314-win32.whl", hash = "sha256:213eb0d3bc695a70cffffe11a1c2e1c2698d89ffd8dba35a49bc44a035d45c93"}, {file = "propcache-0.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:087e2d3d7613e1b59b2ffca0daabd500c1a032d189c65625ee05ea114afcad0b"}, {file = "propcache-0.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:94b0f7407d18001dbdcbb239512e753b1b36725a6e08a4983be1c948f5435f79"}, {file = "propcache-0.4.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:b730048ae8b875e2c0af1a09ca31b303fc7b5ed27652beec03fa22b29545aec9"}, {file = "propcache-0.4.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f495007ada16a4e16312b502636fafff42a9003adf1d4fb7541e0a0870bc056f"}, {file = "propcache-0.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:659a0ea6d9017558ed7af00fb4028186f64d0ba9adfc70a4d2c85fcd3d026321"}, {file = "propcache-0.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d74aa60b1ec076d4d5dcde27c9a535fc0ebb12613f599681c438ca3daa68acac"}, {file = "propcache-0.4.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:34000e31795bdcda9826e0e70e783847a42e3dcd0d6416c5d3cb717905ebaec0"}, {file = "propcache-0.4.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bcb5bfac5b9635e6fc520c8af6efc7a0a56f12a1fe9e9d3eb4328537e316dd6a"}, {file = "propcache-0.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ea11fceb31fa95b0fa2007037f19e922e2caceb7dc6c6cac4cb56e2d291f1a2"}, {file = "propcache-0.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cd8684f628fe285ea5c86f88e1c30716239dc9d6ac55e7851a4b7f555b628da3"}, {file = "propcache-0.4.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:790286d3d542c0ef9f6d0280d1049378e5e776dcba780d169298f664c39394db"}, {file = "propcache-0.4.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:009093c9b5dbae114a5958e6a649f8a5d94dd6866b0f82b60395eb92c58002d4"}, {file = "propcache-0.4.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:728d98179e92d77096937fdfecd2c555a3d613abe56c9909165c24196a3b5012"}, {file = "propcache-0.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a9725d96a81e17e48a0fe82d0c3de2f5e623d7163fec70a6c7df90753edd1bec"}, {file = "propcache-0.4.0-cp314-cp314t-win32.whl", hash = "sha256:0964c55c95625193defeb4fd85f8f28a9a754ed012cab71127d10e3dc66b1373"}, {file = "propcache-0.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:24403152e41abf09488d3ae9c0c3bf7ff93e2fb12b435390718f21810353db28"}, {file = "propcache-0.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0363a696a9f24b37a04ed5e34c2e07ccbe92798c998d37729551120a1bb744c4"}, {file = "propcache-0.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0cd30341142c68377cf3c4e2d9f0581e6e528694b2d57c62c786be441053d2fc"}, {file = "propcache-0.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2c46d37955820dd883cf9156ceb7825b8903e910bdd869902e20a5ac4ecd2c8b"}, {file = "propcache-0.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0b12df77eb19266efd146627a65b8ad414f9d15672d253699a50c8205661a820"}, {file = "propcache-0.4.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1cdabd60e109506462e6a7b37008e57979e737dc6e7dfbe1437adcfe354d1a0a"}, {file = "propcache-0.4.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65ff56a31f25925ef030b494fe63289bf07ef0febe6da181b8219146c590e185"}, {file = "propcache-0.4.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:96153e037ae065bb71cae889f23c933190d81ae183f3696a030b47352fd8655d"}, {file = "propcache-0.4.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bf95be277fbb51513895c2cecc81ab12a421cdbd8837f159828a919a0167f96"}, {file = "propcache-0.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8d18d796ffecdc8253742fd53a94ceee2e77ad149eb9ed5960c2856b5f692f71"}, {file = "propcache-0.4.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:4a52c25a51d5894ba60c567b0dbcf73de2f3cd642cf5343679e07ca3a768b085"}, {file = "propcache-0.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:e0ce7f3d1faf7ad58652ed758cc9753049af5308b38f89948aa71793282419c5"}, {file = "propcache-0.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:545987971b2aded25ba4698135ea0ae128836e7deb6e18c29a581076aaef44aa"}, {file = "propcache-0.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7da5c4c72ae40fd3ce87213ab057db66df53e55600d0b9e72e2b7f5a470a2cc4"}, {file = "propcache-0.4.0-cp39-cp39-win32.whl", hash = "sha256:2015218812ee8f13bbaebc9f52b1e424cc130b68d4857bef018e65e3834e1c4d"}, {file = "propcache-0.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:39f0f6a3b56e82dc91d84c763b783c5c33720a33c70ee48a1c13ba800ac1fa69"}, {file = "propcache-0.4.0-cp39-cp39-win_arm64.whl", hash = "sha256:236c8da353ea7c22a8e963ab78cddb1126f700ae9538e2c4c6ef471e5545494b"}, {file = "propcache-0.4.0-py3-none-any.whl", hash = "sha256:015b2ca2f98ea9e08ac06eecc409d5d988f78c5fd5821b2ad42bc9afcd6b1557"}, {file = "propcache-0.4.0.tar.gz", hash = "sha256:c1ad731253eb738f9cadd9fa1844e019576c70bca6a534252e97cf33a57da529"}, ] [[package]] name = "psutil" version = "7.1.0" description = "Cross-platform lib for process and system monitoring." optional = false python-versions = ">=3.6" groups = ["dev"] files = [ {file = "psutil-7.1.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:76168cef4397494250e9f4e73eb3752b146de1dd950040b29186d0cce1d5ca13"}, {file = "psutil-7.1.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:5d007560c8c372efdff9e4579c2846d71de737e4605f611437255e81efcca2c5"}, {file = "psutil-7.1.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22e4454970b32472ce7deaa45d045b34d3648ce478e26a04c7e858a0a6e75ff3"}, {file = "psutil-7.1.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c70e113920d51e89f212dd7be06219a9b88014e63a4cec69b684c327bc474e3"}, {file = "psutil-7.1.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d4a113425c037300de3ac8b331637293da9be9713855c4fc9d2d97436d7259d"}, {file = "psutil-7.1.0-cp37-abi3-win32.whl", hash = "sha256:09ad740870c8d219ed8daae0ad3b726d3bf9a028a198e7f3080f6a1888b99bca"}, {file = "psutil-7.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:57f5e987c36d3146c0dd2528cd42151cf96cd359b9d67cfff836995cc5df9a3d"}, {file = "psutil-7.1.0-cp37-abi3-win_arm64.whl", hash = "sha256:6937cb68133e7c97b6cc9649a570c9a18ba0efebed46d8c5dae4c07fa1b67a07"}, {file = "psutil-7.1.0.tar.gz", hash = "sha256:655708b3c069387c8b77b072fc429a57d0e214221d01c0a772df7dfedcb3bcd2"}, ] [package.extras] dev = ["abi3audit", "black", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pyreadline ; os_name == \"nt\"", "pytest", "pytest-cov", "pytest-instafail", "pytest-subtests", "pytest-xdist", "pywin32 ; os_name == \"nt\" and platform_python_implementation != \"PyPy\"", "requests", "rstcheck", "ruff", "setuptools", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "vulture", "wheel", "wheel ; os_name == \"nt\" and platform_python_implementation != \"PyPy\"", "wmi ; os_name == \"nt\" and platform_python_implementation != \"PyPy\""] test = ["pytest", "pytest-instafail", "pytest-subtests", "pytest-xdist", "pywin32 ; os_name == \"nt\" and platform_python_implementation != \"PyPy\"", "setuptools", "wheel ; os_name == \"nt\" and platform_python_implementation != \"PyPy\"", "wmi ; os_name == \"nt\" and platform_python_implementation != \"PyPy\""] [[package]] name = "pyasn1" version = "0.6.1" description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" optional = false python-versions = ">=3.8" groups = ["integrations"] files = [ {file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"}, {file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"}, ] [[package]] name = "pyasn1-modules" version = "0.4.2" description = "A collection of ASN.1-based protocols modules" optional = false python-versions = ">=3.8" groups = ["integrations"] files = [ {file = "pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a"}, {file = "pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6"}, ] [package.dependencies] pyasn1 = ">=0.6.1,<0.7.0" [[package]] name = "pycparser" version = "2.23" description = "C parser in Python" optional = false python-versions = ">=3.8" groups = ["dev", "integrations"] files = [ {file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"}, {file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"}, ] markers = {dev = "implementation_name != \"PyPy\"", integrations = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\""} [[package]] name = "pydantic" version = "2.12.0" description = "Data validation using Python type hints" optional = false python-versions = ">=3.9" groups = ["main", "dev", "integrations"] files = [ {file = "pydantic-2.12.0-py3-none-any.whl", hash = "sha256:f6a1da352d42790537e95e83a8bdfb91c7efbae63ffd0b86fa823899e807116f"}, {file = "pydantic-2.12.0.tar.gz", hash = "sha256:c1a077e6270dbfb37bfd8b498b3981e2bb18f68103720e51fa6c306a5a9af563"}, ] [package.dependencies] annotated-types = ">=0.6.0" pydantic-core = "2.41.1" typing-extensions = ">=4.14.1" typing-inspection = ">=0.4.2" [package.extras] email = ["email-validator (>=2.0.0)"] timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] [[package]] name = "pydantic-core" version = "2.41.1" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.9" groups = ["main", "dev", "integrations"] files = [ {file = "pydantic_core-2.41.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:e63036298322e9aea1c8b7c0a6c1204d615dbf6ec0668ce5b83ff27f07404a61"}, {file = "pydantic_core-2.41.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:241299ca91fc77ef64f11ed909d2d9220a01834e8e6f8de61275c4dd16b7c936"}, {file = "pydantic_core-2.41.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ab7e594a2a5c24ab8013a7dc8cfe5f2260e80e490685814122081705c2cf2b0"}, {file = "pydantic_core-2.41.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b054ef1a78519cb934b58e9c90c09e93b837c935dcd907b891f2b265b129eb6e"}, {file = "pydantic_core-2.41.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f2ab7d10d0ab2ed6da54c757233eb0f48ebfb4f86e9b88ccecb3f92bbd61a538"}, {file = "pydantic_core-2.41.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2757606b7948bb853a27e4040820306eaa0ccb9e8f9f8a0fa40cb674e170f350"}, {file = "pydantic_core-2.41.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cec0e75eb61f606bad0a32f2be87507087514e26e8c73db6cbdb8371ccd27917"}, {file = "pydantic_core-2.41.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0234236514f44a5bf552105cfe2543a12f48203397d9d0f866affa569345a5b5"}, {file = "pydantic_core-2.41.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1b974e41adfbb4ebb0f65fc4ca951347b17463d60893ba7d5f7b9bb087c83897"}, {file = "pydantic_core-2.41.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:248dafb3204136113c383e91a4d815269f51562b6659b756cf3df14eefc7d0bb"}, {file = "pydantic_core-2.41.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:678f9d76a91d6bcedd7568bbf6beb77ae8447f85d1aeebaab7e2f0829cfc3a13"}, {file = "pydantic_core-2.41.1-cp310-cp310-win32.whl", hash = "sha256:dff5bee1d21ee58277900692a641925d2dddfde65182c972569b1a276d2ac8fb"}, {file = "pydantic_core-2.41.1-cp310-cp310-win_amd64.whl", hash = "sha256:5042da12e5d97d215f91567110fdfa2e2595a25f17c19b9ff024f31c34f9b53e"}, {file = "pydantic_core-2.41.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4f276a6134fe1fc1daa692642a3eaa2b7b858599c49a7610816388f5e37566a1"}, {file = "pydantic_core-2.41.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:07588570a805296ece009c59d9a679dc08fab72fb337365afb4f3a14cfbfc176"}, {file = "pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28527e4b53400cd60ffbd9812ccb2b5135d042129716d71afd7e45bf42b855c0"}, {file = "pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46a1c935c9228bad738c8a41de06478770927baedf581d172494ab36a6b96575"}, {file = "pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:447ddf56e2b7d28d200d3e9eafa936fe40485744b5a824b67039937580b3cb20"}, {file = "pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63892ead40c1160ac860b5debcc95c95c5a0035e543a8b5a4eac70dd22e995f4"}, {file = "pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4a9543ca355e6df8fbe9c83e9faab707701e9103ae857ecb40f1c0cf8b0e94d"}, {file = "pydantic_core-2.41.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f2611bdb694116c31e551ed82e20e39a90bea9b7ad9e54aaf2d045ad621aa7a1"}, {file = "pydantic_core-2.41.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fecc130893a9b5f7bfe230be1bb8c61fe66a19db8ab704f808cb25a82aad0bc9"}, {file = "pydantic_core-2.41.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:1e2df5f8344c99b6ea5219f00fdc8950b8e6f2c422fbc1cc122ec8641fac85a1"}, {file = "pydantic_core-2.41.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:35291331e9d8ed94c257bab6be1cb3a380b5eee570a2784bffc055e18040a2ea"}, {file = "pydantic_core-2.41.1-cp311-cp311-win32.whl", hash = "sha256:2876a095292668d753f1a868c4a57c4ac9f6acbd8edda8debe4218d5848cf42f"}, {file = "pydantic_core-2.41.1-cp311-cp311-win_amd64.whl", hash = "sha256:b92d6c628e9a338846a28dfe3fcdc1a3279388624597898b105e078cdfc59298"}, {file = "pydantic_core-2.41.1-cp311-cp311-win_arm64.whl", hash = "sha256:7d82ae99409eb69d507a89835488fb657faa03ff9968a9379567b0d2e2e56bc5"}, {file = "pydantic_core-2.41.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:db2f82c0ccbce8f021ad304ce35cbe02aa2f95f215cac388eed542b03b4d5eb4"}, {file = "pydantic_core-2.41.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47694a31c710ced9205d5f1e7e8af3ca57cbb8a503d98cb9e33e27c97a501601"}, {file = "pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e9decce94daf47baf9e9d392f5f2557e783085f7c5e522011545d9d6858e00"}, {file = "pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab0adafdf2b89c8b84f847780a119437a0931eca469f7b44d356f2b426dd9741"}, {file = "pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5da98cc81873f39fd56882e1569c4677940fbc12bce6213fad1ead784192d7c8"}, {file = "pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:209910e88afb01fd0fd403947b809ba8dba0e08a095e1f703294fda0a8fdca51"}, {file = "pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:365109d1165d78d98e33c5bfd815a9b5d7d070f578caefaabcc5771825b4ecb5"}, {file = "pydantic_core-2.41.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:706abf21e60a2857acdb09502bc853ee5bce732955e7b723b10311114f033115"}, {file = "pydantic_core-2.41.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bf0bd5417acf7f6a7ec3b53f2109f587be176cb35f9cf016da87e6017437a72d"}, {file = "pydantic_core-2.41.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2e71b1c6ceb9c78424ae9f63a07292fb769fb890a4e7efca5554c47f33a60ea5"}, {file = "pydantic_core-2.41.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:80745b9770b4a38c25015b517451c817799bfb9d6499b0d13d8227ec941cb513"}, {file = "pydantic_core-2.41.1-cp312-cp312-win32.whl", hash = "sha256:83b64d70520e7890453f1aa21d66fda44e7b35f1cfea95adf7b4289a51e2b479"}, {file = "pydantic_core-2.41.1-cp312-cp312-win_amd64.whl", hash = "sha256:377defd66ee2003748ee93c52bcef2d14fde48fe28a0b156f88c3dbf9bc49a50"}, {file = "pydantic_core-2.41.1-cp312-cp312-win_arm64.whl", hash = "sha256:c95caff279d49c1d6cdfe2996e6c2ad712571d3b9caaa209a404426c326c4bde"}, {file = "pydantic_core-2.41.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:70e790fce5f05204ef4403159857bfcd587779da78627b0babb3654f75361ebf"}, {file = "pydantic_core-2.41.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9cebf1ca35f10930612d60bd0f78adfacee824c30a880e3534ba02c207cceceb"}, {file = "pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:170406a37a5bc82c22c3274616bf6f17cc7df9c4a0a0a50449e559cb755db669"}, {file = "pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12d4257fc9187a0ccd41b8b327d6a4e57281ab75e11dda66a9148ef2e1fb712f"}, {file = "pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a75a33b4db105dd1c8d57839e17ee12db8d5ad18209e792fa325dbb4baeb00f4"}, {file = "pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08a589f850803a74e0fcb16a72081cafb0d72a3cdda500106942b07e76b7bf62"}, {file = "pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a97939d6ea44763c456bd8a617ceada2c9b96bb5b8ab3dfa0d0827df7619014"}, {file = "pydantic_core-2.41.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2ae423c65c556f09569524b80ffd11babff61f33055ef9773d7c9fabc11ed8d"}, {file = "pydantic_core-2.41.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:4dc703015fbf8764d6a8001c327a87f1823b7328d40b47ce6000c65918ad2b4f"}, {file = "pydantic_core-2.41.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:968e4ffdfd35698a5fe659e5e44c508b53664870a8e61c8f9d24d3d145d30257"}, {file = "pydantic_core-2.41.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:fff2b76c8e172d34771cd4d4f0ade08072385310f214f823b5a6ad4006890d32"}, {file = "pydantic_core-2.41.1-cp313-cp313-win32.whl", hash = "sha256:a38a5263185407ceb599f2f035faf4589d57e73c7146d64f10577f6449e8171d"}, {file = "pydantic_core-2.41.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42ae7fd6760782c975897e1fdc810f483b021b32245b0105d40f6e7a3803e4b"}, {file = "pydantic_core-2.41.1-cp313-cp313-win_arm64.whl", hash = "sha256:ad4111acc63b7384e205c27a2f15e23ac0ee21a9d77ad6f2e9cb516ec90965fb"}, {file = "pydantic_core-2.41.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:440d0df7415b50084a4ba9d870480c16c5f67c0d1d4d5119e3f70925533a0edc"}, {file = "pydantic_core-2.41.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71eaa38d342099405dae6484216dcf1e8e4b0bebd9b44a4e08c9b43db6a2ab67"}, {file = "pydantic_core-2.41.1-cp313-cp313t-win_amd64.whl", hash = "sha256:555ecf7e50f1161d3f693bc49f23c82cf6cdeafc71fa37a06120772a09a38795"}, {file = "pydantic_core-2.41.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:05226894a26f6f27e1deb735d7308f74ef5fa3a6de3e0135bb66cdcaee88f64b"}, {file = "pydantic_core-2.41.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:85ff7911c6c3e2fd8d3779c50925f6406d770ea58ea6dde9c230d35b52b16b4a"}, {file = "pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47f1f642a205687d59b52dc1a9a607f45e588f5a2e9eeae05edd80c7a8c47674"}, {file = "pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df11c24e138876ace5ec6043e5cae925e34cf38af1a1b3d63589e8f7b5f5cdc4"}, {file = "pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f0bf7f5c8f7bf345c527e8a0d72d6b26eda99c1227b0c34e7e59e181260de31"}, {file = "pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82b887a711d341c2c47352375d73b029418f55b20bd7815446d175a70effa706"}, {file = "pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5f1d5d6bbba484bdf220c72d8ecd0be460f4bd4c5e534a541bb2cd57589fb8b"}, {file = "pydantic_core-2.41.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2bf1917385ebe0f968dc5c6ab1375886d56992b93ddfe6bf52bff575d03662be"}, {file = "pydantic_core-2.41.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:4f94f3ab188f44b9a73f7295663f3ecb8f2e2dd03a69c8f2ead50d37785ecb04"}, {file = "pydantic_core-2.41.1-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:3925446673641d37c30bd84a9d597e49f72eacee8b43322c8999fa17d5ae5bc4"}, {file = "pydantic_core-2.41.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:49bd51cc27adb980c7b97357ae036ce9b3c4d0bb406e84fbe16fb2d368b602a8"}, {file = "pydantic_core-2.41.1-cp314-cp314-win32.whl", hash = "sha256:a31ca0cd0e4d12ea0df0077df2d487fc3eb9d7f96bbb13c3c5b88dcc21d05159"}, {file = "pydantic_core-2.41.1-cp314-cp314-win_amd64.whl", hash = "sha256:1b5c4374a152e10a22175d7790e644fbd8ff58418890e07e2073ff9d4414efae"}, {file = "pydantic_core-2.41.1-cp314-cp314-win_arm64.whl", hash = "sha256:4fee76d757639b493eb600fba668f1e17475af34c17dd61db7a47e824d464ca9"}, {file = "pydantic_core-2.41.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f9b9c968cfe5cd576fdd7361f47f27adeb120517e637d1b189eea1c3ece573f4"}, {file = "pydantic_core-2.41.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1ebc7ab67b856384aba09ed74e3e977dded40e693de18a4f197c67d0d4e6d8e"}, {file = "pydantic_core-2.41.1-cp314-cp314t-win_amd64.whl", hash = "sha256:8ae0dc57b62a762985bc7fbf636be3412394acc0ddb4ade07fe104230f1b9762"}, {file = "pydantic_core-2.41.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:10ce489cf09a4956a1549af839b983edc59b0f60e1b068c21b10154e58f54f80"}, {file = "pydantic_core-2.41.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ff548c908caffd9455fd1342366bcf8a1ec8a3fca42f35c7fc60883d6a901074"}, {file = "pydantic_core-2.41.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d43bf082025082bda13be89a5f876cc2386b7727c7b322be2d2b706a45cea8e"}, {file = "pydantic_core-2.41.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:666aee751faf1c6864b2db795775dd67b61fdcf646abefa309ed1da039a97209"}, {file = "pydantic_core-2.41.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b83aaeff0d7bde852c32e856f3ee410842ebc08bc55c510771d87dcd1c01e1ed"}, {file = "pydantic_core-2.41.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:055c7931b0329cb8acde20cdde6d9c2cbc2a02a0a8e54a792cddd91e2ea92c65"}, {file = "pydantic_core-2.41.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:530bbb1347e3e5ca13a91ac087c4971d7da09630ef8febd27a20a10800c2d06d"}, {file = "pydantic_core-2.41.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65a0ea16cfea7bfa9e43604c8bd726e63a3788b61c384c37664b55209fcb1d74"}, {file = "pydantic_core-2.41.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8fa93fadff794c6d15c345c560513b160197342275c6d104cc879f932b978afc"}, {file = "pydantic_core-2.41.1-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:c8a1af9ac51969a494c6a82b563abae6859dc082d3b999e8fa7ba5ee1b05e8e8"}, {file = "pydantic_core-2.41.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:30edab28829703f876897c9471a857e43d847b8799c3c9e2fbce644724b50aa4"}, {file = "pydantic_core-2.41.1-cp39-cp39-win32.whl", hash = "sha256:84d0ff869f98be2e93efdf1ae31e5a15f0926d22af8677d51676e373abbfe57a"}, {file = "pydantic_core-2.41.1-cp39-cp39-win_amd64.whl", hash = "sha256:b5674314987cdde5a5511b029fa5fb1556b3d147a367e01dd583b19cfa8e35df"}, {file = "pydantic_core-2.41.1-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:68f2251559b8efa99041bb63571ec7cdd2d715ba74cc82b3bc9eff824ebc8bf0"}, {file = "pydantic_core-2.41.1-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:c7bc140c596097cb53b30546ca257dbe3f19282283190b1b5142928e5d5d3a20"}, {file = "pydantic_core-2.41.1-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2896510fce8f4725ec518f8b9d7f015a00db249d2fd40788f442af303480063d"}, {file = "pydantic_core-2.41.1-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ced20e62cfa0f496ba68fa5d6c7ee71114ea67e2a5da3114d6450d7f4683572a"}, {file = "pydantic_core-2.41.1-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:b04fa9ed049461a7398138c604b00550bc89e3e1151d84b81ad6dc93e39c4c06"}, {file = "pydantic_core-2.41.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:b3b7d9cfbfdc43c80a16638c6dc2768e3956e73031fca64e8e1a3ae744d1faeb"}, {file = "pydantic_core-2.41.1-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eec83fc6abef04c7f9bec616e2d76ee9a6a4ae2a359b10c21d0f680e24a247ca"}, {file = "pydantic_core-2.41.1-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6771a2d9f83c4038dfad5970a3eef215940682b2175e32bcc817bdc639019b28"}, {file = "pydantic_core-2.41.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:fabcbdb12de6eada8d6e9a759097adb3c15440fafc675b3e94ae5c9cb8d678a0"}, {file = "pydantic_core-2.41.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:80e97ccfaf0aaf67d55de5085b0ed0d994f57747d9d03f2de5cc9847ca737b08"}, {file = "pydantic_core-2.41.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34df1fe8fea5d332484a763702e8b6a54048a9d4fe6ccf41e34a128238e01f52"}, {file = "pydantic_core-2.41.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:421b5595f845842fc093f7250e24ee395f54ca62d494fdde96f43ecf9228ae01"}, {file = "pydantic_core-2.41.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:dce8b22663c134583aaad24827863306a933f576c79da450be3984924e2031d1"}, {file = "pydantic_core-2.41.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:300a9c162fea9906cc5c103893ca2602afd84f0ec90d3be36f4cc360125d22e1"}, {file = "pydantic_core-2.41.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e019167628f6e6161ae7ab9fb70f6d076a0bf0d55aa9b20833f86a320c70dd65"}, {file = "pydantic_core-2.41.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:13ab9cc2de6f9d4ab645a050ae5aee61a2424ac4d3a16ba23d4c2027705e0301"}, {file = "pydantic_core-2.41.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:af2385d3f98243fb733862f806c5bb9122e5fba05b373e3af40e3c82d711cef1"}, {file = "pydantic_core-2.41.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:6550617a0c2115be56f90c31a5370261d8ce9dbf051c3ed53b51172dd34da696"}, {file = "pydantic_core-2.41.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc17b6ecf4983d298686014c92ebc955a9f9baf9f57dad4065e7906e7bee6222"}, {file = "pydantic_core-2.41.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:42ae9352cf211f08b04ea110563d6b1e415878eea5b4c70f6bdb17dca3b932d2"}, {file = "pydantic_core-2.41.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e82947de92068b0a21681a13dd2102387197092fbe7defcfb8453e0913866506"}, {file = "pydantic_core-2.41.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e244c37d5471c9acdcd282890c6c4c83747b77238bfa19429b8473586c907656"}, {file = "pydantic_core-2.41.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1e798b4b304a995110d41ec93653e57975620ccb2842ba9420037985e7d7284e"}, {file = "pydantic_core-2.41.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f1fc716c0eb1663c59699b024428ad5ec2bcc6b928527b8fe28de6cb89f47efb"}, {file = "pydantic_core-2.41.1.tar.gz", hash = "sha256:1ad375859a6d8c356b7704ec0f547a58e82ee80bb41baa811ad710e124bc8f2f"}, ] [package.dependencies] typing-extensions = ">=4.14.1" [[package]] name = "pygls" version = "1.3.1" description = "A pythonic generic language server (pronounced like 'pie glass')" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "pygls-1.3.1-py3-none-any.whl", hash = "sha256:6e00f11efc56321bdeb6eac04f6d86131f654c7d49124344a9ebb968da3dd91e"}, {file = "pygls-1.3.1.tar.gz", hash = "sha256:140edceefa0da0e9b3c533547c892a42a7d2fd9217ae848c330c53d266a55018"}, ] [package.dependencies] cattrs = ">=23.1.2" lsprotocol = "2023.0.1" [package.extras] ws = ["websockets (>=11.0.3)"] [[package]] name = "pygments" version = "2.19.2" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" groups = ["main", "dev", "integrations"] files = [ {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, ] [package.extras] windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pyinstrument" version = "5.1.1" description = "Call stack profiler for Python. Shows you why your code is slow!" optional = false python-versions = ">=3.8" groups = ["main", "dev"] files = [ {file = "pyinstrument-5.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f4912edf01912792d2f44dfc43d3f041acc7ea634d0300b3394711963a431d1b"}, {file = "pyinstrument-5.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:217ee5decbc7312b16092307a1bfbe1c04b175bb91ad9622388cd266f54fb260"}, {file = "pyinstrument-5.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fcad0f4211226e445cdbca1ee308567d3d7507cb18355463de64bea2dbe0b80"}, {file = "pyinstrument-5.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:129dcdd57f0720ffb0c181517eddf5db6bfb2cdd1338c6fd5f4082d62c657ba0"}, {file = "pyinstrument-5.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e1efb57a5b708f76b5fed08ecb9dc380a1949f4cb82794d14c3c40113c2f4a2d"}, {file = "pyinstrument-5.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d1e89dcf9a88d328cc7eb7c67c43c8a4ad38c8162eda87d93166a998fc8580d"}, {file = "pyinstrument-5.1.1-cp310-cp310-win32.whl", hash = "sha256:954bc886e11ddcf5789a8a953bec65e663f48d93cc634a8c77585f3e8762bcbb"}, {file = "pyinstrument-5.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:9b2f1d54c72661feeefd5ca81b6f487baf52add1d688ae349be599a258168bfc"}, {file = "pyinstrument-5.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5272cff6eea21163b2105f6a80c907315e0f567720621e6d5672dc01bf71ee48"}, {file = "pyinstrument-5.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d4e7dc5a4aee37a44ff2e63db3127f2044dd95edcae240cb95915adbf223d4be"}, {file = "pyinstrument-5.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:602d55121df1d88aeb6d8ebc801597fdcb9718f78d602ae81458d65c56f25d24"}, {file = "pyinstrument-5.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63b5a788ff955e0597bc95463e64d5fa3747017524fdc02a0f5d12d5117cf2b9"}, {file = "pyinstrument-5.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7f66038ec55a12e5510689240cdc745f8e98c90b93363f745106976e5cfb7397"}, {file = "pyinstrument-5.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:110a08b6e7fa9542eb37c337e79467913d364a03bc2062f85566ba96bc82f54e"}, {file = "pyinstrument-5.1.1-cp311-cp311-win32.whl", hash = "sha256:a223d5e9226ccede5bf2fbd4d13ce0aeb5120501b633ba85290ed94df37d3623"}, {file = "pyinstrument-5.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:16ce582a7b56287d338a8b59688458341aab5c6abda970ba50b2f7b3fd69f89d"}, {file = "pyinstrument-5.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bcd6a03bdf180d73bc8dc7371e09dda089a48057095584e5f2818df1c820525b"}, {file = "pyinstrument-5.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5ffa0948c1e268356dcf930c128624f34037ce92ee865fa4c056dee067aee4c5"}, {file = "pyinstrument-5.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c95adf98a920f2039eb0065966f980452a7af794bab387e9bfe8af3c681affa0"}, {file = "pyinstrument-5.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bcb46ca8596b375c27850d4d06a1ce94ed78074774d35cbed3ccd28b663c5ba6"}, {file = "pyinstrument-5.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3fc16597d26b24a46bf3455686300c0b8a3eb565ebc82396f402c031dccc0145"}, {file = "pyinstrument-5.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5aa135b4bd9667ddcb25fa582f4db77c5117ef207cb10ae901a8e4c5d5cde0e0"}, {file = "pyinstrument-5.1.1-cp312-cp312-win32.whl", hash = "sha256:d15e37f8074b3043fca7aa985cb2079d2c221ccb0d27f059451ede800c801645"}, {file = "pyinstrument-5.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:5c27d5cef0e809f213e5a94143c397d948650f5142c91dcce3611f584779183e"}, {file = "pyinstrument-5.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:45af421c60c943a7f1619afabeba4951d4cc16b4206490d7d5b7ef5a4e2dfd42"}, {file = "pyinstrument-5.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2603db3d745a65de66c96929ab9b0fcce050511eb24e32856ea2458785b8917f"}, {file = "pyinstrument-5.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fe32492100efaa1b0a488c237fe420fdaf141646733a31a97f96c4e1fa6bbf8"}, {file = "pyinstrument-5.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:999b5373f8b1e846357923063ae5c9275ad8a85ed4e0a42960a349288d1f5007"}, {file = "pyinstrument-5.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:58a2f69052178ec624e4df0cf546eda48b3a381572ac1cb3272b4c163888af9d"}, {file = "pyinstrument-5.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4d9bbc00d2e258edbefeb39b61ad4636099b08acd1effdd40d76883a13e7bf5a"}, {file = "pyinstrument-5.1.1-cp313-cp313-win32.whl", hash = "sha256:cf2d8933e2aeaa02d4cb6279d83ef11ee882fb243fff96e3378153a730aadd6e"}, {file = "pyinstrument-5.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:2402683a92617066b13a6d48f904396dcd15938016875b392534df027660eed4"}, {file = "pyinstrument-5.1.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:688acba1c00cad73e43254e610f8e384a53ced3b0dbb5268fb44636e2b99663e"}, {file = "pyinstrument-5.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:116f5ad8cec4d6f5626305d7c1a104f5845a084bfb4b192d231eb8c41ea81f9a"}, {file = "pyinstrument-5.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d139d12a637001d3884344330054ce8335b2c8165dc3dd239726e1b358576bd"}, {file = "pyinstrument-5.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bc5b87b1e27bec94457fed8d03c755a3c09edb4f35d975dbdffd77d863173254"}, {file = "pyinstrument-5.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15f4a2ed9562efab34b555e1208955cf9681b2272489d7a59cd0e289344ada2e"}, {file = "pyinstrument-5.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1cb0c79bfa2b2b5734213429c9d7f455e5af664cfde785c69a5780f6c532c1fd"}, {file = "pyinstrument-5.1.1-cp314-cp314-win32.whl", hash = "sha256:3b9f1216ae4848a8983dc405e1a42e46e75bd8ae96aaba328d4358b8fc80a7a0"}, {file = "pyinstrument-5.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:26971d4a17e0d5d4f6737e71c9de7a7ce5c83ab7daf078c6bf330be41d65273b"}, {file = "pyinstrument-5.1.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:62362843884d654401ec4c25fed35f4b4ded077d96b3396f1e791c31e4203d3e"}, {file = "pyinstrument-5.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f2d640230b71c6d9ac8f27a9c5cd07fc8a6acad9196d1e48d9c33658b176fb80"}, {file = "pyinstrument-5.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f54f7292c63461c75ddf193f5e733803e463ccbc54f2fb7c9591337ddea7d10"}, {file = "pyinstrument-5.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c156eb442f9f22960ae16bd195051863d5e8a68b877926e88bbaf8bbdc1456d1"}, {file = "pyinstrument-5.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:caadaf67ad5926c46af784316024793c909b9e9ee550475855fd32171c4bd033"}, {file = "pyinstrument-5.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88ef2e8f483a5e1501d79a7ebdab592a597467810ed24d8db09ab6f568e938d3"}, {file = "pyinstrument-5.1.1-cp314-cp314t-win32.whl", hash = "sha256:265bc4389f82e6521777bfab426a62a15c4940955e86f75db79a44e7349f9757"}, {file = "pyinstrument-5.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:fa254f269a72a007b5d02c18cd4b67081e0efabbd33e18acdbd5e3be905afa06"}, {file = "pyinstrument-5.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:439ab74bfa8cb6e27f7e8bf6783528cd01167d35a076206e2e61076a48d8c270"}, {file = "pyinstrument-5.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:458d89bb9d8ef9b15a0b781047a55ac4f59eecc6b592b21aafde1caa2f04f070"}, {file = "pyinstrument-5.1.1-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3aa6ea18708cab6ad43121a9a90196f53ff80614b456345daf407c7c260c535"}, {file = "pyinstrument-5.1.1-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0c53800eec349f5727429488eea7788e00f9443fda49b1173b254b7bc202544"}, {file = "pyinstrument-5.1.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:e32e1964cb3c0d28e0d07e996adc857d281eebf636aff1256e914ac768c7729f"}, {file = "pyinstrument-5.1.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:2152461091134c2c98c911688149234cc4321ab7afcde5f46bd1d65559d3b34d"}, {file = "pyinstrument-5.1.1-cp38-cp38-win32.whl", hash = "sha256:a3877eb1738f90cb81e7fd8ff95d527367c97012dbc8f6b2102429ef6450f66e"}, {file = "pyinstrument-5.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:70eff7dd8b320bfbfe83ba3a213fe509052907f3858cd556960eecb35c734e82"}, {file = "pyinstrument-5.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:88985f97a7656f1de4ee44abbc2d058720b493e465f1d159784caadc7e68dde4"}, {file = "pyinstrument-5.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b8725a94eb7a990c4e1937bf5f6f025e8e8d159e7e723f879f9e4e92678e0ea2"}, {file = "pyinstrument-5.1.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18e0a6597eab69590312aa3f162f38e9bd1308a8d770d54d8dc510a51d2218d7"}, {file = "pyinstrument-5.1.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4adffa45d4338a7651ffc2eb9714fc5c8542caf8e7e4497f49695fb721bfb2e0"}, {file = "pyinstrument-5.1.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f415f58f70734815b5e17606ce8474a6a0c27660266868d98824ec49a0f90377"}, {file = "pyinstrument-5.1.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:abfdcf2a1fa2fee2e3f84039ed5fd9914fe0b020a6211e959ae69f7a2c6daac7"}, {file = "pyinstrument-5.1.1-cp39-cp39-win32.whl", hash = "sha256:a8ebaf1d74b89f4216cde10968cbfeccd7038a6c8c62e36a80443159acadc6e0"}, {file = "pyinstrument-5.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:420d3361e258e1d981b83aa1167201c35027895375a3377071d4efa8a95a7e66"}, {file = "pyinstrument-5.1.1.tar.gz", hash = "sha256:bc401cda990b3c1cfe8e0e0473cbd605df3c63b73478a89ac4ab108f2184baa8"}, ] [package.extras] bin = ["click", "nox"] docs = ["furo (==2024.7.18)", "myst-parser (==3.0.1)", "sphinx (==7.4.7)", "sphinx-autobuild (==2024.4.16)", "sphinxcontrib-programoutput (==0.17)"] examples = ["django", "litestar", "numpy"] test = ["cffi (>=1.17.0)", "flaky", "greenlet (>=3)", "ipython", "pytest", "pytest-asyncio (==0.23.8)", "trio"] types = ["typing_extensions"] [[package]] name = "pyopenssl" version = "25.3.0" description = "Python wrapper module around the OpenSSL library" optional = false python-versions = ">=3.7" groups = ["integrations"] files = [ {file = "pyopenssl-25.3.0-py3-none-any.whl", hash = "sha256:1fda6fc034d5e3d179d39e59c1895c9faeaf40a79de5fc4cbbfbe0d36f4a77b6"}, {file = "pyopenssl-25.3.0.tar.gz", hash = "sha256:c981cb0a3fd84e8602d7afc209522773b94c1c2446a3c710a75b06fe1beae329"}, ] [package.dependencies] cryptography = ">=45.0.7,<47" typing-extensions = {version = ">=4.9", markers = "python_version < \"3.13\" and python_version >= \"3.8\""} [package.extras] docs = ["sphinx (!=5.2.0,!=5.2.0.post0,!=7.2.5)", "sphinx_rtd_theme"] test = ["pretend", "pytest (>=3.0.1)", "pytest-rerunfailures"] [[package]] name = "pyproject-hooks" version = "1.2.0" description = "Wrappers to call pyproject.toml-based build backend hooks." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ {file = "pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913"}, {file = "pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8"}, ] [[package]] name = "pyright" version = "1.1.401" description = "Command line wrapper for pyright" optional = false python-versions = ">=3.7" groups = ["dev"] files = [ {file = "pyright-1.1.401-py3-none-any.whl", hash = "sha256:6fde30492ba5b0d7667c16ecaf6c699fab8d7a1263f6a18549e0b00bf7724c06"}, {file = "pyright-1.1.401.tar.gz", hash = "sha256:788a82b6611fa5e34a326a921d86d898768cddf59edde8e93e56087d277cc6f1"}, ] [package.dependencies] nodeenv = ">=1.6.0" typing-extensions = ">=4.1" [package.extras] all = ["nodejs-wheel-binaries", "twine (>=3.4.1)"] dev = ["twine (>=3.4.1)"] nodejs = ["nodejs-wheel-binaries"] [[package]] name = "pytest" version = "7.4.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" groups = ["dev", "integrations"] files = [ {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, ] [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-aiohttp" version = "1.1.0" description = "Pytest plugin for aiohttp support" optional = false python-versions = ">=3.9" groups = ["integrations"] files = [ {file = "pytest_aiohttp-1.1.0-py3-none-any.whl", hash = "sha256:f39a11693a0dce08dd6c542d241e199dd8047a6e6596b2bcfa60d373f143456d"}, {file = "pytest_aiohttp-1.1.0.tar.gz", hash = "sha256:147de8cb164f3fc9d7196967f109ab3c0b93ea3463ab50631e56438eab7b5adc"}, ] [package.dependencies] aiohttp = ">=3.11.0b0" pytest = ">=6.1.0" pytest-asyncio = ">=0.17.2" [package.extras] testing = ["coverage (==6.2)", "mypy (==1.12.1)"] [[package]] name = "pytest-asyncio" version = "0.23.8" description = "Pytest support for asyncio" optional = false python-versions = ">=3.8" groups = ["dev", "integrations"] files = [ {file = "pytest_asyncio-0.23.8-py3-none-any.whl", hash = "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2"}, {file = "pytest_asyncio-0.23.8.tar.gz", hash = "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3"}, ] [package.dependencies] pytest = ">=7.0.0,<9" [package.extras] docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] [[package]] name = "pytest-codspeed" version = "4.1.0" description = "Pytest plugin to create CodSpeed benchmarks" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "pytest_codspeed-4.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f269eb3b215f41196fd5abcf0b4151598c13b91db7a1db0a01b1fc8292664e4e"}, {file = "pytest_codspeed-4.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ab2ee8aae973c1ae5214e6e0bbb0ed2939901561e87175bbf1c29d084b7a1d71"}, {file = "pytest_codspeed-4.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:481f006620e771325d56a6c98daa51b8b47be405d31ddd70268b23f57ec1c064"}, {file = "pytest_codspeed-4.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:175eabef05cc13ecf6cfefc8c9045526a7a3df2242e00587803e4a7d7a3c4572"}, {file = "pytest_codspeed-4.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e41f65af8da69bad3eac96bb1b64220b183661bf7f8618a9bb96b3f85952459f"}, {file = "pytest_codspeed-4.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41417ce934f94eee791e0d1982bcb73787369317d1918befcdb0d4cdb0b5ebf8"}, {file = "pytest_codspeed-4.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79cca787f59d1bfb56a976bc287db670c75f76ff90a8727eaeb313369791a76b"}, {file = "pytest_codspeed-4.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fec5d44bfa008d3ebd97290478d27db7e4208c3012d093dea7bcc3a83e150ae"}, {file = "pytest_codspeed-4.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98d6a656ecf00c5770377955b3bfdf2879078c9fb089e5d5e069eeb76ea9cae4"}, {file = "pytest_codspeed-4.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c86bc7cd4911aea26abc6c7dac8e5e5cf8c2c7399949ce0c19449f0031acb6b8"}, {file = "pytest_codspeed-4.1.0-py3-none-any.whl", hash = "sha256:91c8b348c29522344e83c5b707ff4152836c10f3e8d673379ed93cee442d6c7d"}, {file = "pytest_codspeed-4.1.0.tar.gz", hash = "sha256:86728c7fe12af2ee6ca78673f15091d76ab7346e2f49b11f672e74fc3e5c6cea"}, ] [package.dependencies] cffi = ">=1.17.1" pytest = ">=3.8" rich = ">=13.8.1" [package.extras] compat = ["pytest-benchmark (>=5.0.0,<5.1.0)", "pytest-xdist (>=3.6.1,<3.7.0)"] [[package]] name = "pytest-cov" version = "4.1.0" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, ] [package.dependencies] coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] [[package]] name = "pytest-django" version = "4.11.1" description = "A Django plugin for pytest." optional = false python-versions = ">=3.8" groups = ["integrations"] files = [ {file = "pytest_django-4.11.1-py3-none-any.whl", hash = "sha256:1b63773f648aa3d8541000c26929c1ea63934be1cfa674c76436966d73fe6a10"}, {file = "pytest_django-4.11.1.tar.gz", hash = "sha256:a949141a1ee103cb0e7a20f1451d355f83f5e4a5d07bdd4dcfdd1fd0ff227991"}, ] [package.dependencies] pytest = ">=7.0.0" [package.extras] docs = ["sphinx", "sphinx_rtd_theme"] testing = ["Django", "django-configurations (>=2.0)"] [[package]] name = "pytest-emoji" version = "0.2.0" description = "A pytest plugin that adds emojis to your test result report" optional = false python-versions = ">=3.4" groups = ["dev"] files = [ {file = "pytest-emoji-0.2.0.tar.gz", hash = "sha256:e1bd4790d87649c2d09c272c88bdfc4d37c1cc7c7a46583087d7c510944571e8"}, {file = "pytest_emoji-0.2.0-py3-none-any.whl", hash = "sha256:6e34ed21970fa4b80a56ad11417456bd873eb066c02315fe9df0fafe6d4d4436"}, ] [package.dependencies] pytest = ">=4.2.1" [[package]] name = "pytest-mock" version = "3.15.1" description = "Thin-wrapper around the mock package for easier use with pytest" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d"}, {file = "pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f"}, ] [package.dependencies] pytest = ">=6.2.5" [package.extras] dev = ["pre-commit", "pytest-asyncio", "tox"] [[package]] name = "pytest-snapshot" version = "0.9.0" description = "A plugin for snapshot testing with pytest." optional = false python-versions = ">=3.5" groups = ["dev"] files = [ {file = "pytest-snapshot-0.9.0.tar.gz", hash = "sha256:c7013c3abc3e860f9feff899f8b4debe3708650d8d8242a61bf2625ff64db7f3"}, {file = "pytest_snapshot-0.9.0-py3-none-any.whl", hash = "sha256:4b9fe1c21c868fe53a545e4e3184d36bc1c88946e3f5c1d9dd676962a9b3d4ab"}, ] [package.dependencies] pytest = ">=3.0.0" [[package]] name = "pytest-timeout" version = "2.4.0" description = "pytest plugin to abort hanging tests" optional = false python-versions = ">=3.7" groups = ["dev"] files = [ {file = "pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2"}, {file = "pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a"}, ] [package.dependencies] pytest = ">=7.0.0" [[package]] name = "pytest-xdist" version = "3.8.0" description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88"}, {file = "pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1"}, ] [package.dependencies] execnet = ">=2.1" psutil = {version = ">=3.0", optional = true, markers = "extra == \"psutil\""} pytest = ">=7.0.0" [package.extras] psutil = ["psutil (>=3.0)"] setproctitle = ["setproctitle"] testing = ["filelock"] [[package]] name = "python-dateutil" version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" groups = ["main", "dev", "integrations"] files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, ] [package.dependencies] six = ">=1.5" [[package]] name = "python-multipart" version = "0.0.20" description = "A streaming multipart parser for Python" optional = false python-versions = ">=3.8" groups = ["main", "dev"] files = [ {file = "python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104"}, {file = "python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13"}, ] [[package]] name = "pytokens" version = "0.1.10" description = "A Fast, spec compliant Python 3.12+ tokenizer that runs on older Pythons." optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "pytokens-0.1.10-py3-none-any.whl", hash = "sha256:db7b72284e480e69fb085d9f251f66b3d2df8b7166059261258ff35f50fb711b"}, {file = "pytokens-0.1.10.tar.gz", hash = "sha256:c9a4bfa0be1d26aebce03e6884ba454e842f186a59ea43a6d3b25af58223c044"}, ] [package.extras] dev = ["black", "build", "mypy", "pytest", "pytest-cov", "setuptools", "tox", "twine", "wheel"] [[package]] name = "pywin32-ctypes" version = "0.2.3" description = "A (partial) reimplementation of pywin32 using ctypes/cffi" optional = false python-versions = ">=3.6" groups = ["dev"] markers = "sys_platform == \"win32\"" files = [ {file = "pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"}, {file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"}, ] [[package]] name = "pyyaml" version = "6.0.3" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" groups = ["main", "dev", "integrations"] files = [ {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"}, {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"}, {file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"}, {file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"}, {file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"}, {file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"}, {file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"}, {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"}, {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"}, {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"}, {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"}, {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"}, {file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"}, {file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"}, {file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"}, {file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"}, {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"}, {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"}, {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"}, {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"}, {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"}, {file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"}, {file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"}, {file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"}, {file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"}, {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"}, {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"}, {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"}, {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"}, {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"}, {file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"}, {file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"}, {file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"}, {file = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"}, {file = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"}, {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"}, {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"}, {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"}, {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"}, {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"}, {file = "pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"}, {file = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"}, {file = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"}, {file = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"}, {file = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"}, {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"}, {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"}, {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"}, {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"}, {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"}, {file = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"}, {file = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"}, {file = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"}, {file = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"}, {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"}, {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"}, {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"}, {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"}, {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"}, {file = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"}, {file = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"}, {file = "pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da"}, {file = "pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917"}, {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9"}, {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5"}, {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a"}, {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926"}, {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7"}, {file = "pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0"}, {file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"}, {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, ] [[package]] name = "pyyaml-ft" version = "8.0.0" description = "YAML parser and emitter for Python with support for free-threading" optional = false python-versions = ">=3.13" groups = ["main", "dev"] markers = "python_version >= \"3.13\"" files = [ {file = "pyyaml_ft-8.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8c1306282bc958bfda31237f900eb52c9bedf9b93a11f82e1aab004c9a5657a6"}, {file = "pyyaml_ft-8.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30c5f1751625786c19de751e3130fc345ebcba6a86f6bddd6e1285342f4bbb69"}, {file = "pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fa992481155ddda2e303fcc74c79c05eddcdbc907b888d3d9ce3ff3e2adcfb0"}, {file = "pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cec6c92b4207004b62dfad1f0be321c9f04725e0f271c16247d8b39c3bf3ea42"}, {file = "pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06237267dbcab70d4c0e9436d8f719f04a51123f0ca2694c00dd4b68c338e40b"}, {file = "pyyaml_ft-8.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8a7f332bc565817644cdb38ffe4739e44c3e18c55793f75dddb87630f03fc254"}, {file = "pyyaml_ft-8.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7d10175a746be65f6feb86224df5d6bc5c049ebf52b89a88cf1cd78af5a367a8"}, {file = "pyyaml_ft-8.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:58e1015098cf8d8aec82f360789c16283b88ca670fe4275ef6c48c5e30b22a96"}, {file = "pyyaml_ft-8.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5f3e2ceb790d50602b2fd4ec37abbd760a8c778e46354df647e7c5a4ebb"}, {file = "pyyaml_ft-8.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8d445bf6ea16bb93c37b42fdacfb2f94c8e92a79ba9e12768c96ecde867046d1"}, {file = "pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c56bb46b4fda34cbb92a9446a841da3982cdde6ea13de3fbd80db7eeeab8b49"}, {file = "pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dab0abb46eb1780da486f022dce034b952c8ae40753627b27a626d803926483b"}, {file = "pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd48d639cab5ca50ad957b6dd632c7dd3ac02a1abe0e8196a3c24a52f5db3f7a"}, {file = "pyyaml_ft-8.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:052561b89d5b2a8e1289f326d060e794c21fa068aa11255fe71d65baf18a632e"}, {file = "pyyaml_ft-8.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3bb4b927929b0cb162fb1605392a321e3333e48ce616cdcfa04a839271373255"}, {file = "pyyaml_ft-8.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:de04cfe9439565e32f178106c51dd6ca61afaa2907d143835d501d84703d3793"}, {file = "pyyaml_ft-8.0.0.tar.gz", hash = "sha256:0c947dce03954c7b5d38869ed4878b2e6ff1d44b08a0d84dc83fdad205ae39ab"}, ] [[package]] name = "quart" version = "0.20.0" description = "A Python ASGI web framework with the same API as Flask" optional = false python-versions = ">=3.9" groups = ["main", "integrations"] files = [ {file = "quart-0.20.0-py3-none-any.whl", hash = "sha256:003c08f551746710acb757de49d9b768986fd431517d0eb127380b656b98b8f1"}, {file = "quart-0.20.0.tar.gz", hash = "sha256:08793c206ff832483586f5ae47018c7e40bdd75d886fee3fabbdaa70c2cf505d"}, ] [package.dependencies] aiofiles = "*" blinker = ">=1.6" click = ">=8.0" flask = ">=3.0" hypercorn = ">=0.11.2" itsdangerous = "*" jinja2 = "*" markupsafe = "*" werkzeug = ">=3.0" [package.extras] dotenv = ["python-dotenv"] [[package]] name = "rapidfuzz" version = "3.14.1" description = "rapid fuzzy string matching" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ {file = "rapidfuzz-3.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:489440e4b5eea0d150a31076eb183bed0ec84f934df206c72ae4fc3424501758"}, {file = "rapidfuzz-3.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eff22cc938c3f74d194df03790a6c3325d213b28cf65cdefd6fdeae759b745d5"}, {file = "rapidfuzz-3.14.1-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0307f018b16feaa36074bcec2496f6f120af151a098910296e72e233232a62f"}, {file = "rapidfuzz-3.14.1-cp310-cp310-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bc133652da143aca1ab72de235446432888b2b7f44ee332d006f8207967ecb8a"}, {file = "rapidfuzz-3.14.1-cp310-cp310-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e9e71b3fe7e4a1590843389a90fe2a8684649fc74b9b7446e17ee504ddddb7de"}, {file = "rapidfuzz-3.14.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c51519eb2f20b52eba6fc7d857ae94acc6c2a1f5d0f2d794b9d4977cdc29dd7"}, {file = "rapidfuzz-3.14.1-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:fe87d94602624f8f25fff9a0a7b47f33756c4d9fc32b6d3308bb142aa483b8a4"}, {file = "rapidfuzz-3.14.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2d665380503a575dda52eb712ea521f789e8f8fd629c7a8e6c0f8ff480febc78"}, {file = "rapidfuzz-3.14.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0f0dd022b8a7cbf3c891f6de96a80ab6a426f1069a085327816cea749e096c2"}, {file = "rapidfuzz-3.14.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:bf1ba22d36858b265c95cd774ba7fe8991e80a99cd86fe4f388605b01aee81a3"}, {file = "rapidfuzz-3.14.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ca1c1494ac9f9386d37f0e50cbaf4d07d184903aed7691549df1b37e9616edc9"}, {file = "rapidfuzz-3.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9e4b12e921b0fa90d7c2248742a536f21eae5562174090b83edd0b4ab8b557d7"}, {file = "rapidfuzz-3.14.1-cp310-cp310-win32.whl", hash = "sha256:5e1c1f2292baa4049535b07e9e81feb29e3650d2ba35ee491e64aca7ae4cb15e"}, {file = "rapidfuzz-3.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:59a8694beb9a13c4090ab3d1712cabbd896c6949706d1364e2a2e1713c413760"}, {file = "rapidfuzz-3.14.1-cp310-cp310-win_arm64.whl", hash = "sha256:e94cee93faa792572c574a615abe12912124b4ffcf55876b72312914ab663345"}, {file = "rapidfuzz-3.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4d976701060886a791c8a9260b1d4139d14c1f1e9a6ab6116b45a1acf3baff67"}, {file = "rapidfuzz-3.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5e6ba7e6eb2ab03870dcab441d707513db0b4264c12fba7b703e90e8b4296df2"}, {file = "rapidfuzz-3.14.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e532bf46de5fd3a1efde73a16a4d231d011bce401c72abe3c6ecf9de681003f"}, {file = "rapidfuzz-3.14.1-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f9b6a6fb8ed9b951e5f3b82c1ce6b1665308ec1a0da87f799b16e24fc59e4662"}, {file = "rapidfuzz-3.14.1-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b6ac3f9810949caef0e63380b11a3c32a92f26bacb9ced5e32c33560fcdf8d1"}, {file = "rapidfuzz-3.14.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e52e4c34fd567f77513e886b66029c1ae02f094380d10eba18ba1c68a46d8b90"}, {file = "rapidfuzz-3.14.1-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:2ef72e41b1a110149f25b14637f1cedea6df192462120bea3433980fe9d8ac05"}, {file = "rapidfuzz-3.14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fb654a35b373d712a6b0aa2a496b2b5cdd9d32410cfbaecc402d7424a90ba72a"}, {file = "rapidfuzz-3.14.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:2b2c12e5b9eb8fe9a51b92fe69e9ca362c0970e960268188a6d295e1dec91e6d"}, {file = "rapidfuzz-3.14.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4f069dec5c450bd987481e752f0a9979e8fdf8e21e5307f5058f5c4bb162fa56"}, {file = "rapidfuzz-3.14.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4d0d9163725b7ad37a8c46988cae9ebab255984db95ad01bf1987ceb9e3058dd"}, {file = "rapidfuzz-3.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db656884b20b213d846f6bc990c053d1f4a60e6d4357f7211775b02092784ca1"}, {file = "rapidfuzz-3.14.1-cp311-cp311-win32.whl", hash = "sha256:4b42f7b9c58cbcfbfaddc5a6278b4ca3b6cd8983e7fd6af70ca791dff7105fb9"}, {file = "rapidfuzz-3.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:e5847f30d7d4edefe0cb37294d956d3495dd127c1c56e9128af3c2258a520bb4"}, {file = "rapidfuzz-3.14.1-cp311-cp311-win_arm64.whl", hash = "sha256:5087d8ad453092d80c042a08919b1cb20c8ad6047d772dc9312acd834da00f75"}, {file = "rapidfuzz-3.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:809515194f628004aac1b1b280c3734c5ea0ccbd45938c9c9656a23ae8b8f553"}, {file = "rapidfuzz-3.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0afcf2d6cb633d0d4260d8df6a40de2d9c93e9546e2c6b317ab03f89aa120ad7"}, {file = "rapidfuzz-3.14.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5c1c3d07d53dcafee10599da8988d2b1f39df236aee501ecbd617bd883454fcd"}, {file = "rapidfuzz-3.14.1-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e9ee3e1eb0a027717ee72fe34dc9ac5b3e58119f1bd8dd15bc19ed54ae3e62b"}, {file = "rapidfuzz-3.14.1-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:70c845b64a033a20c44ed26bc890eeb851215148cc3e696499f5f65529afb6cb"}, {file = "rapidfuzz-3.14.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26db0e815213d04234298dea0d884d92b9cb8d4ba954cab7cf67a35853128a33"}, {file = "rapidfuzz-3.14.1-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:6ad3395a416f8b126ff11c788531f157c7debeb626f9d897c153ff8980da10fb"}, {file = "rapidfuzz-3.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:61c5b9ab6f730e6478aa2def566223712d121c6f69a94c7cc002044799442afd"}, {file = "rapidfuzz-3.14.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:13e0ea3d0c533969158727d1bb7a08c2cc9a816ab83f8f0dcfde7e38938ce3e6"}, {file = "rapidfuzz-3.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6325ca435b99f4001aac919ab8922ac464999b100173317defb83eae34e82139"}, {file = "rapidfuzz-3.14.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:07a9fad3247e68798424bdc116c1094e88ecfabc17b29edf42a777520347648e"}, {file = "rapidfuzz-3.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8ff5dbe78db0a10c1f916368e21d328935896240f71f721e073cf6c4c8cdedd"}, {file = "rapidfuzz-3.14.1-cp312-cp312-win32.whl", hash = "sha256:9c83270e44a6ae7a39fc1d7e72a27486bccc1fa5f34e01572b1b90b019e6b566"}, {file = "rapidfuzz-3.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:e06664c7fdb51c708e082df08a6888fce4c5c416d7e3cc2fa66dd80eb76a149d"}, {file = "rapidfuzz-3.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:6c7c26025f7934a169a23dafea6807cfc3fb556f1dd49229faf2171e5d8101cc"}, {file = "rapidfuzz-3.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8d69f470d63ee824132ecd80b1974e1d15dd9df5193916901d7860cef081a260"}, {file = "rapidfuzz-3.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6f571d20152fc4833b7b5e781b36d5e4f31f3b5a596a3d53cf66a1bd4436b4f4"}, {file = "rapidfuzz-3.14.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61d77e09b2b6bc38228f53b9ea7972a00722a14a6048be9a3672fb5cb08bad3a"}, {file = "rapidfuzz-3.14.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b41d95ef86a6295d353dc3bb6c80550665ba2c3bef3a9feab46074d12a9af8f"}, {file = "rapidfuzz-3.14.1-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0591df2e856ad583644b40a2b99fb522f93543c65e64b771241dda6d1cfdc96b"}, {file = "rapidfuzz-3.14.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f277801f55b2f3923ef2de51ab94689a0671a4524bf7b611de979f308a54cd6f"}, {file = "rapidfuzz-3.14.1-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:893fdfd4f66ebb67f33da89eb1bd1674b7b30442fdee84db87f6cb9074bf0ce9"}, {file = "rapidfuzz-3.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fe2651258c1f1afa9b66f44bf82f639d5f83034f9804877a1bbbae2120539ad1"}, {file = "rapidfuzz-3.14.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ace21f7a78519d8e889b1240489cd021c5355c496cb151b479b741a4c27f0a25"}, {file = "rapidfuzz-3.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:cb5acf24590bc5e57027283b015950d713f9e4d155fda5cfa71adef3b3a84502"}, {file = "rapidfuzz-3.14.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:67ea46fa8cc78174bad09d66b9a4b98d3068e85de677e3c71ed931a1de28171f"}, {file = "rapidfuzz-3.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:44e741d785de57d1a7bae03599c1cbc7335d0b060a35e60c44c382566e22782e"}, {file = "rapidfuzz-3.14.1-cp313-cp313-win32.whl", hash = "sha256:b1fe6001baa9fa36bcb565e24e88830718f6c90896b91ceffcb48881e3adddbc"}, {file = "rapidfuzz-3.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:83b8cc6336709fa5db0579189bfd125df280a554af544b2dc1c7da9cdad7e44d"}, {file = "rapidfuzz-3.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:cf75769662eadf5f9bd24e865c19e5ca7718e879273dce4e7b3b5824c4da0eb4"}, {file = "rapidfuzz-3.14.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d937dbeda71c921ef6537c6d41a84f1b8112f107589c9977059de57a1d726dd6"}, {file = "rapidfuzz-3.14.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7a2d80cc1a4fcc7e259ed4f505e70b36433a63fa251f1bb69ff279fe376c5efd"}, {file = "rapidfuzz-3.14.1-cp313-cp313t-win32.whl", hash = "sha256:40875e0c06f1a388f1cab3885744f847b557e0b1642dfc31ff02039f9f0823ef"}, {file = "rapidfuzz-3.14.1-cp313-cp313t-win_amd64.whl", hash = "sha256:876dc0c15552f3d704d7fb8d61bdffc872ff63bedf683568d6faad32e51bbce8"}, {file = "rapidfuzz-3.14.1-cp313-cp313t-win_arm64.whl", hash = "sha256:61458e83b0b3e2abc3391d0953c47d6325e506ba44d6a25c869c4401b3bc222c"}, {file = "rapidfuzz-3.14.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e84d9a844dc2e4d5c4cabd14c096374ead006583304333c14a6fbde51f612a44"}, {file = "rapidfuzz-3.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:40301b93b99350edcd02dbb22e37ca5f2a75d0db822e9b3c522da451a93d6f27"}, {file = "rapidfuzz-3.14.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fedd5097a44808dddf341466866e5c57a18a19a336565b4ff50aa8f09eb528f6"}, {file = "rapidfuzz-3.14.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e3e61c9e80d8c26709d8aa5c51fdd25139c81a4ab463895f8a567f8347b0548"}, {file = "rapidfuzz-3.14.1-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da011a373722fac6e64687297a1d17dc8461b82cb12c437845d5a5b161bc24b9"}, {file = "rapidfuzz-3.14.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5967d571243cfb9ad3710e6e628ab68c421a237b76e24a67ac22ee0ff12784d6"}, {file = "rapidfuzz-3.14.1-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:474f416cbb9099676de54aa41944c154ba8d25033ee460f87bb23e54af6d01c9"}, {file = "rapidfuzz-3.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ae2d57464b59297f727c4e201ea99ec7b13935f1f056c753e8103da3f2fc2404"}, {file = "rapidfuzz-3.14.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:57047493a1f62f11354c7143c380b02f1b355c52733e6b03adb1cb0fe8fb8816"}, {file = "rapidfuzz-3.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4acc20776f225ee37d69517a237c090b9fa7e0836a0b8bc58868e9168ba6ef6f"}, {file = "rapidfuzz-3.14.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4373f914ff524ee0146919dea96a40a8200ab157e5a15e777a74a769f73d8a4a"}, {file = "rapidfuzz-3.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:37017b84953927807847016620d61251fe236bd4bcb25e27b6133d955bb9cafb"}, {file = "rapidfuzz-3.14.1-cp314-cp314-win32.whl", hash = "sha256:c8d1dd1146539e093b84d0805e8951475644af794ace81d957ca612e3eb31598"}, {file = "rapidfuzz-3.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:f51c7571295ea97387bac4f048d73cecce51222be78ed808263b45c79c40a440"}, {file = "rapidfuzz-3.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:01eab10ec90912d7d28b3f08f6c91adbaf93458a53f849ff70776ecd70dd7a7a"}, {file = "rapidfuzz-3.14.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:60879fcae2f7618403c4c746a9a3eec89327d73148fb6e89a933b78442ff0669"}, {file = "rapidfuzz-3.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f94d61e44db3fc95a74006a394257af90fa6e826c900a501d749979ff495d702"}, {file = "rapidfuzz-3.14.1-cp314-cp314t-win32.whl", hash = "sha256:93b6294a3ffab32a9b5f9b5ca048fa0474998e7e8bb0f2d2b5e819c64cb71ec7"}, {file = "rapidfuzz-3.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6cb56b695421538fdbe2c0c85888b991d833b8637d2f2b41faa79cea7234c000"}, {file = "rapidfuzz-3.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7cd312c380d3ce9d35c3ec9726b75eee9da50e8a38e89e229a03db2262d3d96b"}, {file = "rapidfuzz-3.14.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:673ce55a9be5b772dade911909e42382c0828b8a50ed7f9168763fa6b9f7054d"}, {file = "rapidfuzz-3.14.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:45c62ada1980ebf4c64c4253993cc8daa018c63163f91db63bb3af69cb74c2e3"}, {file = "rapidfuzz-3.14.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4d51efb29c0df0d4f7f64f672a7624c2146527f0745e3572098d753676538800"}, {file = "rapidfuzz-3.14.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4a21ccdf1bd7d57a1009030527ba8fae1c74bf832d0a08f6b67de8f5c506c96f"}, {file = "rapidfuzz-3.14.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:589fb0af91d3aff318750539c832ea1100dbac2c842fde24e42261df443845f6"}, {file = "rapidfuzz-3.14.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a4f18092db4825f2517d135445015b40033ed809a41754918a03ef062abe88a0"}, {file = "rapidfuzz-3.14.1.tar.gz", hash = "sha256:b02850e7f7152bd1edff27e9d584505b84968cacedee7a734ec4050c655a803c"}, ] [package.extras] all = ["numpy"] [[package]] name = "readchar" version = "4.2.1" description = "Library to easily read single chars and key strokes" optional = false python-versions = ">=3.8" groups = ["main", "dev", "integrations"] files = [ {file = "readchar-4.2.1-py3-none-any.whl", hash = "sha256:a769305cd3994bb5fa2764aa4073452dc105a4ec39068ffe6efd3c20c60acc77"}, {file = "readchar-4.2.1.tar.gz", hash = "sha256:91ce3faf07688de14d800592951e5575e9c7a3213738ed01d394dcc949b79adb"}, ] [[package]] name = "requests" version = "2.32.5" description = "Python HTTP for Humans." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, ] [package.dependencies] certifi = ">=2017.4.17" charset_normalizer = ">=2,<4" idna = ">=2.5,<4" urllib3 = ">=1.21.1,<3" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "requests-toolbelt" version = "1.0.0" description = "A utility belt for advanced users of python-requests" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" groups = ["dev"] files = [ {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"}, {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"}, ] [package.dependencies] requests = ">=2.0.1,<3.0.0" [[package]] name = "rich" version = "14.1.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.8.0" groups = ["main", "dev", "integrations"] files = [ {file = "rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f"}, {file = "rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8"}, ] [package.dependencies] markdown-it-py = ">=2.2.0" pygments = ">=2.13.0,<3.0.0" [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "rich-click" version = "1.9.2" description = "Format click help output nicely with rich" optional = false python-versions = ">=3.8" groups = ["main", "integrations"] files = [ {file = "rich_click-1.9.2-py3-none-any.whl", hash = "sha256:5079dad67ed7df434a9ec1f20b1d62d831e58c78740026f968ce3d3b861f01a0"}, {file = "rich_click-1.9.2.tar.gz", hash = "sha256:1c4212f05561be0cac6a9c1743e1ebcd4fe1fb1e311f9f672abfada3be649db6"}, ] [package.dependencies] click = ">=8" colorama = {version = "*", markers = "platform_system == \"Windows\""} rich = ">=12" typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} [package.extras] dev = ["inline-snapshot (>=0.24)", "jsonschema (>=4)", "mypy (>=1.14.1)", "nodeenv (>=1.9.1)", "packaging (>=25)", "pre-commit (>=3.5)", "pytest (>=8.3.5)", "pytest-cov (>=5)", "rich-codex (>=1.2.11)", "ruff (>=0.12.4)", "typer (>=0.15)", "types-setuptools (>=75.8.0.20250110)"] docs = ["markdown-include (>=0.8.1)", "mike (>=2.1.3)", "mkdocs-github-admonitions-plugin (>=0.1.1)", "mkdocs-glightbox (>=0.4)", "mkdocs-include-markdown-plugin (>=7.1.7) ; python_version >= \"3.9\"", "mkdocs-material-extensions (>=1.3.1)", "mkdocs-material[imaging] (>=9.5.18,<9.6.0)", "mkdocs-redirects (>=1.2.2)", "mkdocs-rss-plugin (>=1.15)", "mkdocs[docs] (>=1.6.1)", "mkdocstrings[python] (>=0.26.1)", "rich-codex (>=1.2.11)", "typer (>=0.15)"] [[package]] name = "ruff" version = "0.14.1" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ {file = "ruff-0.14.1-py3-none-linux_armv6l.whl", hash = "sha256:083bfc1f30f4a391ae09c6f4f99d83074416b471775b59288956f5bc18e82f8b"}, {file = "ruff-0.14.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f6fa757cd717f791009f7669fefb09121cc5f7d9bd0ef211371fad68c2b8b224"}, {file = "ruff-0.14.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d6191903d39ac156921398e9c86b7354d15e3c93772e7dbf26c9fcae59ceccd5"}, {file = "ruff-0.14.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed04f0e04f7a4587244e5c9d7df50e6b5bf2705d75059f409a6421c593a35896"}, {file = "ruff-0.14.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5c9e6cf6cd4acae0febbce29497accd3632fe2025c0c583c8b87e8dbdeae5f61"}, {file = "ruff-0.14.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6fa2458527794ecdfbe45f654e42c61f2503a230545a91af839653a0a93dbc6"}, {file = "ruff-0.14.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:39f1c392244e338b21d42ab29b8a6392a722c5090032eb49bb4d6defcdb34345"}, {file = "ruff-0.14.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7382fa12a26cce1f95070ce450946bec357727aaa428983036362579eadcc5cf"}, {file = "ruff-0.14.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd0bf2be3ae8521e1093a487c4aa3b455882f139787770698530d28ed3fbb37c"}, {file = "ruff-0.14.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cabcaa9ccf8089fb4fdb78d17cc0e28241520f50f4c2e88cb6261ed083d85151"}, {file = "ruff-0.14.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:747d583400f6125ec11a4c14d1c8474bf75d8b419ad22a111a537ec1a952d192"}, {file = "ruff-0.14.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5a6e74c0efd78515a1d13acbfe6c90f0f5bd822aa56b4a6d43a9ffb2ae6e56cd"}, {file = "ruff-0.14.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0ea6a864d2fb41a4b6d5b456ed164302a0d96f4daac630aeba829abfb059d020"}, {file = "ruff-0.14.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0826b8764f94229604fa255918d1cc45e583e38c21c203248b0bfc9a0e930be5"}, {file = "ruff-0.14.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cbc52160465913a1a3f424c81c62ac8096b6a491468e7d872cb9444a860bc33d"}, {file = "ruff-0.14.1-py3-none-win32.whl", hash = "sha256:e037ea374aaaff4103240ae79168c0945ae3d5ae8db190603de3b4012bd1def6"}, {file = "ruff-0.14.1-py3-none-win_amd64.whl", hash = "sha256:59d599cdff9c7f925a017f6f2c256c908b094e55967f93f2821b1439928746a1"}, {file = "ruff-0.14.1-py3-none-win_arm64.whl", hash = "sha256:e3b443c4c9f16ae850906b8d0a707b2a4c16f8d2f0a7fe65c475c5886665ce44"}, {file = "ruff-0.14.1.tar.gz", hash = "sha256:1dd86253060c4772867c61791588627320abcb6ed1577a90ef432ee319729b69"}, ] [[package]] name = "runs" version = "1.2.2" description = "🏃 Run a block of text as a subprocess 🏃" optional = false python-versions = ">=3.8" groups = ["main", "dev", "integrations"] files = [ {file = "runs-1.2.2-py3-none-any.whl", hash = "sha256:0980dcbc25aba1505f307ac4f0e9e92cbd0be2a15a1e983ee86c24c87b839dfd"}, {file = "runs-1.2.2.tar.gz", hash = "sha256:9dc1815e2895cfb3a48317b173b9f1eac9ba5549b36a847b5cc60c3bf82ecef1"}, ] [package.dependencies] xmod = "*" [[package]] name = "sanic" version = "25.3.0" description = "A web server and web framework that's written to go fast. Build fast. Run fast." optional = false python-versions = ">=3.8" groups = ["main", "integrations"] files = [ {file = "sanic-25.3.0-py3-none-any.whl", hash = "sha256:fb519b38b4c220569b0e2e868583ffeaffaab96a78b2e42ae78bc56a644a4cd7"}, {file = "sanic-25.3.0.tar.gz", hash = "sha256:775d522001ec81f034ec8e4d7599e2175bfc097b8d57884f5e4c9322f5e369bb"}, ] [package.dependencies] aiofiles = ">=0.6.0" html5tagger = ">=1.2.1" httptools = ">=0.0.10" multidict = ">=5.0,<7.0" sanic-routing = ">=23.12.0" setuptools = ">=70.1.0" tracerite = ">=1.0.0" typing-extensions = ">=4.4.0" ujson = {version = ">=1.35", markers = "sys_platform != \"win32\" and implementation_name == \"cpython\""} uvloop = {version = ">=0.15.0", markers = "sys_platform != \"win32\" and implementation_name == \"cpython\""} websockets = ">=10.0" [package.extras] all = ["autodocsumm (>=0.2.11)", "bandit", "beautifulsoup4", "chardet (==3.*)", "coverage", "cryptography", "docutils", "enum-tools[sphinx]", "m2r2", "mistune (<2.0.0)", "mypy", "pygments", "pytest (>=8.2.2)", "pytest-benchmark", "pytest-sanic", "ruff", "sanic-testing (>=23.6.0)", "slotscheck (>=0.8.0,<1)", "sphinx (>=2.1.2)", "sphinx_rtd_theme (>=0.4.3)", "towncrier", "tox", "types-ujson ; sys_platform != \"win32\" and implementation_name == \"cpython\"", "uvicorn"] dev = ["bandit", "beautifulsoup4", "chardet (==3.*)", "coverage", "cryptography", "docutils", "mypy", "pygments", "pytest (>=8.2.2)", "pytest-benchmark", "pytest-sanic", "ruff", "sanic-testing (>=23.6.0)", "slotscheck (>=0.8.0,<1)", "towncrier", "tox", "types-ujson ; sys_platform != \"win32\" and implementation_name == \"cpython\"", "uvicorn"] docs = ["autodocsumm (>=0.2.11)", "docutils", "enum-tools[sphinx]", "m2r2", "mistune (<2.0.0)", "pygments", "sphinx (>=2.1.2)", "sphinx_rtd_theme (>=0.4.3)"] ext = ["sanic-ext"] http3 = ["aioquic"] test = ["bandit", "beautifulsoup4", "chardet (==3.*)", "coverage", "docutils", "mypy", "pygments", "pytest (>=8.2.2)", "pytest-benchmark", "pytest-sanic", "ruff", "sanic-testing (>=23.6.0)", "slotscheck (>=0.8.0,<1)", "types-ujson ; sys_platform != \"win32\" and implementation_name == \"cpython\"", "uvicorn"] [[package]] name = "sanic-routing" version = "23.12.0" description = "Core routing component for Sanic" optional = false python-versions = "*" groups = ["main", "integrations"] files = [ {file = "sanic-routing-23.12.0.tar.gz", hash = "sha256:1dcadc62c443e48c852392dba03603f9862b6197fc4cba5bbefeb1ace0848b04"}, {file = "sanic_routing-23.12.0-py3-none-any.whl", hash = "sha256:1558a72afcb9046ed3134a5edae02fc1552cff08f0fff2e8d5de0877ea43ed73"}, ] [[package]] name = "sanic-testing" version = "23.12.0" description = "Core testing clients for Sanic" optional = false python-versions = "*" groups = ["dev"] files = [ {file = "sanic-testing-23.12.0.tar.gz", hash = "sha256:2b9c52b7314b7e1807958f41581e18b8254c5161c953e70fcf492e0dd2fe133f"}, {file = "sanic_testing-23.12.0-py3-none-any.whl", hash = "sha256:d809911fca49cba93e1df9de5c6ab8d95d91bdc03b18ba8a25b4e0b66c4e4c73"}, ] [package.dependencies] httpx = ">=0.18" [package.extras] dev = ["pytest", "pytest-asyncio", "sanic (>=22.12)", "setuptools ; python_version > \"3.11\""] [[package]] name = "secretstorage" version = "3.4.0" description = "Python bindings to FreeDesktop.org Secret Service API" optional = false python-versions = ">=3.10" groups = ["dev"] markers = "sys_platform == \"linux\"" files = [ {file = "secretstorage-3.4.0-py3-none-any.whl", hash = "sha256:0e3b6265c2c63509fb7415717607e4b2c9ab767b7f344a57473b779ca13bd02e"}, {file = "secretstorage-3.4.0.tar.gz", hash = "sha256:c46e216d6815aff8a8a18706a2fbfd8d53fcbb0dce99301881687a1b0289ef7c"}, ] [package.dependencies] cryptography = ">=2.0" jeepney = ">=0.6" [[package]] name = "sentry-sdk" version = "2.40.0" description = "Python client for Sentry (https://sentry.io)" optional = false python-versions = ">=3.6" groups = ["dev"] files = [ {file = "sentry_sdk-2.40.0-py2.py3-none-any.whl", hash = "sha256:d5f6ae0f27ea73e7b09c70ad7d42242326eb44765e87a15d8c5aab96b80013e6"}, {file = "sentry_sdk-2.40.0.tar.gz", hash = "sha256:b9c4672fb2cafabcc28586ab8fd0ceeff9b2352afcf2b936e13d5ba06d141b9f"}, ] [package.dependencies] certifi = "*" urllib3 = ">=1.26.11" [package.extras] aiohttp = ["aiohttp (>=3.5)"] anthropic = ["anthropic (>=0.16)"] arq = ["arq (>=0.23)"] asyncpg = ["asyncpg (>=0.23)"] beam = ["apache-beam (>=2.12)"] bottle = ["bottle (>=0.12.13)"] celery = ["celery (>=3)"] celery-redbeat = ["celery-redbeat (>=2)"] chalice = ["chalice (>=1.16.0)"] clickhouse-driver = ["clickhouse-driver (>=0.2.0)"] django = ["django (>=1.8)"] falcon = ["falcon (>=1.4)"] fastapi = ["fastapi (>=0.79.0)"] flask = ["blinker (>=1.1)", "flask (>=0.11)", "markupsafe"] grpcio = ["grpcio (>=1.21.1)", "protobuf (>=3.8.0)"] http2 = ["httpcore[http2] (==1.*)"] httpx = ["httpx (>=0.16.0)"] huey = ["huey (>=2)"] huggingface-hub = ["huggingface_hub (>=0.22)"] langchain = ["langchain (>=0.0.210)"] langgraph = ["langgraph (>=0.6.6)"] launchdarkly = ["launchdarkly-server-sdk (>=9.8.0)"] litellm = ["litellm (>=1.77.5)"] litestar = ["litestar (>=2.0.0)"] loguru = ["loguru (>=0.5)"] openai = ["openai (>=1.0.0)", "tiktoken (>=0.3.0)"] openfeature = ["openfeature-sdk (>=0.7.1)"] opentelemetry = ["opentelemetry-distro (>=0.35b0)"] opentelemetry-experimental = ["opentelemetry-distro"] pure-eval = ["asttokens", "executing", "pure_eval"] pymongo = ["pymongo (>=3.1)"] pyspark = ["pyspark (>=2.4.4)"] quart = ["blinker (>=1.1)", "quart (>=0.16.1)"] rq = ["rq (>=0.6)"] sanic = ["sanic (>=0.8)"] sqlalchemy = ["sqlalchemy (>=1.2)"] starlette = ["starlette (>=0.19.1)"] starlite = ["starlite (>=1.48)"] statsig = ["statsig (>=0.55.3)"] tornado = ["tornado (>=6)"] unleash = ["UnleashClient (>=6.0.1)"] [[package]] name = "service-identity" version = "24.2.0" description = "Service identity verification for pyOpenSSL & cryptography." optional = false python-versions = ">=3.8" groups = ["integrations"] files = [ {file = "service_identity-24.2.0-py3-none-any.whl", hash = "sha256:6b047fbd8a84fd0bb0d55ebce4031e400562b9196e1e0d3e0fe2b8a59f6d4a85"}, {file = "service_identity-24.2.0.tar.gz", hash = "sha256:b8683ba13f0d39c6cd5d625d2c5f65421d6d707b013b375c355751557cbe8e09"}, ] [package.dependencies] attrs = ">=19.1.0" cryptography = "*" pyasn1 = "*" pyasn1-modules = "*" [package.extras] dev = ["coverage[toml] (>=5.0.2)", "idna", "mypy", "pyopenssl", "pytest", "types-pyopenssl"] docs = ["furo", "myst-parser", "pyopenssl", "sphinx", "sphinx-notfound-page"] idna = ["idna"] mypy = ["idna", "mypy", "types-pyopenssl"] tests = ["coverage[toml] (>=5.0.2)", "pytest"] [[package]] name = "setuptools" version = "80.9.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.9" groups = ["main", "integrations"] files = [ {file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"}, {file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"}, ] [package.extras] check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] [[package]] name = "shellingham" version = "1.5.4" description = "Tool to Detect Surrounding Shell" optional = false python-versions = ">=3.7" groups = ["main", "dev"] files = [ {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, ] [[package]] name = "six" version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" groups = ["main", "dev", "integrations"] files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] [[package]] name = "smmap" version = "5.0.2" description = "A pure Python implementation of a sliding window memory map manager" optional = false python-versions = ">=3.7" groups = ["dev"] files = [ {file = "smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e"}, {file = "smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5"}, ] [[package]] name = "sniffio" version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" groups = ["main", "dev", "integrations"] files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] [[package]] name = "sqlparse" version = "0.5.3" description = "A non-validating SQL parser." optional = false python-versions = ">=3.8" groups = ["main", "integrations"] files = [ {file = "sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca"}, {file = "sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272"}, ] [package.extras] dev = ["build", "hatch"] doc = ["sphinx"] [[package]] name = "starlette" version = "0.48.0" description = "The little ASGI library that shines." optional = false python-versions = ">=3.9" groups = ["main", "integrations"] files = [ {file = "starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659"}, {file = "starlette-0.48.0.tar.gz", hash = "sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46"}, ] [package.dependencies] anyio = ">=3.6.2,<5" typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\""} [package.extras] full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] [[package]] name = "taskgroup" version = "0.2.2" description = "backport of asyncio.TaskGroup, asyncio.Runner and asyncio.timeout" optional = false python-versions = "*" groups = ["main", "integrations"] markers = "python_version == \"3.10\"" files = [ {file = "taskgroup-0.2.2-py2.py3-none-any.whl", hash = "sha256:e2c53121609f4ae97303e9ea1524304b4de6faf9eb2c9280c7f87976479a52fb"}, {file = "taskgroup-0.2.2.tar.gz", hash = "sha256:078483ac3e78f2e3f973e2edbf6941374fbea81b9c5d0a96f51d297717f4752d"}, ] [package.dependencies] exceptiongroup = "*" typing_extensions = ">=4.12.2,<5" [[package]] name = "timeout-decorator" version = "0.5.0" description = "Timeout decorator" optional = false python-versions = "*" groups = ["dev"] files = [ {file = "timeout-decorator-0.5.0.tar.gz", hash = "sha256:6a2f2f58db1c5b24a2cc79de6345760377ad8bdc13813f5265f6c3e63d16b3d7"}, ] [[package]] name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" groups = ["dev"] files = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] [[package]] name = "tomli" version = "2.2.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" groups = ["main", "dev", "integrations"] markers = "python_version == \"3.10\"" files = [ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] [[package]] name = "tomlkit" version = "0.13.3" description = "Style preserving TOML library" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0"}, {file = "tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1"}, ] [[package]] name = "tracerite" version = "1.1.3" description = "Human-readable HTML tracebacks for Python exceptions" optional = false python-versions = "*" groups = ["main", "integrations"] files = [ {file = "tracerite-1.1.3-py3-none-any.whl", hash = "sha256:811d8e2e0fb563b77340eebe2e9f7b324acfe01e09ea58db8bcaecb24327c823"}, {file = "tracerite-1.1.3.tar.gz", hash = "sha256:119fc006f240aa03fffb41cf99cf82fda5c0449c7d4b6fe42c6340403578b31e"}, ] [package.dependencies] html5tagger = ">=1.2.1" [[package]] name = "trove-classifiers" version = "2025.9.11.17" description = "Canonical source for classifiers on PyPI (pypi.org)." optional = false python-versions = "*" groups = ["dev"] files = [ {file = "trove_classifiers-2025.9.11.17-py3-none-any.whl", hash = "sha256:5d392f2d244deb1866556457d6f3516792124a23d1c3a463a2e8668a5d1c15dd"}, {file = "trove_classifiers-2025.9.11.17.tar.gz", hash = "sha256:931ca9841a5e9c9408bc2ae67b50d28acf85bef56219b56860876dd1f2d024dd"}, ] [[package]] name = "twisted" version = "25.5.0" description = "An asynchronous networking framework written in Python" optional = false python-versions = ">=3.8.0" groups = ["integrations"] files = [ {file = "twisted-25.5.0-py3-none-any.whl", hash = "sha256:8559f654d01a54a8c3efe66d533d43f383531ebf8d81d9f9ab4769d91ca15df7"}, {file = "twisted-25.5.0.tar.gz", hash = "sha256:1deb272358cb6be1e3e8fc6f9c8b36f78eb0fa7c2233d2dbe11ec6fee04ea316"}, ] [package.dependencies] attrs = ">=22.2.0" automat = ">=24.8.0" constantly = ">=15.1" hyperlink = ">=17.1.1" idna = {version = ">=2.4", optional = true, markers = "extra == \"tls\""} incremental = ">=24.7.0" pyopenssl = {version = ">=21.0.0", optional = true, markers = "extra == \"tls\""} service-identity = {version = ">=18.1.0", optional = true, markers = "extra == \"tls\""} typing-extensions = ">=4.2.0" zope-interface = ">=5" [package.extras] all-non-platform = ["appdirs (>=1.4.0)", "appdirs (>=1.4.0)", "bcrypt (>=3.1.3)", "bcrypt (>=3.1.3)", "cryptography (>=3.3)", "cryptography (>=3.3)", "cython-test-exception-raiser (>=1.0.2,<2)", "cython-test-exception-raiser (>=1.0.2,<2)", "h2 (>=3.2,<5.0)", "h2 (>=3.2,<5.0)", "httpx[http2] (>=0.27)", "httpx[http2] (>=0.27)", "hypothesis (>=6.56)", "hypothesis (>=6.56)", "idna (>=2.4)", "idna (>=2.4)", "priority (>=1.1.0,<2.0)", "priority (>=1.1.0,<2.0)", "pyhamcrest (>=2)", "pyhamcrest (>=2)", "pyopenssl (>=21.0.0)", "pyopenssl (>=21.0.0)", "pyserial (>=3.0)", "pyserial (>=3.0)", "pywin32 (!=226) ; platform_system == \"Windows\"", "pywin32 (!=226) ; platform_system == \"Windows\"", "service-identity (>=18.1.0)", "service-identity (>=18.1.0)", "wsproto", "wsproto"] conch = ["appdirs (>=1.4.0)", "bcrypt (>=3.1.3)", "cryptography (>=3.3)"] dev = ["coverage (>=7.5,<8.0)", "cython-test-exception-raiser (>=1.0.2,<2)", "httpx[http2] (>=0.27)", "hypothesis (>=6.56)", "pydoctor (>=24.11.1,<24.12.0)", "pyflakes (>=2.2,<3.0)", "pyhamcrest (>=2)", "python-subunit (>=1.4,<2.0)", "sphinx (>=6,<7)", "sphinx-rtd-theme (>=1.3,<2.0)", "towncrier (>=23.6,<24.0)", "twistedchecker (>=0.7,<1.0)"] dev-release = ["pydoctor (>=24.11.1,<24.12.0)", "pydoctor (>=24.11.1,<24.12.0)", "sphinx (>=6,<7)", "sphinx (>=6,<7)", "sphinx-rtd-theme (>=1.3,<2.0)", "sphinx-rtd-theme (>=1.3,<2.0)", "towncrier (>=23.6,<24.0)", "towncrier (>=23.6,<24.0)"] gtk-platform = ["appdirs (>=1.4.0)", "appdirs (>=1.4.0)", "bcrypt (>=3.1.3)", "bcrypt (>=3.1.3)", "cryptography (>=3.3)", "cryptography (>=3.3)", "cython-test-exception-raiser (>=1.0.2,<2)", "cython-test-exception-raiser (>=1.0.2,<2)", "h2 (>=3.2,<5.0)", "h2 (>=3.2,<5.0)", "httpx[http2] (>=0.27)", "httpx[http2] (>=0.27)", "hypothesis (>=6.56)", "hypothesis (>=6.56)", "idna (>=2.4)", "idna (>=2.4)", "priority (>=1.1.0,<2.0)", "priority (>=1.1.0,<2.0)", "pygobject", "pygobject", "pyhamcrest (>=2)", "pyhamcrest (>=2)", "pyopenssl (>=21.0.0)", "pyopenssl (>=21.0.0)", "pyserial (>=3.0)", "pyserial (>=3.0)", "pywin32 (!=226) ; platform_system == \"Windows\"", "pywin32 (!=226) ; platform_system == \"Windows\"", "service-identity (>=18.1.0)", "service-identity (>=18.1.0)", "wsproto", "wsproto"] http2 = ["h2 (>=3.2,<5.0)", "priority (>=1.1.0,<2.0)"] macos-platform = ["appdirs (>=1.4.0)", "appdirs (>=1.4.0)", "bcrypt (>=3.1.3)", "bcrypt (>=3.1.3)", "cryptography (>=3.3)", "cryptography (>=3.3)", "cython-test-exception-raiser (>=1.0.2,<2)", "cython-test-exception-raiser (>=1.0.2,<2)", "h2 (>=3.2,<5.0)", "h2 (>=3.2,<5.0)", "httpx[http2] (>=0.27)", "httpx[http2] (>=0.27)", "hypothesis (>=6.56)", "hypothesis (>=6.56)", "idna (>=2.4)", "idna (>=2.4)", "priority (>=1.1.0,<2.0)", "priority (>=1.1.0,<2.0)", "pyhamcrest (>=2)", "pyhamcrest (>=2)", "pyobjc-core (<11) ; python_version < \"3.9\"", "pyobjc-core (<11) ; python_version < \"3.9\"", "pyobjc-core ; python_version >= \"3.9\"", "pyobjc-core ; python_version >= \"3.9\"", "pyobjc-framework-cfnetwork (<11) ; python_version < \"3.9\"", "pyobjc-framework-cfnetwork (<11) ; python_version < \"3.9\"", "pyobjc-framework-cfnetwork ; python_version >= \"3.9\"", "pyobjc-framework-cfnetwork ; python_version >= \"3.9\"", "pyobjc-framework-cocoa (<11) ; python_version < \"3.9\"", "pyobjc-framework-cocoa (<11) ; python_version < \"3.9\"", "pyobjc-framework-cocoa ; python_version >= \"3.9\"", "pyobjc-framework-cocoa ; python_version >= \"3.9\"", "pyopenssl (>=21.0.0)", "pyopenssl (>=21.0.0)", "pyserial (>=3.0)", "pyserial (>=3.0)", "pywin32 (!=226) ; platform_system == \"Windows\"", "pywin32 (!=226) ; platform_system == \"Windows\"", "service-identity (>=18.1.0)", "service-identity (>=18.1.0)", "wsproto", "wsproto"] mypy = ["appdirs (>=1.4.0)", "bcrypt (>=3.1.3)", "coverage (>=7.5,<8.0)", "cryptography (>=3.3)", "cython-test-exception-raiser (>=1.0.2,<2)", "h2 (>=3.2,<5.0)", "httpx[http2] (>=0.27)", "hypothesis (>=6.56)", "idna (>=2.4)", "mypy (==1.10.1)", "mypy-zope (==1.0.6)", "priority (>=1.1.0,<2.0)", "pydoctor (>=24.11.1,<24.12.0)", "pyflakes (>=2.2,<3.0)", "pyhamcrest (>=2)", "pyopenssl (>=21.0.0)", "pyserial (>=3.0)", "python-subunit (>=1.4,<2.0)", "pywin32 (!=226) ; platform_system == \"Windows\"", "service-identity (>=18.1.0)", "sphinx (>=6,<7)", "sphinx-rtd-theme (>=1.3,<2.0)", "towncrier (>=23.6,<24.0)", "twistedchecker (>=0.7,<1.0)", "types-pyopenssl", "types-setuptools", "wsproto"] osx-platform = ["appdirs (>=1.4.0)", "appdirs (>=1.4.0)", "bcrypt (>=3.1.3)", "bcrypt (>=3.1.3)", "cryptography (>=3.3)", "cryptography (>=3.3)", "cython-test-exception-raiser (>=1.0.2,<2)", "cython-test-exception-raiser (>=1.0.2,<2)", "h2 (>=3.2,<5.0)", "h2 (>=3.2,<5.0)", "httpx[http2] (>=0.27)", "httpx[http2] (>=0.27)", "hypothesis (>=6.56)", "hypothesis (>=6.56)", "idna (>=2.4)", "idna (>=2.4)", "priority (>=1.1.0,<2.0)", "priority (>=1.1.0,<2.0)", "pyhamcrest (>=2)", "pyhamcrest (>=2)", "pyobjc-core (<11) ; python_version < \"3.9\"", "pyobjc-core (<11) ; python_version < \"3.9\"", "pyobjc-core ; python_version >= \"3.9\"", "pyobjc-core ; python_version >= \"3.9\"", "pyobjc-framework-cfnetwork (<11) ; python_version < \"3.9\"", "pyobjc-framework-cfnetwork (<11) ; python_version < \"3.9\"", "pyobjc-framework-cfnetwork ; python_version >= \"3.9\"", "pyobjc-framework-cfnetwork ; python_version >= \"3.9\"", "pyobjc-framework-cocoa (<11) ; python_version < \"3.9\"", "pyobjc-framework-cocoa (<11) ; python_version < \"3.9\"", "pyobjc-framework-cocoa ; python_version >= \"3.9\"", "pyobjc-framework-cocoa ; python_version >= \"3.9\"", "pyopenssl (>=21.0.0)", "pyopenssl (>=21.0.0)", "pyserial (>=3.0)", "pyserial (>=3.0)", "pywin32 (!=226) ; platform_system == \"Windows\"", "pywin32 (!=226) ; platform_system == \"Windows\"", "service-identity (>=18.1.0)", "service-identity (>=18.1.0)", "wsproto", "wsproto"] serial = ["pyserial (>=3.0)", "pywin32 (!=226) ; platform_system == \"Windows\""] test = ["cython-test-exception-raiser (>=1.0.2,<2)", "httpx[http2] (>=0.27)", "hypothesis (>=6.56)", "pyhamcrest (>=2)"] tls = ["idna (>=2.4)", "pyopenssl (>=21.0.0)", "service-identity (>=18.1.0)"] websocket = ["wsproto"] windows-platform = ["appdirs (>=1.4.0)", "appdirs (>=1.4.0)", "bcrypt (>=3.1.3)", "bcrypt (>=3.1.3)", "cryptography (>=3.3)", "cryptography (>=3.3)", "cython-test-exception-raiser (>=1.0.2,<2)", "cython-test-exception-raiser (>=1.0.2,<2)", "h2 (>=3.2,<5.0)", "h2 (>=3.2,<5.0)", "httpx[http2] (>=0.27)", "httpx[http2] (>=0.27)", "hypothesis (>=6.56)", "hypothesis (>=6.56)", "idna (>=2.4)", "idna (>=2.4)", "priority (>=1.1.0,<2.0)", "priority (>=1.1.0,<2.0)", "pyhamcrest (>=2)", "pyhamcrest (>=2)", "pyopenssl (>=21.0.0)", "pyopenssl (>=21.0.0)", "pyserial (>=3.0)", "pyserial (>=3.0)", "pywin32 (!=226)", "pywin32 (!=226)", "pywin32 (!=226) ; platform_system == \"Windows\"", "pywin32 (!=226) ; platform_system == \"Windows\"", "service-identity (>=18.1.0)", "service-identity (>=18.1.0)", "twisted-iocpsupport (>=1.0.2)", "twisted-iocpsupport (>=1.0.2)", "wsproto", "wsproto"] [[package]] name = "txaio" version = "25.9.2" description = "Compatibility API between asyncio/Twisted/Trollius" optional = false python-versions = ">=3.10" groups = ["integrations"] files = [ {file = "txaio-25.9.2-py3-none-any.whl", hash = "sha256:a23ce6e627d130e9b795cbdd46c9eaf8abd35e42d2401bb3fea63d38beda0991"}, {file = "txaio-25.9.2.tar.gz", hash = "sha256:e42004a077c02eb5819ff004a4989e49db113836708430d59cb13d31bd309099"}, ] [package.extras] all = ["twisted (>=22.10.0)", "zope.interface (>=5.2.0)"] dev = ["black (>=25.1.0)", "build", "flake8 (>=7.3.0)", "mypy (>=1.18.2)", "myst_parser (>=4.0.1)", "pyenchant (>=1.6.6)", "pytest (>=2.6.4)", "pytest-cov (>=1.8.1)", "ruff (>=0.13.1)", "sphinx (>=7.2.6)", "sphinx-autoapi (>=3.0.0)", "sphinx-rtd-theme (>=2.0.0)", "sphinxcontrib-bibtex (>=2.6.1)", "sphinxcontrib-images (>=0.9.4)", "sphinxcontrib-spelling (>=2.1.2)", "tox (>=2.1.1)", "tox-gh-actions (>=2.2.0)", "twine (>=1.6.5)", "wheel"] twisted = ["twisted (>=22.10.0)", "zope.interface (>=5.2.0)"] [[package]] name = "typer" version = "0.19.2" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." optional = false python-versions = ">=3.8" groups = ["main", "dev"] files = [ {file = "typer-0.19.2-py3-none-any.whl", hash = "sha256:755e7e19670ffad8283db353267cb81ef252f595aa6834a0d1ca9312d9326cb9"}, {file = "typer-0.19.2.tar.gz", hash = "sha256:9ad824308ded0ad06cc716434705f691d4ee0bfd0fb081839d2e426860e7fdca"}, ] [package.dependencies] click = ">=8.0.0" rich = ">=10.11.0" shellingham = ">=1.3.0" typing-extensions = ">=3.7.4.3" [[package]] name = "types-aiofiles" version = "24.1.0.20250822" description = "Typing stubs for aiofiles" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "types_aiofiles-24.1.0.20250822-py3-none-any.whl", hash = "sha256:0ec8f8909e1a85a5a79aed0573af7901f53120dd2a29771dd0b3ef48e12328b0"}, {file = "types_aiofiles-24.1.0.20250822.tar.gz", hash = "sha256:9ab90d8e0c307fe97a7cf09338301e3f01a163e39f3b529ace82466355c84a7b"}, ] [[package]] name = "types-certifi" version = "2021.10.8.3" description = "Typing stubs for certifi" optional = false python-versions = "*" groups = ["dev"] files = [ {file = "types-certifi-2021.10.8.3.tar.gz", hash = "sha256:72cf7798d165bc0b76e1c10dd1ea3097c7063c42c21d664523b928e88b554a4f"}, {file = "types_certifi-2021.10.8.3-py3-none-any.whl", hash = "sha256:b2d1e325e69f71f7c78e5943d410e650b4707bb0ef32e4ddf3da37f54176e88a"}, ] [[package]] name = "types-chardet" version = "5.0.4.6" description = "Typing stubs for chardet" optional = false python-versions = "*" groups = ["dev"] files = [ {file = "types-chardet-5.0.4.6.tar.gz", hash = "sha256:caf4c74cd13ccfd8b3313c314aba943b159de562a2573ed03137402b2bb37818"}, {file = "types_chardet-5.0.4.6-py3-none-any.whl", hash = "sha256:ea832d87e798abf1e4dfc73767807c2b7fee35d0003ae90348aea4ae00fb004d"}, ] [[package]] name = "types-deprecated" version = "1.2.15.20250304" description = "Typing stubs for Deprecated" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "types_deprecated-1.2.15.20250304-py3-none-any.whl", hash = "sha256:86a65aa550ea8acf49f27e226b8953288cd851de887970fbbdf2239c116c3107"}, {file = "types_deprecated-1.2.15.20250304.tar.gz", hash = "sha256:c329030553029de5cc6cb30f269c11f4e00e598c4241290179f63cda7d33f719"}, ] [[package]] name = "types-freezegun" version = "1.1.10" description = "Typing stubs for freezegun" optional = false python-versions = "*" groups = ["dev"] files = [ {file = "types-freezegun-1.1.10.tar.gz", hash = "sha256:cb3a2d2eee950eacbaac0673ab50499823365ceb8c655babb1544a41446409ec"}, {file = "types_freezegun-1.1.10-py3-none-any.whl", hash = "sha256:fadebe72213e0674036153366205038e1f95c8ca96deb4ef9b71ddc15413543e"}, ] [[package]] name = "types-protobuf" version = "5.29.1.20250403" description = "Typing stubs for protobuf" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "types_protobuf-5.29.1.20250403-py3-none-any.whl", hash = "sha256:c71de04106a2d54e5b2173d0a422058fae0ef2d058d70cf369fb797bf61ffa59"}, {file = "types_protobuf-5.29.1.20250403.tar.gz", hash = "sha256:7ff44f15022119c9d7558ce16e78b2d485bf7040b4fadced4dd069bb5faf77a2"}, ] [[package]] name = "types-python-dateutil" version = "2.9.0.20250822" description = "Typing stubs for python-dateutil" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "types_python_dateutil-2.9.0.20250822-py3-none-any.whl", hash = "sha256:849d52b737e10a6dc6621d2bd7940ec7c65fcb69e6aa2882acf4e56b2b508ddc"}, {file = "types_python_dateutil-2.9.0.20250822.tar.gz", hash = "sha256:84c92c34bd8e68b117bff742bc00b692a1e8531262d4507b33afcc9f7716cd53"}, ] [[package]] name = "types-pyyaml" version = "6.0.12.20250915" description = "Typing stubs for PyYAML" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "types_pyyaml-6.0.12.20250915-py3-none-any.whl", hash = "sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6"}, {file = "types_pyyaml-6.0.12.20250915.tar.gz", hash = "sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3"}, ] [[package]] name = "types-six" version = "1.17.0.20250515" description = "Typing stubs for six" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "types_six-1.17.0.20250515-py3-none-any.whl", hash = "sha256:adfaa9568caf35e03d80ffa4ed765c33b282579c869b40bf4b6009c7d8db3fb1"}, {file = "types_six-1.17.0.20250515.tar.gz", hash = "sha256:f4f7f0398cb79304e88397336e642b15e96fbeacf5b96d7625da366b069d2d18"}, ] [[package]] name = "types-toml" version = "0.10.8.20240310" description = "Typing stubs for toml" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "types-toml-0.10.8.20240310.tar.gz", hash = "sha256:3d41501302972436a6b8b239c850b26689657e25281b48ff0ec06345b8830331"}, {file = "types_toml-0.10.8.20240310-py3-none-any.whl", hash = "sha256:627b47775d25fa29977d9c70dc0cbab3f314f32c8d8d0c012f2ef5de7aaec05d"}, ] [[package]] name = "types-typed-ast" version = "1.5.8.7" description = "Typing stubs for typed-ast" optional = false python-versions = "*" groups = ["dev"] files = [ {file = "types-typed-ast-1.5.8.7.tar.gz", hash = "sha256:f7795f6f9d597b35212314040b993f6613b51d81738edce3c1e3a3e9ef655124"}, {file = "types_typed_ast-1.5.8.7-py3-none-any.whl", hash = "sha256:97bdd9b4228f96c6904a76e10a050305ddadb529bd35e4d8234711e09c41b543"}, ] [[package]] name = "types-ujson" version = "5.10.0.20250822" description = "Typing stubs for ujson" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "types_ujson-5.10.0.20250822-py3-none-any.whl", hash = "sha256:3e9e73a6dc62ccc03449d9ac2c580cd1b7a8e4873220db498f7dd056754be080"}, {file = "types_ujson-5.10.0.20250822.tar.gz", hash = "sha256:0a795558e1f78532373cf3f03f35b1f08bc60d52d924187b97995ee3597ba006"}, ] [[package]] name = "typeshed-client" version = "2.8.2" description = "A library for accessing stubs in typeshed." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "typeshed_client-2.8.2-py3-none-any.whl", hash = "sha256:4cf886d976c777689cd31889f13abf5bfb7797c82519b07e5969e541380c75ee"}, {file = "typeshed_client-2.8.2.tar.gz", hash = "sha256:9d8e29fb74574d87bf9a719f77131dc40f2aeea20e97d25d4a3dc2cc30debd31"}, ] [package.dependencies] importlib_resources = ">=1.4.0" typing-extensions = ">=4.5.0" [[package]] name = "typing-extensions" version = "4.15.0" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" groups = ["main", "dev", "integrations"] files = [ {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, ] [[package]] name = "typing-inspect" version = "0.9.0" description = "Runtime inspection utilities for typing module." optional = false python-versions = "*" groups = ["dev"] files = [ {file = "typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f"}, {file = "typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78"}, ] [package.dependencies] mypy-extensions = ">=0.3.0" typing-extensions = ">=3.7.4" [[package]] name = "typing-inspection" version = "0.4.2" description = "Runtime typing introspection tools" optional = false python-versions = ">=3.9" groups = ["main", "dev", "integrations"] files = [ {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, ] [package.dependencies] typing-extensions = ">=4.12.0" [[package]] name = "tzdata" version = "2025.2" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" groups = ["main", "integrations"] files = [ {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, ] [[package]] name = "ujson" version = "5.11.0" description = "Ultra fast JSON encoder and decoder for Python" optional = false python-versions = ">=3.9" groups = ["main", "integrations"] markers = "sys_platform != \"win32\" and implementation_name == \"cpython\"" files = [ {file = "ujson-5.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:446e8c11c06048611c9d29ef1237065de0af07cabdd97e6b5b527b957692ec25"}, {file = "ujson-5.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:16ccb973b7ada0455201808ff11d48fe9c3f034a6ab5bd93b944443c88299f89"}, {file = "ujson-5.11.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3134b783ab314d2298d58cda7e47e7a0f7f71fc6ade6ac86d5dbeaf4b9770fa6"}, {file = "ujson-5.11.0-cp310-cp310-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:185f93ebccffebc8baf8302c869fac70dd5dd78694f3b875d03a31b03b062cdb"}, {file = "ujson-5.11.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d06e87eded62ff0e5f5178c916337d2262fdbc03b31688142a3433eabb6511db"}, {file = "ujson-5.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:181fb5b15703a8b9370b25345d2a1fd1359f0f18776b3643d24e13ed9c036d4c"}, {file = "ujson-5.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a4df61a6df0a4a8eb5b9b1ffd673429811f50b235539dac586bb7e9e91994138"}, {file = "ujson-5.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6eff24e1abd79e0ec6d7eae651dd675ddbc41f9e43e29ef81e16b421da896915"}, {file = "ujson-5.11.0-cp310-cp310-win32.whl", hash = "sha256:30f607c70091483550fbd669a0b37471e5165b317d6c16e75dba2aa967608723"}, {file = "ujson-5.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:3d2720e9785f84312b8e2cb0c2b87f1a0b1c53aaab3b2af3ab817d54409012e0"}, {file = "ujson-5.11.0-cp310-cp310-win_arm64.whl", hash = "sha256:85e6796631165f719084a9af00c79195d3ebf108151452fefdcb1c8bb50f0105"}, {file = "ujson-5.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d7c46cb0fe5e7056b9acb748a4c35aa1b428025853032540bb7e41f46767321f"}, {file = "ujson-5.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8951bb7a505ab2a700e26f691bdfacf395bc7e3111e3416d325b513eea03a58"}, {file = "ujson-5.11.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:952c0be400229940248c0f5356514123d428cba1946af6fa2bbd7503395fef26"}, {file = "ujson-5.11.0-cp311-cp311-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:94fcae844f1e302f6f8095c5d1c45a2f0bfb928cccf9f1b99e3ace634b980a2a"}, {file = "ujson-5.11.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e0ec1646db172beb8d3df4c32a9d78015e671d2000af548252769e33079d9a6"}, {file = "ujson-5.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:da473b23e3a54448b008d33f742bcd6d5fb2a897e42d1fc6e7bf306ea5d18b1b"}, {file = "ujson-5.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:aa6b3d4f1c0d3f82930f4cbd7fe46d905a4a9205a7c13279789c1263faf06dba"}, {file = "ujson-5.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4843f3ab4fe1cc596bb7e02228ef4c25d35b4bb0809d6a260852a4bfcab37ba3"}, {file = "ujson-5.11.0-cp311-cp311-win32.whl", hash = "sha256:e979fbc469a7f77f04ec2f4e853ba00c441bf2b06720aa259f0f720561335e34"}, {file = "ujson-5.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:683f57f0dd3acdd7d9aff1de0528d603aafcb0e6d126e3dc7ce8b020a28f5d01"}, {file = "ujson-5.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:7855ccea3f8dad5e66d8445d754fc1cf80265a4272b5f8059ebc7ec29b8d0835"}, {file = "ujson-5.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7895f0d2d53bd6aea11743bd56e3cb82d729980636cd0ed9b89418bf66591702"}, {file = "ujson-5.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12b5e7e22a1fe01058000d1b317d3b65cc3daf61bd2ea7a2b76721fe160fa74d"}, {file = "ujson-5.11.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0180a480a7d099082501cad1fe85252e4d4bf926b40960fb3d9e87a3a6fbbc80"}, {file = "ujson-5.11.0-cp312-cp312-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:fa79fdb47701942c2132a9dd2297a1a85941d966d8c87bfd9e29b0cf423f26cc"}, {file = "ujson-5.11.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8254e858437c00f17cb72e7a644fc42dad0ebb21ea981b71df6e84b1072aaa7c"}, {file = "ujson-5.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1aa8a2ab482f09f6c10fba37112af5f957689a79ea598399c85009f2f29898b5"}, {file = "ujson-5.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a638425d3c6eed0318df663df44480f4a40dc87cc7c6da44d221418312f6413b"}, {file = "ujson-5.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7e3cff632c1d78023b15f7e3a81c3745cd3f94c044d1e8fa8efbd6b161997bbc"}, {file = "ujson-5.11.0-cp312-cp312-win32.whl", hash = "sha256:be6b0eaf92cae8cdee4d4c9e074bde43ef1c590ed5ba037ea26c9632fb479c88"}, {file = "ujson-5.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:b7b136cc6abc7619124fd897ef75f8e63105298b5ca9bdf43ebd0e1fa0ee105f"}, {file = "ujson-5.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:6cd2df62f24c506a0ba322d5e4fe4466d47a9467b57e881ee15a31f7ecf68ff6"}, {file = "ujson-5.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:109f59885041b14ee9569bf0bb3f98579c3fa0652317b355669939e5fc5ede53"}, {file = "ujson-5.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a31c6b8004438e8c20fc55ac1c0e07dad42941db24176fe9acf2815971f8e752"}, {file = "ujson-5.11.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78c684fb21255b9b90320ba7e199780f653e03f6c2528663768965f4126a5b50"}, {file = "ujson-5.11.0-cp313-cp313-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:4c9f5d6a27d035dd90a146f7761c2272cf7103de5127c9ab9c4cd39ea61e878a"}, {file = "ujson-5.11.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:837da4d27fed5fdc1b630bd18f519744b23a0b5ada1bbde1a36ba463f2900c03"}, {file = "ujson-5.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:787aff4a84da301b7f3bac09bc696e2e5670df829c6f8ecf39916b4e7e24e701"}, {file = "ujson-5.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6dd703c3e86dc6f7044c5ac0b3ae079ed96bf297974598116aa5fb7f655c3a60"}, {file = "ujson-5.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3772e4fe6b0c1e025ba3c50841a0ca4786825a4894c8411bf8d3afe3a8061328"}, {file = "ujson-5.11.0-cp313-cp313-win32.whl", hash = "sha256:8fa2af7c1459204b7a42e98263b069bd535ea0cd978b4d6982f35af5a04a4241"}, {file = "ujson-5.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:34032aeca4510a7c7102bd5933f59a37f63891f30a0706fb46487ab6f0edf8f0"}, {file = "ujson-5.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:ce076f2df2e1aa62b685086fbad67f2b1d3048369664b4cdccc50707325401f9"}, {file = "ujson-5.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:65724738c73645db88f70ba1f2e6fb678f913281804d5da2fd02c8c5839af302"}, {file = "ujson-5.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29113c003ca33ab71b1b480bde952fbab2a0b6b03a4ee4c3d71687cdcbd1a29d"}, {file = "ujson-5.11.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c44c703842024d796b4c78542a6fcd5c3cb948b9fc2a73ee65b9c86a22ee3638"}, {file = "ujson-5.11.0-cp314-cp314-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:e750c436fb90edf85585f5c62a35b35082502383840962c6983403d1bd96a02c"}, {file = "ujson-5.11.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f278b31a7c52eb0947b2db55a5133fbc46b6f0ef49972cd1a80843b72e135aba"}, {file = "ujson-5.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ab2cb8351d976e788669c8281465d44d4e94413718af497b4e7342d7b2f78018"}, {file = "ujson-5.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:090b4d11b380ae25453100b722d0609d5051ffe98f80ec52853ccf8249dfd840"}, {file = "ujson-5.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:80017e870d882d5517d28995b62e4e518a894f932f1e242cbc802a2fd64d365c"}, {file = "ujson-5.11.0-cp314-cp314-win32.whl", hash = "sha256:1d663b96eb34c93392e9caae19c099ec4133ba21654b081956613327f0e973ac"}, {file = "ujson-5.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:849e65b696f0d242833f1df4182096cedc50d414215d1371fca85c541fbff629"}, {file = "ujson-5.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:e73df8648c9470af2b6a6bf5250d4744ad2cf3d774dcf8c6e31f018bdd04d764"}, {file = "ujson-5.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:de6e88f62796372fba1de973c11138f197d3e0e1d80bcb2b8aae1e826096d433"}, {file = "ujson-5.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e56ef8066f11b80d620985ae36869a3ff7e4b74c3b6129182ec5d1df0255f3"}, {file = "ujson-5.11.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1a325fd2c3a056cf6c8e023f74a0c478dd282a93141356ae7f16d5309f5ff823"}, {file = "ujson-5.11.0-cp314-cp314t-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:a0af6574fc1d9d53f4ff371f58c96673e6d988ed2b5bf666a6143c782fa007e9"}, {file = "ujson-5.11.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10f29e71ecf4ecd93a6610bd8efa8e7b6467454a363c3d6416db65de883eb076"}, {file = "ujson-5.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1a0a9b76a89827a592656fe12e000cf4f12da9692f51a841a4a07aa4c7ecc41c"}, {file = "ujson-5.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b16930f6a0753cdc7d637b33b4e8f10d5e351e1fb83872ba6375f1e87be39746"}, {file = "ujson-5.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:04c41afc195fd477a59db3a84d5b83a871bd648ef371cf8c6f43072d89144eef"}, {file = "ujson-5.11.0-cp314-cp314t-win32.whl", hash = "sha256:aa6d7a5e09217ff93234e050e3e380da62b084e26b9f2e277d2606406a2fc2e5"}, {file = "ujson-5.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:48055e1061c1bb1f79e75b4ac39e821f3f35a9b82de17fce92c3140149009bec"}, {file = "ujson-5.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:1194b943e951092db611011cb8dbdb6cf94a3b816ed07906e14d3bc6ce0e90ab"}, {file = "ujson-5.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:65f3c279f4ed4bf9131b11972040200c66ae040368abdbb21596bf1564899694"}, {file = "ujson-5.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:99c49400572cd77050894e16864a335225191fd72a818ea6423ae1a06467beac"}, {file = "ujson-5.11.0-cp39-cp39-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0654a2691fc252c3c525e3d034bb27b8a7546c9d3eb33cd29ce6c9feda361a6a"}, {file = "ujson-5.11.0-cp39-cp39-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:6b6ec7e7321d7fc19abdda3ad809baef935f49673951a8bab486aea975007e02"}, {file = "ujson-5.11.0-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f62b9976fabbcde3ab6e413f4ec2ff017749819a0786d84d7510171109f2d53c"}, {file = "ujson-5.11.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7f1a27ab91083b4770e160d17f61b407f587548f2c2b5fbf19f94794c495594a"}, {file = "ujson-5.11.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ecd6ff8a3b5a90c292c2396c2d63c687fd0ecdf17de390d852524393cd9ed052"}, {file = "ujson-5.11.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9aacbeb23fdbc4b256a7d12e0beb9063a1ba5d9e0dbb2cfe16357c98b4334596"}, {file = "ujson-5.11.0-cp39-cp39-win32.whl", hash = "sha256:674f306e3e6089f92b126eb2fe41bcb65e42a15432c143365c729fdb50518547"}, {file = "ujson-5.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:c6618f480f7c9ded05e78a1938873fde68baf96cdd74e6d23c7e0a8441175c4b"}, {file = "ujson-5.11.0-cp39-cp39-win_arm64.whl", hash = "sha256:5600202a731af24a25e2d7b6eb3f648e4ecd4bb67c4d5cf12f8fab31677469c9"}, {file = "ujson-5.11.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:abae0fb58cc820092a0e9e8ba0051ac4583958495bfa5262a12f628249e3b362"}, {file = "ujson-5.11.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fac6c0649d6b7c3682a0a6e18d3de6857977378dce8d419f57a0b20e3d775b39"}, {file = "ujson-5.11.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b42c115c7c6012506e8168315150d1e3f76e7ba0f4f95616f4ee599a1372bbc"}, {file = "ujson-5.11.0-pp311-pypy311_pp73-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:86baf341d90b566d61a394869ce77188cc8668f76d7bb2c311d77a00f4bdf844"}, {file = "ujson-5.11.0-pp311-pypy311_pp73-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4598bf3965fc1a936bd84034312bcbe00ba87880ef1ee33e33c1e88f2c398b49"}, {file = "ujson-5.11.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:416389ec19ef5f2013592f791486bef712ebce0cd59299bf9df1ba40bb2f6e04"}, {file = "ujson-5.11.0.tar.gz", hash = "sha256:e204ae6f909f099ba6b6b942131cee359ddda2b6e4ea39c12eb8b991fe2010e0"}, ] [[package]] name = "unidiff" version = "0.7.5" description = "Unified diff parsing/metadata extraction library." optional = false python-versions = "*" groups = ["dev"] files = [ {file = "unidiff-0.7.5-py2.py3-none-any.whl", hash = "sha256:c93bf2265cc1ba2a520e415ab05da587370bc2a3ae9e0414329f54f0c2fc09e8"}, {file = "unidiff-0.7.5.tar.gz", hash = "sha256:2e5f0162052248946b9f0970a40e9e124236bf86c82b70821143a6fc1dea2574"}, ] [[package]] name = "unittest-xml-reporting" version = "3.2.0" description = "unittest-based test runner with Ant/JUnit like XML reporting." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ {file = "unittest-xml-reporting-3.2.0.tar.gz", hash = "sha256:edd8d3170b40c3a81b8cf910f46c6a304ae2847ec01036d02e9c0f9b85762d28"}, {file = "unittest_xml_reporting-3.2.0-py2.py3-none-any.whl", hash = "sha256:f3d7402e5b3ac72a5ee3149278339db1a8f932ee405f48bcb9c681372f2717d5"}, ] [package.dependencies] lxml = "*" [[package]] name = "urllib3" version = "1.26.20" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" groups = ["main", "dev", "integrations"] files = [ {file = "urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e"}, {file = "urllib3-1.26.20.tar.gz", hash = "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32"}, ] [package.extras] brotli = ["brotli (==1.0.9) ; os_name != \"nt\" and python_version < \"3\" and platform_python_implementation == \"CPython\"", "brotli (>=1.0.9) ; python_version >= \"3\" and platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; (os_name != \"nt\" or python_version >= \"3\") and platform_python_implementation != \"CPython\"", "brotlipy (>=0.6.0) ; os_name == \"nt\" and python_version < \"3\""] secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress ; python_version == \"2.7\"", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "uvicorn" version = "0.37.0" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.9" groups = ["main", "integrations"] files = [ {file = "uvicorn-0.37.0-py3-none-any.whl", hash = "sha256:913b2b88672343739927ce381ff9e2ad62541f9f8289664fa1d1d3803fa2ce6c"}, {file = "uvicorn-0.37.0.tar.gz", hash = "sha256:4115c8add6d3fd536c8ee77f0e14a7fd2ebba939fed9b02583a97f80648f9e13"}, ] [package.dependencies] click = ">=7.0" h11 = ">=0.8" typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} [package.extras] standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] [[package]] name = "uvloop" version = "0.21.0" description = "Fast implementation of asyncio event loop on top of libuv" optional = false python-versions = ">=3.8.0" groups = ["main", "integrations"] markers = "sys_platform != \"win32\" and implementation_name == \"cpython\"" files = [ {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f"}, {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d"}, {file = "uvloop-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26"}, {file = "uvloop-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb"}, {file = "uvloop-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f"}, {file = "uvloop-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c"}, {file = "uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8"}, {file = "uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0"}, {file = "uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e"}, {file = "uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb"}, {file = "uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6"}, {file = "uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d"}, {file = "uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c"}, {file = "uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2"}, {file = "uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d"}, {file = "uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc"}, {file = "uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb"}, {file = "uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f"}, {file = "uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281"}, {file = "uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af"}, {file = "uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6"}, {file = "uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816"}, {file = "uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc"}, {file = "uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553"}, {file = "uvloop-0.21.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:17df489689befc72c39a08359efac29bbee8eee5209650d4b9f34df73d22e414"}, {file = "uvloop-0.21.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bc09f0ff191e61c2d592a752423c767b4ebb2986daa9ed62908e2b1b9a9ae206"}, {file = "uvloop-0.21.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0ce1b49560b1d2d8a2977e3ba4afb2414fb46b86a1b64056bc4ab929efdafbe"}, {file = "uvloop-0.21.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e678ad6fe52af2c58d2ae3c73dc85524ba8abe637f134bf3564ed07f555c5e79"}, {file = "uvloop-0.21.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:460def4412e473896ef179a1671b40c039c7012184b627898eea5072ef6f017a"}, {file = "uvloop-0.21.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:10da8046cc4a8f12c91a1c39d1dd1585c41162a15caaef165c2174db9ef18bdc"}, {file = "uvloop-0.21.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c097078b8031190c934ed0ebfee8cc5f9ba9642e6eb88322b9958b649750f72b"}, {file = "uvloop-0.21.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:46923b0b5ee7fc0020bef24afe7836cb068f5050ca04caf6b487c513dc1a20b2"}, {file = "uvloop-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53e420a3afe22cdcf2a0f4846e377d16e718bc70103d7088a4f7623567ba5fb0"}, {file = "uvloop-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88cb67cdbc0e483da00af0b2c3cdad4b7c61ceb1ee0f33fe00e09c81e3a6cb75"}, {file = "uvloop-0.21.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:221f4f2a1f46032b403bf3be628011caf75428ee3cc204a22addf96f586b19fd"}, {file = "uvloop-0.21.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2d1f581393673ce119355d56da84fe1dd9d2bb8b3d13ce792524e1607139feff"}, {file = "uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3"}, ] [package.extras] dev = ["Cython (>=3.0,<4.0)", "setuptools (>=60)"] docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] test = ["aiohttp (>=3.10.5)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] [[package]] name = "virtualenv" version = "20.34.0" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "virtualenv-20.34.0-py3-none-any.whl", hash = "sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026"}, {file = "virtualenv-20.34.0.tar.gz", hash = "sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a"}, ] [package.dependencies] distlib = ">=0.3.7,<1" filelock = ">=3.12.2,<4" platformdirs = ">=3.9.1,<5" typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\""} [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"GraalVM\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] [[package]] name = "wcwidth" version = "0.2.14" description = "Measures the displayed width of unicode strings in a terminal" optional = false python-versions = ">=3.6" groups = ["main", "dev", "integrations"] files = [ {file = "wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1"}, {file = "wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605"}, ] [[package]] name = "websockets" version = "15.0.1" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" optional = false python-versions = ">=3.9" groups = ["main", "integrations"] files = [ {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b"}, {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205"}, {file = "websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a"}, {file = "websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e"}, {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf"}, {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb"}, {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d"}, {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9"}, {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c"}, {file = "websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256"}, {file = "websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41"}, {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431"}, {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57"}, {file = "websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905"}, {file = "websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562"}, {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792"}, {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413"}, {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8"}, {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3"}, {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf"}, {file = "websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85"}, {file = "websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065"}, {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3"}, {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665"}, {file = "websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2"}, {file = "websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215"}, {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5"}, {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65"}, {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe"}, {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4"}, {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597"}, {file = "websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9"}, {file = "websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7"}, {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931"}, {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675"}, {file = "websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151"}, {file = "websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22"}, {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f"}, {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8"}, {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375"}, {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d"}, {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4"}, {file = "websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa"}, {file = "websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561"}, {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5"}, {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a"}, {file = "websockets-15.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b"}, {file = "websockets-15.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770"}, {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb"}, {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054"}, {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee"}, {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed"}, {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880"}, {file = "websockets-15.0.1-cp39-cp39-win32.whl", hash = "sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411"}, {file = "websockets-15.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4"}, {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3"}, {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1"}, {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475"}, {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9"}, {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04"}, {file = "websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122"}, {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940"}, {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e"}, {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9"}, {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b"}, {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f"}, {file = "websockets-15.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123"}, {file = "websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f"}, {file = "websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee"}, ] [[package]] name = "werkzeug" version = "3.1.3" description = "The comprehensive WSGI web application library." optional = false python-versions = ">=3.9" groups = ["main", "integrations"] files = [ {file = "werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e"}, {file = "werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746"}, ] [package.dependencies] MarkupSafe = ">=2.1.1" [package.extras] watchdog = ["watchdog (>=2.3)"] [[package]] name = "wheel" version = "0.45.1" description = "A built-package format for Python" optional = false python-versions = ">=3.8" groups = ["main", "integrations"] files = [ {file = "wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248"}, {file = "wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729"}, ] [package.extras] test = ["pytest (>=6.0.0)", "setuptools (>=65)"] [[package]] name = "wsproto" version = "1.2.0" description = "WebSockets state-machine based protocol implementation" optional = false python-versions = ">=3.7.0" groups = ["main", "integrations"] files = [ {file = "wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"}, {file = "wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065"}, ] [package.dependencies] h11 = ">=0.9.0,<1" [[package]] name = "xattr" version = "1.2.0" description = "Python wrapper for extended filesystem attributes" optional = false python-versions = ">=3.8" groups = ["dev"] markers = "sys_platform == \"darwin\"" files = [ {file = "xattr-1.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3df4d8d91e2996c3c72a390ec82e8544acdcb6c7df67b954f1736ff37ea4293e"}, {file = "xattr-1.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f5eec248976bbfa6c23df25d4995413df57dccf4161f6cbae36f643e99dbc397"}, {file = "xattr-1.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fafecfdedf7e8d455443bec2c3edab8a93d64672619cd1a4ee043a806152e19c"}, {file = "xattr-1.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c229e245c6c9a85d2fd7d07531498f837dd34670e556b552f73350f11edf000c"}, {file = "xattr-1.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:376631e2383918fbc3dc9bcaeb9a533e319322d2cff1c119635849edf74e1126"}, {file = "xattr-1.2.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbae24ab22afe078d549645501ecacaa17229e0b7769c8418fad69b51ad37c9"}, {file = "xattr-1.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a161160211081d765ac41fa056f4f9b1051f027f08188730fbc9782d0dce623e"}, {file = "xattr-1.2.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a542acf6c4e8221664b51b35e0160c44bd0ed1f2fd80019476f7698f4911e560"}, {file = "xattr-1.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:034f075fc5a9391a1597a6c9a21cb57b688680f0f18ecf73b2efc22b8d330cff"}, {file = "xattr-1.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:00c26c14c90058338993bb2d3e1cebf562e94ec516cafba64a8f34f74b9d18b4"}, {file = "xattr-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b4f43dc644db87d5eb9484a9518c34a864cb2e588db34cffc42139bf55302a1c"}, {file = "xattr-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c7602583fc643ca76576498e2319c7cef0b72aef1936701678589da6371b731b"}, {file = "xattr-1.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90c3ad4a9205cceb64ec54616aa90aa42d140c8ae3b9710a0aaa2843a6f1aca7"}, {file = "xattr-1.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83d87cfe19cd606fc0709d45a4d6efc276900797deced99e239566926a5afedf"}, {file = "xattr-1.2.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c67dabd9ddc04ead63fbc85aed459c9afcc24abfc5bb3217fff7ec9a466faacb"}, {file = "xattr-1.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9a18ee82d8ba2c17f1e8414bfeb421fa763e0fb4acbc1e124988ca1584ad32d5"}, {file = "xattr-1.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:38de598c47b85185e745986a061094d2e706e9c2d9022210d2c738066990fe91"}, {file = "xattr-1.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:15e754e854bdaac366ad3f1c8fbf77f6668e8858266b4246e8c5f487eeaf1179"}, {file = "xattr-1.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:daff0c1f5c5e4eaf758c56259c4f72631fa9619875e7a25554b6077dc73da964"}, {file = "xattr-1.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:109b11fb3f73a0d4e199962f11230ab5f462e85a8021874f96c1732aa61148d5"}, {file = "xattr-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7c7c12968ce0bf798d8ba90194cef65de768bee9f51a684e022c74cab4218305"}, {file = "xattr-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37989dabf25ff18773e4aaeebcb65604b9528f8645f43e02bebaa363e3ae958"}, {file = "xattr-1.2.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:165de92b0f2adafb336f936931d044619b9840e35ba01079f4dd288747b73714"}, {file = "xattr-1.2.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82191c006ae4c609b22b9aea5f38f68fff022dc6884c4c0e1dba329effd4b288"}, {file = "xattr-1.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2b2e9c87dc643b09d86befad218e921f6e65b59a4668d6262b85308de5dbd1dd"}, {file = "xattr-1.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:14edd5d47d0bb92b23222c0bb6379abbddab01fb776b2170758e666035ecf3aa"}, {file = "xattr-1.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:12183d5eb104d4da787638c7dadf63b718472d92fec6dbe12994ea5d094d7863"}, {file = "xattr-1.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c385ea93a18aeb6443a719eb6a6b1d7f7b143a4d1f2b08bc4fadfc429209e629"}, {file = "xattr-1.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2d39d7b36842c67ab3040bead7eb6d601e35fa0d6214ed20a43df4ec30b6f9f9"}, {file = "xattr-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:320ef856bb817f4c40213b6de956dc440d0f23cdc62da3ea02239eb5147093f8"}, {file = "xattr-1.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26d306bfb3b5641726f2ee0da6f63a2656aa7fdcfd15de61c476e3ca6bc3277e"}, {file = "xattr-1.2.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c67e70d5d8136d328ad13f85b887ffa97690422f1a11fb29ab2f702cf66e825a"}, {file = "xattr-1.2.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8904d3539afe1a84fc0b7f02fa91da60d2505adf2d5951dc855bf9e75fe322b2"}, {file = "xattr-1.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2520516c1d058895eae00b2b2f10833514caea6dc6802eef1e431c474b5317ad"}, {file = "xattr-1.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:29d06abbef4024b7469fcd0d4ade6d2290582350a4df95fcc48fa48b2e83246b"}, {file = "xattr-1.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:093c75f7d9190be355b8e86da3f460b9bfe3d6a176f92852d44dcc3289aa10dc"}, {file = "xattr-1.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ee3901db48de913dcef004c5d7b477a1f4aadff997445ef62907b10fdad57de"}, {file = "xattr-1.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b837898a5225c7f7df731783cd78bae2ed81b84bacf020821f1cd2ab2d74de58"}, {file = "xattr-1.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cedc281811e424ecf6a14208532f7ac646866f91f88e8eadd00d8fe535e505fd"}, {file = "xattr-1.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf60577caa248f539e4e646090b10d6ad1f54189de9a7f1854c23fdef28f574e"}, {file = "xattr-1.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:363724f33510d2e7c7e080b389271a1241cb4929a1d9294f89721152b4410972"}, {file = "xattr-1.2.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97db00596865845efb72f3d565a1f82b01006c5bf5a87d8854a6afac43502593"}, {file = "xattr-1.2.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:0b199ba31078f3e4181578595cd60400ee055b4399672169ceee846d33ff26de"}, {file = "xattr-1.2.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:b19472dc38150ac09a478c71092738d86882bc9ff687a4a8f7d1a25abce20b5e"}, {file = "xattr-1.2.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:79f7823b30ed557e0e7ffd9a6b1a821a22f485f5347e54b8d24c4a34b7545ba4"}, {file = "xattr-1.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8eee258f5774933cb972cff5c3388166374e678980d2a1f417d7d6f61d9ae172"}, {file = "xattr-1.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2a9de621eadf0466c391363bd6ed903b1a1bcd272422b5183fd06ef79d05347b"}, {file = "xattr-1.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc714f236f17c57c510ae9ada9962d8e4efc9f9ea91504e2c6a09008f3918ddf"}, {file = "xattr-1.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:545e0ad3f706724029efd23dec58fb358422ae68ab4b560b712aedeaf40446a0"}, {file = "xattr-1.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:200bb3cdba057cb721b727607bc340a74c28274f4a628a26011f574860f5846b"}, {file = "xattr-1.2.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b0b27c889cc9ff0dba62ac8a2eef98f4911c1621e4e8c409d5beb224c4c227c"}, {file = "xattr-1.2.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ea7cf8afd717853ad78eba8ca83ff66a53484ba2bb2a4283462bc5c767518174"}, {file = "xattr-1.2.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:02fa813db054bbb7a61c570ae025bd01c36fc20727b40f49031feb930234bc72"}, {file = "xattr-1.2.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2827e23d7a1a20f31162c47ab4bd341a31e83421121978c4ab2aad5cd79ea82b"}, {file = "xattr-1.2.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:29ae44247d46e63671311bf7e700826a97921278e2c0c04c2d11741888db41b8"}, {file = "xattr-1.2.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:629c42c1dd813442d90f281f69b88ef0c9625f604989bef8411428671f70f43e"}, {file = "xattr-1.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:549f8fbda5da48cafc81ba6ab7bb8e8e14c4b0748c37963dc504bcae505474b7"}, {file = "xattr-1.2.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa83e677b5f92a3c5c86eaf875e9d3abbc43887ff1767178def865fa9f12a3a0"}, {file = "xattr-1.2.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb669f01627962ce2bc556f19d421162247bc2cad0d4625d6ea5eb32af4cf29b"}, {file = "xattr-1.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:212156aa5fb987a53211606bc09e6fea3eda3855af9f2940e40df5a2a592425a"}, {file = "xattr-1.2.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:7dc4fa9448a513077c5ccd1ce428ff0682cdddfc71301dbbe4ee385c74517f73"}, {file = "xattr-1.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e4b93f2e74793b61c0a7b7bdef4a3813930df9c01eda72fad706b8db7658bc2"}, {file = "xattr-1.2.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dddd5f6d0bb95b099d6a3888c248bf246525647ccb8cf9e8f0fc3952e012d6fb"}, {file = "xattr-1.2.0-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68fbdffebe8c398a82c84ecf5e6f6a3adde9364f891cba066e58352af404a45c"}, {file = "xattr-1.2.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c9ee84de7cd4a6d61b0b79e2f58a6bdb13b03dbad948489ebb0b73a95caee7ae"}, {file = "xattr-1.2.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5594fcbc38fdbb3af16a8ad18c37c81c8814955f0d636be857a67850cd556490"}, {file = "xattr-1.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:017aac8005e1e84d5efa4b86c0896c6eb96f2331732d388600a5b999166fec1c"}, {file = "xattr-1.2.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d27a64f695440450c119ae4bc8f54b0b726a812ebea1666fff3873236936f36"}, {file = "xattr-1.2.0-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f7e7067e1a400ad4485536a9e84c3330373086b2324fafa26d07527eeb4b175"}, {file = "xattr-1.2.0.tar.gz", hash = "sha256:a64c8e21eff1be143accf80fd3b8fde3e28a478c37da298742af647ac3e5e0a7"}, ] [package.dependencies] cffi = ">=1.16.0" [package.extras] test = ["pytest"] [[package]] name = "xmod" version = "1.8.1" description = "🌱 Turn any object into a module 🌱" optional = false python-versions = ">=3.8" groups = ["main", "dev", "integrations"] files = [ {file = "xmod-1.8.1-py3-none-any.whl", hash = "sha256:a24e9458a4853489042522bdca9e50ee2eac5ab75c809a91150a8a7f40670d48"}, {file = "xmod-1.8.1.tar.gz", hash = "sha256:38c76486b9d672c546d57d8035df0beb7f4a9b088bc3fb2de5431ae821444377"}, ] [[package]] name = "yarl" version = "1.22.0" description = "Yet another URL library" optional = false python-versions = ">=3.9" groups = ["main", "integrations"] files = [ {file = "yarl-1.22.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e"}, {file = "yarl-1.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f"}, {file = "yarl-1.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07a524d84df0c10f41e3ee918846e1974aba4ec017f990dc735aad487a0bdfdf"}, {file = "yarl-1.22.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1b329cb8146d7b736677a2440e422eadd775d1806a81db2d4cded80a48efc1a"}, {file = "yarl-1.22.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:75976c6945d85dbb9ee6308cd7ff7b1fb9409380c82d6119bd778d8fcfe2931c"}, {file = "yarl-1.22.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:80ddf7a5f8c86cb3eb4bc9028b07bbbf1f08a96c5c0bc1244be5e8fefcb94147"}, {file = "yarl-1.22.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d332fc2e3c94dad927f2112395772a4e4fedbcf8f80efc21ed7cdfae4d574fdb"}, {file = "yarl-1.22.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cf71bf877efeac18b38d3930594c0948c82b64547c1cf420ba48722fe5509f6"}, {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:663e1cadaddae26be034a6ab6072449a8426ddb03d500f43daf952b74553bba0"}, {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6dcbb0829c671f305be48a7227918cfcd11276c2d637a8033a99a02b67bf9eda"}, {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f0d97c18dfd9a9af4490631905a3f131a8e4c9e80a39353919e2cfed8f00aedc"}, {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:437840083abe022c978470b942ff832c3940b2ad3734d424b7eaffcd07f76737"}, {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a899cbd98dce6f5d8de1aad31cb712ec0a530abc0a86bd6edaa47c1090138467"}, {file = "yarl-1.22.0-cp310-cp310-win32.whl", hash = "sha256:595697f68bd1f0c1c159fcb97b661fc9c3f5db46498043555d04805430e79bea"}, {file = "yarl-1.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca"}, {file = "yarl-1.22.0-cp310-cp310-win_arm64.whl", hash = "sha256:b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b"}, {file = "yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511"}, {file = "yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6"}, {file = "yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028"}, {file = "yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d"}, {file = "yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503"}, {file = "yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65"}, {file = "yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e"}, {file = "yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d"}, {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7"}, {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967"}, {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed"}, {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6"}, {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e"}, {file = "yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca"}, {file = "yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b"}, {file = "yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376"}, {file = "yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f"}, {file = "yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2"}, {file = "yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74"}, {file = "yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df"}, {file = "yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb"}, {file = "yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2"}, {file = "yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82"}, {file = "yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a"}, {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124"}, {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa"}, {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7"}, {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d"}, {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520"}, {file = "yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8"}, {file = "yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c"}, {file = "yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74"}, {file = "yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53"}, {file = "yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a"}, {file = "yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c"}, {file = "yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601"}, {file = "yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a"}, {file = "yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df"}, {file = "yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2"}, {file = "yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b"}, {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273"}, {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a"}, {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d"}, {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02"}, {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67"}, {file = "yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95"}, {file = "yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d"}, {file = "yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b"}, {file = "yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10"}, {file = "yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3"}, {file = "yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9"}, {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f"}, {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0"}, {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e"}, {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708"}, {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f"}, {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d"}, {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8"}, {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5"}, {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f"}, {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62"}, {file = "yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03"}, {file = "yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249"}, {file = "yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b"}, {file = "yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4"}, {file = "yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683"}, {file = "yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b"}, {file = "yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e"}, {file = "yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590"}, {file = "yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2"}, {file = "yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da"}, {file = "yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784"}, {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b"}, {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694"}, {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d"}, {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd"}, {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da"}, {file = "yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2"}, {file = "yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79"}, {file = "yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33"}, {file = "yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1"}, {file = "yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca"}, {file = "yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53"}, {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c"}, {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf"}, {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face"}, {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b"}, {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486"}, {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138"}, {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a"}, {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529"}, {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093"}, {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c"}, {file = "yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e"}, {file = "yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27"}, {file = "yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1"}, {file = "yarl-1.22.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3aa27acb6de7a23785d81557577491f6c38a5209a254d1191519d07d8fe51748"}, {file = "yarl-1.22.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:af74f05666a5e531289cb1cc9c883d1de2088b8e5b4de48004e5ca8a830ac859"}, {file = "yarl-1.22.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:62441e55958977b8167b2709c164c91a6363e25da322d87ae6dd9c6019ceecf9"}, {file = "yarl-1.22.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b580e71cac3f8113d3135888770903eaf2f507e9421e5697d6ee6d8cd1c7f054"}, {file = "yarl-1.22.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e81fda2fb4a07eda1a2252b216aa0df23ebcd4d584894e9612e80999a78fd95b"}, {file = "yarl-1.22.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:99b6fc1d55782461b78221e95fc357b47ad98b041e8e20f47c1411d0aacddc60"}, {file = "yarl-1.22.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:088e4e08f033db4be2ccd1f34cf29fe994772fb54cfe004bbf54db320af56890"}, {file = "yarl-1.22.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e4e1f6f0b4da23e61188676e3ed027ef0baa833a2e633c29ff8530800edccba"}, {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:84fc3ec96fce86ce5aa305eb4aa9358279d1aa644b71fab7b8ed33fe3ba1a7ca"}, {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5dbeefd6ca588b33576a01b0ad58aa934bc1b41ef89dee505bf2932b22ddffba"}, {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:14291620375b1060613f4aab9ebf21850058b6b1b438f386cc814813d901c60b"}, {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:a4fcfc8eb2c34148c118dfa02e6427ca278bfd0f3df7c5f99e33d2c0e81eae3e"}, {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:029866bde8d7b0878b9c160e72305bbf0a7342bcd20b9999381704ae03308dc8"}, {file = "yarl-1.22.0-cp39-cp39-win32.whl", hash = "sha256:4dcc74149ccc8bba31ce1944acee24813e93cfdee2acda3c172df844948ddf7b"}, {file = "yarl-1.22.0-cp39-cp39-win_amd64.whl", hash = "sha256:10619d9fdee46d20edc49d3479e2f8269d0779f1b031e6f7c2aa1c76be04b7ed"}, {file = "yarl-1.22.0-cp39-cp39-win_arm64.whl", hash = "sha256:dd7afd3f8b0bfb4e0d9fc3c31bfe8a4ec7debe124cfd90619305def3c8ca8cd2"}, {file = "yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff"}, {file = "yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71"}, ] [package.dependencies] idna = ">=2.0" multidict = ">=4.0" propcache = ">=0.2.1" [[package]] name = "z3-solver" version = "4.15.3.0" description = "an efficient SMT solver library" optional = false python-versions = "*" groups = ["dev"] files = [ {file = "z3_solver-4.15.3.0-py3-none-macosx_13_0_arm64.whl", hash = "sha256:65335aab295ded7c0ce27c85556067087a87052389ff160777d1a1d48ef0d74f"}, {file = "z3_solver-4.15.3.0-py3-none-macosx_13_0_x86_64.whl", hash = "sha256:3e62e93adff2def3537ff1ca67c3d58a6ca6d1944e0b5e774f88627b199d50e7"}, {file = "z3_solver-4.15.3.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9afd9ceb290482097474d43f08415bcc1874f433189d1449f6c1508e9c68384"}, {file = "z3_solver-4.15.3.0-py3-none-manylinux_2_34_aarch64.whl", hash = "sha256:f61ef44552489077eedd7e6d9bed52ef1875decf86d66027742099a2703b1c77"}, {file = "z3_solver-4.15.3.0-py3-none-win32.whl", hash = "sha256:0c603f6bad7423d6411adda6af55030b725e3d30f54ea91b714abcedd73b848a"}, {file = "z3_solver-4.15.3.0-py3-none-win_amd64.whl", hash = "sha256:06abdf6c36f97c463aea827533504fd59476d015a65cf170a88bd6a53ba13ab5"}, {file = "z3_solver-4.15.3.0.tar.gz", hash = "sha256:78f69aebda5519bfd8af146a129f36cf4721a3c2667e80d9fe35cc9bb4d214a6"}, ] [[package]] name = "zipp" version = "3.23.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, ] [package.extras] check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] type = ["pytest-mypy"] [[package]] name = "zope-interface" version = "8.0.1" description = "Interfaces for Python" optional = false python-versions = ">=3.9" groups = ["integrations"] files = [ {file = "zope_interface-8.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fd7195081b8637eeed8d73e4d183b07199a1dc738fb28b3de6666b1b55662570"}, {file = "zope_interface-8.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f7c4bc4021108847bce763673ce70d0716b08dfc2ba9889e7bad46ac2b3bb924"}, {file = "zope_interface-8.0.1-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:758803806b962f32c87b31bb18c298b022965ba34fe532163831cc39118c24ab"}, {file = "zope_interface-8.0.1-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f8e88f35f86bbe8243cad4b2972deef0fdfca0a0723455abbebdc83bbab96b69"}, {file = "zope_interface-8.0.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7844765695937d9b0d83211220b72e2cf6ac81a08608ad2b58f2c094af498d83"}, {file = "zope_interface-8.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:64fa7b206dd9669f29d5c1241a768bebe8ab1e8a4b63ee16491f041e058c09d0"}, {file = "zope_interface-8.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4bd01022d2e1bce4a4a4ed9549edb25393c92e607d7daa6deff843f1f68b479d"}, {file = "zope_interface-8.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:29be8db8b712d94f1c05e24ea230a879271d787205ba1c9a6100d1d81f06c69a"}, {file = "zope_interface-8.0.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:51ae1b856565b30455b7879fdf0a56a88763b401d3f814fa9f9542d7410dbd7e"}, {file = "zope_interface-8.0.1-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d2e7596149cb1acd1d4d41b9f8fe2ffc0e9e29e2e91d026311814181d0d9efaf"}, {file = "zope_interface-8.0.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b2737c11c34fb9128816759864752d007ec4f987b571c934c30723ed881a7a4f"}, {file = "zope_interface-8.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:cf66e4bf731aa7e0ced855bb3670e8cda772f6515a475c6a107bad5cb6604103"}, {file = "zope_interface-8.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:115f27c1cc95ce7a517d960ef381beedb0a7ce9489645e80b9ab3cbf8a78799c"}, {file = "zope_interface-8.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af655c573b84e3cb6a4f6fd3fbe04e4dc91c63c6b6f99019b3713ef964e589bc"}, {file = "zope_interface-8.0.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:23f82ef9b2d5370750cc1bf883c3b94c33d098ce08557922a3fbc7ff3b63dfe1"}, {file = "zope_interface-8.0.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35a1565d5244997f2e629c5c68715b3d9d9036e8df23c4068b08d9316dcb2822"}, {file = "zope_interface-8.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:029ea1db7e855a475bf88d9910baab4e94d007a054810e9007ac037a91c67c6f"}, {file = "zope_interface-8.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0beb3e7f7dc153944076fcaf717a935f68d39efa9fce96ec97bafcc0c2ea6cab"}, {file = "zope_interface-8.0.1-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:c7cc027fc5c61c5d69e5080c30b66382f454f43dc379c463a38e78a9c6bab71a"}, {file = "zope_interface-8.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fcf9097ff3003b7662299f1c25145e15260ec2a27f9a9e69461a585d79ca8552"}, {file = "zope_interface-8.0.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6d965347dd1fb9e9a53aa852d4ded46b41ca670d517fd54e733a6b6a4d0561c2"}, {file = "zope_interface-8.0.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9a3b8bb77a4b89427a87d1e9eb969ab05e38e6b4a338a9de10f6df23c33ec3c2"}, {file = "zope_interface-8.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:87e6b089002c43231fb9afec89268391bcc7a3b66e76e269ffde19a8112fb8d5"}, {file = "zope_interface-8.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:64a43f5280aa770cbafd0307cb3d1ff430e2a1001774e8ceb40787abe4bb6658"}, {file = "zope_interface-8.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b84464a9fcf801289fa8b15bfc0829e7855d47fb4a8059555effc6f2d1d9a613"}, {file = "zope_interface-8.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7b915cf7e747b5356d741be79a153aa9107e8923bc93bcd65fc873caf0fb5c50"}, {file = "zope_interface-8.0.1-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:110c73ddf974b369ef3c6e7b0d87d44673cf4914eba3fe8a33bfb21c6c606ad8"}, {file = "zope_interface-8.0.1-cp39-cp39-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9e9bdca901c1bcc34e438001718512c65b3b8924aabcd732b6e7a7f0cd715f17"}, {file = "zope_interface-8.0.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bbd22d4801ad3e8ec704ba9e3e6a4ac2e875e4d77e363051ccb76153d24c5519"}, {file = "zope_interface-8.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:a0016ca85f93b938824e2f9a43534446e95134a2945b084944786e1ace2020bc"}, {file = "zope_interface-8.0.1.tar.gz", hash = "sha256:eba5610d042c3704a48222f7f7c6ab5b243ed26f917e2bc69379456b115e02d1"}, ] [package.extras] docs = ["Sphinx", "furo", "repoze.sphinx.autointerface"] test = ["coverage[toml]", "zope.event", "zope.testing"] testing = ["coverage[toml]", "zope.event", "zope.testing"] [[package]] name = "zstandard" version = "0.25.0" description = "Zstandard bindings for Python" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {file = "zstandard-0.25.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e59fdc271772f6686e01e1b3b74537259800f57e24280be3f29c8a0deb1904dd"}, {file = "zstandard-0.25.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4d441506e9b372386a5271c64125f72d5df6d2a8e8a2a45a0ae09b03cb781ef7"}, {file = "zstandard-0.25.0-cp310-cp310-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:ab85470ab54c2cb96e176f40342d9ed41e58ca5733be6a893b730e7af9c40550"}, {file = "zstandard-0.25.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e05ab82ea7753354bb054b92e2f288afb750e6b439ff6ca78af52939ebbc476d"}, {file = "zstandard-0.25.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:78228d8a6a1c177a96b94f7e2e8d012c55f9c760761980da16ae7546a15a8e9b"}, {file = "zstandard-0.25.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b6bd67528ee8b5c5f10255735abc21aa106931f0dbaf297c7be0c886353c3d0"}, {file = "zstandard-0.25.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4b6d83057e713ff235a12e73916b6d356e3084fd3d14ced499d84240f3eecee0"}, {file = "zstandard-0.25.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9174f4ed06f790a6869b41cba05b43eeb9a35f8993c4422ab853b705e8112bbd"}, {file = "zstandard-0.25.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:25f8f3cd45087d089aef5ba3848cd9efe3ad41163d3400862fb42f81a3a46701"}, {file = "zstandard-0.25.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3756b3e9da9b83da1796f8809dd57cb024f838b9eeafde28f3cb472012797ac1"}, {file = "zstandard-0.25.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:81dad8d145d8fd981b2962b686b2241d3a1ea07733e76a2f15435dfb7fb60150"}, {file = "zstandard-0.25.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a5a419712cf88862a45a23def0ae063686db3d324cec7edbe40509d1a79a0aab"}, {file = "zstandard-0.25.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e7360eae90809efd19b886e59a09dad07da4ca9ba096752e61a2e03c8aca188e"}, {file = "zstandard-0.25.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:75ffc32a569fb049499e63ce68c743155477610532da1eb38e7f24bf7cd29e74"}, {file = "zstandard-0.25.0-cp310-cp310-win32.whl", hash = "sha256:106281ae350e494f4ac8a80470e66d1fe27e497052c8d9c3b95dc4cf1ade81aa"}, {file = "zstandard-0.25.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea9d54cc3d8064260114a0bbf3479fc4a98b21dffc89b3459edd506b69262f6e"}, {file = "zstandard-0.25.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c"}, {file = "zstandard-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f"}, {file = "zstandard-0.25.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431"}, {file = "zstandard-0.25.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a"}, {file = "zstandard-0.25.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc"}, {file = "zstandard-0.25.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6"}, {file = "zstandard-0.25.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072"}, {file = "zstandard-0.25.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277"}, {file = "zstandard-0.25.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313"}, {file = "zstandard-0.25.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097"}, {file = "zstandard-0.25.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c8e167d5adf59476fa3e37bee730890e389410c354771a62e3c076c86f9f7778"}, {file = "zstandard-0.25.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065"}, {file = "zstandard-0.25.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa"}, {file = "zstandard-0.25.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7"}, {file = "zstandard-0.25.0-cp311-cp311-win32.whl", hash = "sha256:f5aeea11ded7320a84dcdd62a3d95b5186834224a9e55b92ccae35d21a8b63d4"}, {file = "zstandard-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:daab68faadb847063d0c56f361a289c4f268706b598afbf9ad113cbe5c38b6b2"}, {file = "zstandard-0.25.0-cp311-cp311-win_arm64.whl", hash = "sha256:22a06c5df3751bb7dc67406f5374734ccee8ed37fc5981bf1ad7041831fa1137"}, {file = "zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b"}, {file = "zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00"}, {file = "zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64"}, {file = "zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea"}, {file = "zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb"}, {file = "zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a"}, {file = "zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902"}, {file = "zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f"}, {file = "zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b"}, {file = "zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6"}, {file = "zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91"}, {file = "zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708"}, {file = "zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512"}, {file = "zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa"}, {file = "zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd"}, {file = "zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01"}, {file = "zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9"}, {file = "zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94"}, {file = "zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1"}, {file = "zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f"}, {file = "zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea"}, {file = "zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e"}, {file = "zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551"}, {file = "zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a"}, {file = "zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611"}, {file = "zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3"}, {file = "zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b"}, {file = "zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851"}, {file = "zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250"}, {file = "zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98"}, {file = "zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf"}, {file = "zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09"}, {file = "zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5"}, {file = "zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049"}, {file = "zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3"}, {file = "zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f"}, {file = "zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c"}, {file = "zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439"}, {file = "zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043"}, {file = "zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859"}, {file = "zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0"}, {file = "zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7"}, {file = "zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2"}, {file = "zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344"}, {file = "zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c"}, {file = "zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088"}, {file = "zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12"}, {file = "zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2"}, {file = "zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d"}, {file = "zstandard-0.25.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b9af1fe743828123e12b41dd8091eca1074d0c1569cc42e6e1eee98027f2bbd0"}, {file = "zstandard-0.25.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4b14abacf83dfb5c25eb4e4a79520de9e7e205f72c9ee7702f91233ae57d33a2"}, {file = "zstandard-0.25.0-cp39-cp39-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:a51ff14f8017338e2f2e5dab738ce1ec3b5a851f23b18c1ae1359b1eecbee6df"}, {file = "zstandard-0.25.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3b870ce5a02d4b22286cf4944c628e0f0881b11b3f14667c1d62185a99e04f53"}, {file = "zstandard-0.25.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:05353cef599a7b0b98baca9b068dd36810c3ef0f42bf282583f438caf6ddcee3"}, {file = "zstandard-0.25.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:19796b39075201d51d5f5f790bf849221e58b48a39a5fc74837675d8bafc7362"}, {file = "zstandard-0.25.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:53e08b2445a6bc241261fea89d065536f00a581f02535f8122eba42db9375530"}, {file = "zstandard-0.25.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1f3689581a72eaba9131b1d9bdbfe520ccd169999219b41000ede2fca5c1bfdb"}, {file = "zstandard-0.25.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d8c56bb4e6c795fc77d74d8e8b80846e1fb8292fc0b5060cd8131d522974b751"}, {file = "zstandard-0.25.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:53f94448fe5b10ee75d246497168e5825135d54325458c4bfffbaafabcc0a577"}, {file = "zstandard-0.25.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:c2ba942c94e0691467ab901fc51b6f2085ff48f2eea77b1a48240f011e8247c7"}, {file = "zstandard-0.25.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:07b527a69c1e1c8b5ab1ab14e2afe0675614a09182213f21a0717b62027b5936"}, {file = "zstandard-0.25.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:51526324f1b23229001eb3735bc8c94f9c578b1bd9e867a0a646a3b17109f388"}, {file = "zstandard-0.25.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89c4b48479a43f820b749df49cd7ba2dbc2b1b78560ecb5ab52985574fd40b27"}, {file = "zstandard-0.25.0-cp39-cp39-win32.whl", hash = "sha256:1cd5da4d8e8ee0e88be976c294db744773459d51bb32f707a0f166e5ad5c8649"}, {file = "zstandard-0.25.0-cp39-cp39-win_amd64.whl", hash = "sha256:37daddd452c0ffb65da00620afb8e17abd4adaae6ce6310702841760c2c26860"}, {file = "zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b"}, ] [package.extras] cffi = ["cffi (>=1.17,<2.0) ; platform_python_implementation != \"PyPy\" and python_version < \"3.14\"", "cffi (>=2.0.0b) ; platform_python_implementation != \"PyPy\" and python_version >= \"3.14\""] [extras] aiohttp = ["aiohttp"] asgi = ["python-multipart", "starlette"] chalice = ["chalice"] channels = ["asgiref", "channels"] cli = ["libcst", "pygments", "python-multipart", "rich", "starlette", "typer", "uvicorn", "websockets"] debug = ["libcst", "rich"] debug-server = ["libcst", "pygments", "python-multipart", "rich", "starlette", "typer", "uvicorn", "websockets"] django = ["Django", "asgiref"] fastapi = ["fastapi", "python-multipart"] flask = ["flask"] litestar = ["litestar"] opentelemetry = ["opentelemetry-api", "opentelemetry-sdk"] pydantic = ["pydantic"] pyinstrument = ["pyinstrument"] quart = ["quart"] sanic = ["sanic"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" content-hash = "5cf0ff0ad77dc736874c34e4b1084d8257d168d136eab0f2b0326120ca576ae9" strawberry-graphql-0.287.0/pyproject.toml000066400000000000000000000223211511033167500204460ustar00rootroot00000000000000[project] name = "strawberry-graphql" version = "0.287.0" description = "A library for creating GraphQL APIs" authors = [{ name = "Patrick Arminio", email = "patrick.arminio@gmail.com" }] license = { text = "MIT" } readme = "README.md" keywords = ["graphql", "api", "rest", "starlette", "async"] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules", "License :: OSI Approved :: MIT License", ] requires-python = ">=3.10,<4.0" dependencies = [ "graphql-core>=3.2.0,<3.4.0", "typing-extensions>=4.5.0", "python-dateutil~=2.7", "packaging>=23", "lia-web (>=0.2.1)", ] [project.urls] homepage = "https://strawberry.rocks/" repository = "https://github.com/strawberry-graphql/strawberry" documentation = "https://strawberry.rocks/" "Changelog" = "https://strawberry.rocks/changelog" "Discord" = "https://discord.com/invite/3uQ2PaY" "Twitter" = "https://twitter.com/strawberry_gql" "Mastodon" = "https://farbun.social/@strawberry" "Sponsor on GitHub" = "https://github.com/sponsors/strawberry-graphql" "Sponsor on Open Collective" = "https://opencollective.com/strawberry-graphql" [project.scripts] strawberry = "strawberry.cli:run" [project.optional-dependencies] aiohttp = ["aiohttp>=3.7.4.post0,<4"] asgi = ["starlette>=0.18.0", "python-multipart>=0.0.7"] debug = ["rich>=12.0.0", "libcst"] debug-server = [ "starlette>=0.18.0", "uvicorn>=0.11.6", "websockets>=15.0.1,<16", "python-multipart>=0.0.7", "typer>=0.12.4", "pygments~=2.3", "rich>=12.0.0", "libcst", ] django = ["Django>=3.2", "asgiref~=3.2"] channels = ["channels>=3.0.5", "asgiref~=3.2"] flask = ["flask>=1.1"] quart = ["quart>=0.19.3"] opentelemetry = ["opentelemetry-api<2", "opentelemetry-sdk<2"] pydantic = ["pydantic>1.6.1"] sanic = ["sanic>=20.12.2"] fastapi = ["fastapi>=0.65.2", "python-multipart>=0.0.7"] chalice = ["chalice~=1.22"] cli = [ "typer>=0.12.4", "pygments~=2.3", "rich>=12.0.0", "libcst", "starlette>=0.18.0", "uvicorn>=0.11.6", "websockets>=15.0.1,<16", "python-multipart>=0.0.7", ] litestar = ["litestar>=2; python_version~='3.10'"] pyinstrument = ["pyinstrument>=4.0.0"] [dependency-groups] dev = [ "ruff (>=0.14.1,<0.15.0)", "asgiref (>=3.2,<4.0)", "email-validator (>=1.1.3,<3.0.0)", "freezegun (>=1.2.1,<2.0.0)", "libcst (>=1.8.2,<2.0.0)", "markupsafe (==2.1.3)", "nox (>=2024.4.15,<2025.0.0)", "nox-poetry (>=1.0.3,<2.0.0)", "opentelemetry-api (<2)", "opentelemetry-sdk (<2)", "pygments (>=2.3,<3.0)", "pyinstrument (>=4.0.0)", "pytest (>=7.2,<8.0)", "pytest-asyncio (>=0.20.3)", "pytest-codspeed (>=3.0.0)", "pytest-cov (>=4.0.0,<5.0.0)", "pytest-emoji (>=0.2.0,<0.3.0)", "pytest-mock (>=3.10,<4.0)", "pytest-snapshot (>=0.9.0,<0.10.0)", "pytest-xdist[psutil] (>=3.1.0,<4.0.0)", "python-multipart (>=0.0.7)", "rich (>=12.5.1)", "sanic-testing (>=22.9,<24.0)", "typer (>=0.12.4)", "types-aiofiles (>=24.1.0.20250326,<25.0.0.0)", "types-certifi (>=2021.10.8,<2022.0.0)", "types-chardet (>=5.0.4,<6.0.0)", "types-freezegun (>=1.1.9,<2.0.0)", "types-python-dateutil (>=2.8.19,<3.0.0)", "types-toml (>=0.10.8,<0.11.0)", "types-typed-ast (>=1.5.8,<2.0.0)", "types-ujson (>=5.10.0.20250326,<6.0.0.0)", "types-protobuf (>=5.29.1.20250403,<6.0.0.0)", "poetry-plugin-export (>=1.6.0,<2.0.0)", # another bug in poetry "urllib3 (<2)", "inline-snapshot (>=0.10.1,<0.11.0)", "types-deprecated (>=1.2.15.20241117,<2.0.0.0)", "types-six (>=1.17.0.20250403,<2.0.0.0)", "types-pyyaml (>=6.0.12.20240917,<7.0.0.0)", "mypy (>=1.15.0,<2.0.0)", "pyright (==1.1.401)", "codeflash (>=0.9.2)", "pre-commit", ] integrations = [ "aiohttp (>=3.7.4.post0,<4.0.0)", "chalice (>=1.22,<2.0)", "channels (>=3.0.5,<5.0.0)", "django (>=3.2)", "fastapi (>=0.65.0)", "flask (>=1.1)", "quart (>=0.19.3)", "pydantic (>=2.0)", "pytest-aiohttp (>=1.0.3,<2.0.0)", "pytest-django (>=4.5,<5.0)", "sanic (>=20.12.2)", "starlette (>=0.13.6)", "litestar (>=2) ; python_version >= \"3.10\"", "uvicorn (>=0.11.6)", "daphne (>=4.0.0,<5.0.0)", ] [tool.poetry] packages = [{ include = "strawberry" }] include = ["strawberry/py.typed"] [tool.pytest.ini_options] addopts = "--emoji" DJANGO_SETTINGS_MODULE = "tests.django.django_settings" testpaths = ["tests/"] markers = [ "aiohttp", "asgi", "chalice", "channels", "django_db", "django", "fastapi", "flaky", "flask", "litestar", "pydantic", "quart", "relay", "sanic", "starlette", ] asyncio_mode = "auto" filterwarnings = [ "ignore::DeprecationWarning:strawberry.*.resolver", "ignore:LazyType is deprecated:DeprecationWarning", "ignore::DeprecationWarning:ddtrace.internal", "ignore::DeprecationWarning:django.utils.encoding", # ignoring the text instead of the whole warning because we'd # get an error when django is not installed "ignore:The default value of USE_TZ", "ignore::DeprecationWarning:pydantic_openapi_schema.*", "ignore::DeprecationWarning:graphql.*", "ignore::DeprecationWarning:websockets.*", "ignore::DeprecationWarning:pydantic.*", "ignore::UserWarning:pydantic.*", "ignore::DeprecationWarning:pkg_resources.*", ] [tool.autopub] git-username = "Botberry" git-email = "bot@strawberry.rocks" project-name = "🍓" append-github-contributor = true [tool.pyright] # include = ["strawberry"] exclude = ["**/__pycache__", "**/.venv", "**/.pytest_cache", "**/.nox"] reportMissingImports = true reportMissingTypeStubs = false pythonVersion = "3.10" stubPath = "" [tool.ruff] line-length = 88 target-version = "py310" fix = true exclude = [ ".bzr", ".direnv", ".eggs", ".git", ".hg", ".mypy_cache", ".nox", ".pants.d", ".ruff_cache", ".svn", ".tox", ".venv", "__pypackages__", "_build", "buck-out", "build", "dist", "node_modules", "venv", "tests/*/snapshots", ] src = ["strawberry", "tests"] [tool.ruff.lint] select = ["ALL"] ignore = [ # × this should be fine to use :) "RUF002", # we use asserts in tests and to hint mypy "S101", # Allow "Any" for annotations. We have too many Any annotations and some # are legit. Maybe reconsider in the future, except for tests? "ANN401", # Allow our exceptions to have names that don't end in "Error". Maybe refactor # in the future? But that would be a breaking change. "N818", # Allow "type: ignore" without rule code. Because we support both mypy and # pyright, and they have different codes for the same error, we can't properly # fix those issues. "PGH003", # Variable `T` in function should be lowercase # this seems a potential bug or opportunity for improvement in ruff "N806", # shadowing builtins "A001", "A002", "A003", "A005", # Unused arguments "ARG001", "ARG002", "ARG003", "ARG004", "ARG005", # Boolean positional arguments "FBT001", "FBT002", "FBT003", # Too many arguments/branches/return statements "PLR0913", "PLR0912", "PLR0911", # Do not force adding _co to covariant typevars "PLC0105", # Allow private access to attributes "SLF001", # code complexity "C901", # Allow todo/fixme/etc comments "TD002", "TD003", "FIX001", "FIX002", # We don't want to add "from __future__ mport annotations" everywhere "FA100", # Docstrings, maybe to enable later "D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107", "D412", # Allow to define exceptions text in the exception body "TRY003", "EM101", "EM102", "EM103", # Allow comparisons with magic numbers "PLR2004", # Allow methods to use lru_cache "B019", # Don't force if branches to be converted to "or" "SIM114", # Allow imports inside functions "PLC0415", # ruff formatter recommends to disable those, as they conflict with it # we don't need to ever enable those. "COM812", "COM819", "D206", "E111", "E114", "E117", "E501", "ISC001", "Q000", "Q001", "Q002", "Q003", "W191", "PLR0915", ] [tool.ruff.lint.per-file-ignores] ".github/*" = ["INP001"] "federation-compatibility/*" = ["INP001"] "strawberry/cli/*" = ["B008"] "strawberry/extensions/tracing/__init__.py" = ["TCH004"] "strawberry/fastapi/*" = ["B008"] "strawberry/annotation.py" = ["RET505"] "tests/*" = [ "ANN001", "ANN201", "ANN202", "ANN204", "B008", "B018", "D", "DTZ001", "DTZ005", "FA102", "N805", "PLC1901", "PLR2004", "PLW0603", "PT011", "RUF012", "S105", "S106", "S603", "S607", "TCH001", "TCH002", "TCH003", "TRY002", "UP007", "UP045", ] [tool.ruff.lint.isort] known-first-party = ["strawberry"] known-third-party = [ "django", "graphql", "fastapi", "pydantic", "litestar", "aiohttp", "channels", "sanic", "chalice", "quart", ] extra-standard-library = ["typing_extensions"] [tool.ruff.format] exclude = ['tests/codegen/snapshots/', 'tests/cli/snapshots/'] [tool.ruff.lint.pydocstyle] convention = "google" [tool.codeflash] # All paths are relative to this pyproject.toml's directory. module-root = "strawberry" tests-root = "tests" test-framework = "pytest" ignore-paths = [] formatter-cmds = ["ruff check --exit-zero --fix $file", "ruff format $file"] [build-system] requires = ["poetry-core>=1.6"] build-backend = "poetry.core.masonry.api" strawberry-graphql-0.287.0/setup.py000077500000000000000000000003541511033167500172510ustar00rootroot00000000000000#!/usr/bin/env python # we use poetry for our build, but this file seems to be required # in order to get GitHub dependencies graph to work import setuptools if __name__ == "__main__": setuptools.setup(name="strawberry-graphql") strawberry-graphql-0.287.0/strawberry/000077500000000000000000000000001511033167500177365ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/__init__.py000066400000000000000000000030101511033167500220410ustar00rootroot00000000000000"""Strawberry is a Python library for GraphQL. Strawberry is a Python library for GraphQL that aims to stay close to the GraphQL specification and allow for a more natural way of defining GraphQL schemas. """ from . import experimental, federation, relay from .directive import directive, directive_field from .parent import Parent from .permission import BasePermission from .scalars import ID from .schema import Schema from .schema_directive import schema_directive from .streamable import Streamable from .types.arguments import argument from .types.auto import auto from .types.cast import cast from .types.enum import enum, enum_value from .types.field import field from .types.info import Info from .types.lazy_type import LazyType, lazy from .types.maybe import Maybe, Some from .types.mutation import mutation, subscription from .types.object_type import asdict, input, interface, type # noqa: A004 from .types.private import Private from .types.scalar import scalar from .types.union import union from .types.unset import UNSET __all__ = [ "ID", "UNSET", "BasePermission", "Info", "LazyType", "Maybe", "Parent", "Private", "Schema", "Some", "Streamable", "argument", "asdict", "auto", "cast", "directive", "directive_field", "enum", "enum_value", "experimental", "federation", "field", "input", "interface", "lazy", "mutation", "relay", "scalar", "schema_directive", "subscription", "type", "union", ] strawberry-graphql-0.287.0/strawberry/__main__.py000066400000000000000000000000731511033167500220300ustar00rootroot00000000000000from .cli import run if __name__ == "__main__": run() strawberry-graphql-0.287.0/strawberry/aiohttp/000077500000000000000000000000001511033167500214065ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/aiohttp/__init__.py000066400000000000000000000000001511033167500235050ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/aiohttp/test/000077500000000000000000000000001511033167500223655ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/aiohttp/test/__init__.py000066400000000000000000000001071511033167500244740ustar00rootroot00000000000000from .client import GraphQLTestClient __all__ = ["GraphQLTestClient"] strawberry-graphql-0.287.0/strawberry/aiohttp/test/client.py000066400000000000000000000032131511033167500242140ustar00rootroot00000000000000from __future__ import annotations import warnings from typing import Any from strawberry.test.client import BaseGraphQLTestClient, Response class GraphQLTestClient(BaseGraphQLTestClient): async def query( self, query: str, variables: dict[str, Any] | None = None, headers: dict[str, object] | None = None, asserts_errors: bool | None = None, files: dict[str, object] | None = None, assert_no_errors: bool | None = True, ) -> Response: body = self._build_body(query, variables, files) resp = await self.request(body, headers, files) data = await resp.json() response = Response( errors=data.get("errors"), data=data.get("data"), extensions=data.get("extensions"), ) if asserts_errors is not None: warnings.warn( "The `asserts_errors` argument has been renamed to `assert_no_errors`", DeprecationWarning, stacklevel=2, ) assert_no_errors = ( assert_no_errors if asserts_errors is None else asserts_errors ) if assert_no_errors: assert resp.status == 200 assert response.errors is None return response async def request( self, body: dict[str, object], headers: dict[str, object] | None = None, files: dict[str, object] | None = None, ) -> Any: return await self._client.post( self.url, json=body if not files else None, data=body if files else None, ) __all__ = ["GraphQLTestClient"] strawberry-graphql-0.287.0/strawberry/aiohttp/views.py000066400000000000000000000152471511033167500231260ustar00rootroot00000000000000from __future__ import annotations import asyncio import warnings from datetime import timedelta from json.decoder import JSONDecodeError from typing import ( TYPE_CHECKING, TypeGuard, ) from aiohttp import ClientConnectionResetError, http, web from lia import AiohttpHTTPRequestAdapter, HTTPException from strawberry.http.async_base_view import ( AsyncBaseHTTPView, AsyncWebSocketAdapter, ) from strawberry.http.exceptions import ( NonJsonMessageReceived, NonTextMessageReceived, WebSocketDisconnected, ) from strawberry.http.typevars import ( Context, RootValue, ) from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL if TYPE_CHECKING: from collections.abc import AsyncGenerator, Callable, Mapping, Sequence from strawberry.http import GraphQLHTTPResponse from strawberry.http.ides import GraphQL_IDE from strawberry.schema import BaseSchema class AiohttpWebSocketAdapter(AsyncWebSocketAdapter): def __init__( self, view: AsyncBaseHTTPView, request: web.Request, ws: web.WebSocketResponse ) -> None: super().__init__(view) self.request = request self.ws = ws async def iter_json( self, *, ignore_parsing_errors: bool = False ) -> AsyncGenerator[object, None]: async for ws_message in self.ws: if ws_message.type == http.WSMsgType.TEXT: try: yield self.view.decode_json(ws_message.data) except JSONDecodeError as e: if not ignore_parsing_errors: raise NonJsonMessageReceived from e elif ws_message.type == http.WSMsgType.BINARY: raise NonTextMessageReceived async def send_json(self, message: Mapping[str, object]) -> None: try: encoded_data = self.view.encode_json(message) if isinstance(encoded_data, bytes): await self.ws.send_bytes(encoded_data) else: await self.ws.send_str(encoded_data) except (RuntimeError, ClientConnectionResetError) as exc: raise WebSocketDisconnected from exc async def close(self, code: int, reason: str) -> None: await self.ws.close(code=code, message=reason.encode()) class GraphQLView( AsyncBaseHTTPView[ web.Request, web.Response | web.StreamResponse, web.Response, web.Request, web.WebSocketResponse, Context, RootValue, ] ): # Mark the view as coroutine so that AIOHTTP does not confuse it with a deprecated # bare handler function. _is_coroutine = asyncio.coroutines._is_coroutine # type: ignore[attr-defined] allow_queries_via_get = True request_adapter_class = AiohttpHTTPRequestAdapter websocket_adapter_class = AiohttpWebSocketAdapter # type: ignore def __init__( self, schema: BaseSchema, graphiql: bool | None = None, graphql_ide: GraphQL_IDE | None = "graphiql", allow_queries_via_get: bool = True, keep_alive: bool = True, keep_alive_interval: float = 1, subscription_protocols: Sequence[str] = ( GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL, ), connection_init_wait_timeout: timedelta = timedelta(minutes=1), multipart_uploads_enabled: bool = False, ) -> None: self.schema = schema self.allow_queries_via_get = allow_queries_via_get self.keep_alive = keep_alive self.keep_alive_interval = keep_alive_interval self.subscription_protocols = subscription_protocols self.connection_init_wait_timeout = connection_init_wait_timeout self.multipart_uploads_enabled = multipart_uploads_enabled if graphiql is not None: warnings.warn( "The `graphiql` argument is deprecated in favor of `graphql_ide`", DeprecationWarning, stacklevel=2, ) self.graphql_ide = "graphiql" if graphiql else None else: self.graphql_ide = graphql_ide async def render_graphql_ide(self, request: web.Request) -> web.Response: return web.Response(text=self.graphql_ide_html, content_type="text/html") async def get_sub_response(self, request: web.Request) -> web.Response: return web.Response() def is_websocket_request(self, request: web.Request) -> TypeGuard[web.Request]: ws = web.WebSocketResponse(protocols=self.subscription_protocols) return ws.can_prepare(request).ok async def pick_websocket_subprotocol(self, request: web.Request) -> str | None: ws = web.WebSocketResponse(protocols=self.subscription_protocols) return ws.can_prepare(request).protocol async def create_websocket_response( self, request: web.Request, subprotocol: str | None ) -> web.WebSocketResponse: protocols = [subprotocol] if subprotocol else [] ws = web.WebSocketResponse(protocols=protocols) await ws.prepare(request) return ws async def __call__(self, request: web.Request) -> web.StreamResponse: try: return await self.run(request=request) except HTTPException as e: return web.Response( body=e.reason, status=e.status_code, ) async def get_root_value(self, request: web.Request) -> RootValue | None: return None async def get_context( self, request: web.Request, response: web.Response | web.WebSocketResponse ) -> Context: return {"request": request, "response": response} # type: ignore def create_response( self, response_data: GraphQLHTTPResponse | list[GraphQLHTTPResponse], sub_response: web.Response, ) -> web.Response: encoded_data = self.encode_json(response_data) if isinstance(encoded_data, bytes): encoded_data = encoded_data.decode() sub_response.text = encoded_data sub_response.content_type = "application/json" return sub_response async def create_streaming_response( self, request: web.Request, stream: Callable[[], AsyncGenerator[str, None]], sub_response: web.Response, headers: dict[str, str], ) -> web.StreamResponse: response = web.StreamResponse( status=sub_response.status, headers={ **sub_response.headers, **headers, }, ) await response.prepare(request) async for data in stream(): await response.write(data.encode()) await response.write_eof() return response __all__ = ["GraphQLView"] strawberry-graphql-0.287.0/strawberry/annotation.py000066400000000000000000000366011511033167500224700ustar00rootroot00000000000000from __future__ import annotations import sys import typing import warnings from collections import abc from enum import Enum from types import UnionType from typing import ( TYPE_CHECKING, Annotated, Any, ForwardRef, TypeVar, Union, cast, get_args, get_origin, ) from typing_extensions import Self from strawberry.streamable import StrawberryStreamable from strawberry.types.base import ( StrawberryList, StrawberryMaybe, StrawberryObjectDefinition, StrawberryOptional, StrawberryTypeVar, get_object_definition, has_object_definition, ) from strawberry.types.enum import StrawberryEnumDefinition from strawberry.types.enum import enum as strawberry_enum from strawberry.types.lazy_type import LazyType from strawberry.types.maybe import _annotation_is_maybe from strawberry.types.private import is_private from strawberry.types.scalar import ScalarDefinition from strawberry.types.unset import UNSET from strawberry.utils.typing import eval_type, is_generic, is_type_var if TYPE_CHECKING: from strawberry.types.base import StrawberryType from strawberry.types.field import StrawberryField from strawberry.types.union import StrawberryUnion ASYNC_TYPES = ( abc.AsyncGenerator, abc.AsyncIterable, abc.AsyncIterator, typing.AsyncContextManager, typing.AsyncGenerator, typing.AsyncIterable, typing.AsyncIterator, ) class StrawberryAnnotation: __slots__ = "__resolve_cache__", "namespace", "raw_annotation" def __init__( self, annotation: object | str, *, namespace: dict[str, Any] | None = None, ) -> None: self.raw_annotation = annotation self.namespace = namespace self.__resolve_cache__: StrawberryType | type | None = None def __eq__(self, other: object) -> bool: if not isinstance(other, StrawberryAnnotation): return NotImplemented return self.resolve() == other.resolve() def __hash__(self) -> int: return hash(self.resolve()) @staticmethod def from_annotation( annotation: object, namespace: dict[str, Any] | None = None ) -> StrawberryAnnotation | None: if annotation is None: return None if not isinstance(annotation, StrawberryAnnotation): return StrawberryAnnotation(annotation, namespace=namespace) return annotation @property def annotation(self) -> object | str: """Return evaluated type on success or fallback to raw (string) annotation.""" try: return self.evaluate() except NameError: # Evaluation failures can happen when importing types within a TYPE_CHECKING # block or if the type is declared later on in a module. return self.raw_annotation @annotation.setter def annotation(self, value: object | str) -> None: self.raw_annotation = value self.__resolve_cache__ = None def evaluate(self) -> type: """Return evaluated annotation using `strawberry.util.typing.eval_type`.""" annotation = self.raw_annotation if isinstance(annotation, str): annotation = ForwardRef(annotation) return eval_type(annotation, self.namespace, None) def _get_type_with_args( self, evaled_type: type[Any] ) -> tuple[type[Any], list[Any]]: if self._is_async_type(evaled_type): return self._get_type_with_args(self._strip_async_type(evaled_type)) if get_origin(evaled_type) is Annotated: evaled_type, *args = get_args(evaled_type) stripped_type, stripped_args = self._get_type_with_args(evaled_type) return stripped_type, args + stripped_args return evaled_type, [] def resolve( self, *, type_definition: StrawberryObjectDefinition | None = None, ) -> StrawberryType | type: """Return resolved (transformed) annotation.""" if (resolved := self.__resolve_cache__) is None: resolved = self._resolve() self.__resolve_cache__ = resolved # If this is a generic field, try to resolve it using its origin's # specialized type_var_map if self._is_type_generic(resolved) and type_definition is not None: from strawberry.types.base import StrawberryType specialized_type_var_map = type_definition.specialized_type_var_map if specialized_type_var_map and isinstance(resolved, StrawberryType): resolved = resolved.copy_with(specialized_type_var_map) # If the field is still generic, try to resolve it from the type_definition # that is asking for it. if ( self._is_type_generic(resolved) and type_definition.type_var_map and isinstance(resolved, StrawberryType) ): resolved = resolved.copy_with(type_definition.type_var_map) # Resolve the type again to resolve any `Annotated` types resolved = self._resolve_evaled_type(resolved) return resolved def _resolve(self) -> StrawberryType | type: evaled_type = cast("Any", self.evaluate()) return self._resolve_evaled_type(evaled_type) def _resolve_evaled_type(self, evaled_type: Any) -> StrawberryType | type: if is_private(evaled_type): return evaled_type args: list[Any] = [] evaled_type, args = self._get_type_with_args(evaled_type) if self._is_lazy_type(evaled_type): return evaled_type if self._is_streamable(evaled_type, args): return self.create_list(list[evaled_type]) if self._is_list(evaled_type): return self.create_list(evaled_type) if self._is_maybe(evaled_type): return self.create_maybe(evaled_type) if self._is_graphql_generic(evaled_type): if any(is_type_var(type_) for type_ in get_args(evaled_type)): return evaled_type return self.create_concrete_type(evaled_type) # Everything remaining should be a raw annotation that needs to be turned into # a StrawberryType if self._is_enum(evaled_type): return self.create_enum(evaled_type) if self._is_optional(evaled_type, args): return self.create_optional(evaled_type) if self._is_union(evaled_type, args): return self.create_union(evaled_type, args) if is_type_var(evaled_type) or evaled_type is Self: return self.create_type_var(cast("TypeVar", evaled_type)) if self._is_strawberry_type(evaled_type): # Simply return objects that are already StrawberryTypes return evaled_type # TODO: Raise exception now, or later? # ... raise NotImplementedError(f"Unknown type {evaled_type}") return evaled_type def set_namespace_from_field(self, field: StrawberryField) -> None: module = sys.modules[field.origin.__module__] self.namespace = module.__dict__ self.__resolve_cache__ = None # Invalidate cache to allow re-evaluation def create_concrete_type(self, evaled_type: type) -> type: if has_object_definition(evaled_type): return evaled_type.__strawberry_definition__.resolve_generic(evaled_type) raise ValueError(f"Not supported {evaled_type}") def create_enum(self, evaled_type: Any) -> StrawberryEnumDefinition: try: return evaled_type.__strawberry_definition__ except AttributeError: return strawberry_enum(evaled_type).__strawberry_definition__ def create_list(self, evaled_type: Any) -> StrawberryList: item_type, *_ = get_args(evaled_type) of_type = StrawberryAnnotation( annotation=item_type, namespace=self.namespace, ).resolve() return StrawberryList(of_type) def create_optional(self, evaled_type: Any) -> StrawberryOptional: types = get_args(evaled_type) non_optional_types = tuple( filter( lambda x: x is not type(None) and x is not type(UNSET) and x != type[UNSET], types, ) ) # Note that passing a single type to `Union` is equivalent to not using `Union` # at all. This allows us to not do any checks for how many types have been # passed as we can safely use `Union` for both optional types # (e.g. `Optional[str]`) and optional unions (e.g. # `Optional[Union[TypeA, TypeB]]`) child_type = Union[non_optional_types] # type: ignore # noqa: UP007 of_type = StrawberryAnnotation( annotation=child_type, namespace=self.namespace, ).resolve() return StrawberryOptional(of_type) def create_maybe(self, evaled_type: Any) -> StrawberryMaybe: # we expect a single arg to the evaled type, # as unions on input types are not supported # and maybe[t] already represents t | None inner_type = get_args(evaled_type)[0] of_type = StrawberryAnnotation( annotation=inner_type, namespace=self.namespace ).resolve() return StrawberryMaybe(of_type) def create_type_var(self, evaled_type: TypeVar) -> StrawberryTypeVar: return StrawberryTypeVar(evaled_type) def create_union(self, evaled_type: type[Any], args: list[Any]) -> StrawberryUnion: # Prevent import cycles from strawberry.types.union import StrawberryUnion # TODO: Deal with Forward References/origin if isinstance(evaled_type, StrawberryUnion): return evaled_type types = get_args(evaled_type) # this happens when we have single type unions, e.g. `Union[TypeA]` # since python treats `Union[TypeA]` as `TypeA` =) if not types: types = (evaled_type,) union = StrawberryUnion( type_annotations=tuple(StrawberryAnnotation(type_) for type_ in types), ) union_args = [arg for arg in args if isinstance(arg, StrawberryUnion)] if len(union_args) > 1: warnings.warn( ( "Duplicate union definition detected. " "Only the first definition will be considered" ), stacklevel=2, ) if union_args: arg = union_args[0] union.graphql_name = arg.graphql_name union.description = arg.description union.directives = arg.directives union._source_file = arg._source_file union._source_line = arg._source_line return union @classmethod def _is_async_type(cls, annotation: type) -> bool: origin = getattr(annotation, "__origin__", None) return origin in ASYNC_TYPES @classmethod def _is_enum(cls, annotation: Any) -> bool: # Type aliases are not types so we need to make sure annotation can go into # issubclass if not isinstance(annotation, type): return False return issubclass(annotation, Enum) @classmethod def _is_type_generic(cls, type_: StrawberryType | type) -> bool: """Returns True if `resolver_type` is generic else False.""" from strawberry.types.base import StrawberryType if isinstance(type_, StrawberryType): return type_.is_graphql_generic # solves the Generic subclass case if has_object_definition(type_): return type_.__strawberry_definition__.is_graphql_generic return False @classmethod def _is_graphql_generic(cls, annotation: Any) -> bool: if hasattr(annotation, "__origin__"): if definition := get_object_definition(annotation.__origin__): return definition.is_graphql_generic return is_generic(annotation.__origin__) return False @classmethod def _is_lazy_type(cls, annotation: Any) -> bool: return isinstance(annotation, LazyType) @classmethod def _is_optional(cls, annotation: Any, args: list[Any]) -> bool: """Returns True if the annotation is Optional[SomeType].""" # Optionals are represented as unions if not cls._is_union(annotation, args): return False types = get_args(annotation) # A Union to be optional needs to have at least one None type return any(x is type(None) for x in types) @classmethod def _is_list(cls, annotation: Any) -> bool: """Returns True if annotation is a List.""" annotation_origin = get_origin(annotation) annotation_mro = getattr(annotation, "__mro__", []) is_list = any(x is list for x in annotation_mro) return ( (annotation_origin in (list, tuple)) or annotation_origin is abc.Sequence or is_list ) @classmethod def _is_maybe(cls, annotation: Any) -> bool: return _annotation_is_maybe(annotation) @classmethod def _is_streamable(cls, annotation: Any, args: list[Any]) -> bool: return any(isinstance(arg, StrawberryStreamable) for arg in args) @classmethod def _is_strawberry_type(cls, evaled_type: Any) -> bool: # Prevent import cycles from strawberry.types.union import StrawberryUnion if isinstance(evaled_type, StrawberryEnumDefinition): return True elif _is_input_type(evaled_type): # TODO: Replace with StrawberryInputObject return True # TODO: add support for StrawberryInterface when implemented elif isinstance(evaled_type, StrawberryList): return True elif has_object_definition(evaled_type): return True elif isinstance(evaled_type, StrawberryObjectDefinition): return True elif isinstance(evaled_type, StrawberryOptional): return True elif isinstance( evaled_type, ScalarDefinition ): # TODO: Replace with StrawberryScalar return True elif isinstance(evaled_type, StrawberryUnion): return True return False @classmethod def _is_union(cls, annotation: Any, args: list[Any]) -> bool: """Returns True if annotation is a Union.""" # this check is needed because unions declared with the new syntax `A | B` # don't have a `__origin__` property on them, but they are instances of if isinstance(annotation, UnionType): return True # unions declared as Union[A, B] fall through to this check annotation_origin = getattr(annotation, "__origin__", None) if annotation_origin is typing.Union: return True from strawberry.types.union import StrawberryUnion return any(isinstance(arg, StrawberryUnion) for arg in args) @classmethod def _strip_async_type(cls, annotation: type[Any]) -> type: return annotation.__args__[0] @classmethod def _strip_lazy_type(cls, annotation: LazyType[Any, Any]) -> type: return annotation.resolve_type() ################################################################################ # Temporary functions to be removed with new types ################################################################################ def _is_input_type(type_: Any) -> bool: if not has_object_definition(type_): return False return type_.__strawberry_definition__.is_input __all__ = ["StrawberryAnnotation"] strawberry-graphql-0.287.0/strawberry/asgi/000077500000000000000000000000001511033167500206615ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/asgi/__init__.py000066400000000000000000000163101511033167500227730ustar00rootroot00000000000000from __future__ import annotations import warnings from datetime import timedelta from json import JSONDecodeError from typing import ( TYPE_CHECKING, TypeGuard, ) from lia import HTTPException, StarletteRequestAdapter from starlette import status from starlette.requests import Request from starlette.responses import ( HTMLResponse, PlainTextResponse, Response, StreamingResponse, ) from starlette.websockets import WebSocket, WebSocketDisconnect, WebSocketState from strawberry.http.async_base_view import ( AsyncBaseHTTPView, AsyncWebSocketAdapter, ) from strawberry.http.exceptions import ( NonJsonMessageReceived, NonTextMessageReceived, WebSocketDisconnected, ) from strawberry.http.typevars import ( Context, RootValue, ) from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL if TYPE_CHECKING: from collections.abc import ( AsyncGenerator, AsyncIterator, Callable, Mapping, Sequence, ) from starlette.types import Receive, Scope, Send from strawberry.http import GraphQLHTTPResponse from strawberry.http.ides import GraphQL_IDE from strawberry.schema import BaseSchema class ASGIWebSocketAdapter(AsyncWebSocketAdapter): def __init__( self, view: AsyncBaseHTTPView, request: WebSocket, response: WebSocket ) -> None: super().__init__(view) self.ws = response async def iter_json( self, *, ignore_parsing_errors: bool = False ) -> AsyncGenerator[object, None]: try: while self.ws.application_state != WebSocketState.DISCONNECTED: try: text = await self.ws.receive_text() yield self.view.decode_json(text) except JSONDecodeError as e: # noqa: PERF203 if not ignore_parsing_errors: raise NonJsonMessageReceived from e except KeyError as e: raise NonTextMessageReceived from e except WebSocketDisconnect: # pragma: no cover pass async def send_json(self, message: Mapping[str, object]) -> None: try: encoded_data = self.view.encode_json(message) if isinstance(encoded_data, bytes): await self.ws.send_bytes(encoded_data) else: await self.ws.send_text(encoded_data) except WebSocketDisconnect as exc: raise WebSocketDisconnected from exc async def close(self, code: int, reason: str) -> None: await self.ws.close(code=code, reason=reason) class GraphQL( AsyncBaseHTTPView[ Request, Response, Response, WebSocket, WebSocket, Context, RootValue, ] ): allow_queries_via_get = True request_adapter_class = StarletteRequestAdapter websocket_adapter_class = ASGIWebSocketAdapter # type: ignore def __init__( self, schema: BaseSchema, graphiql: bool | None = None, graphql_ide: GraphQL_IDE | None = "graphiql", allow_queries_via_get: bool = True, keep_alive: bool = False, keep_alive_interval: float = 1, subscription_protocols: Sequence[str] = ( GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL, ), connection_init_wait_timeout: timedelta = timedelta(minutes=1), multipart_uploads_enabled: bool = False, ) -> None: self.schema = schema self.allow_queries_via_get = allow_queries_via_get self.keep_alive = keep_alive self.keep_alive_interval = keep_alive_interval self.protocols = subscription_protocols self.connection_init_wait_timeout = connection_init_wait_timeout self.multipart_uploads_enabled = multipart_uploads_enabled if graphiql is not None: warnings.warn( "The `graphiql` argument is deprecated in favor of `graphql_ide`", DeprecationWarning, stacklevel=2, ) self.graphql_ide = "graphiql" if graphiql else None else: self.graphql_ide = graphql_ide async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: if scope["type"] == "http": http_request = Request(scope=scope, receive=receive) try: response = await self.run(http_request) except HTTPException as e: response = PlainTextResponse(e.reason, status_code=e.status_code) await response(scope, receive, send) elif scope["type"] == "websocket": ws_request = WebSocket(scope, receive=receive, send=send) await self.run(ws_request) else: # pragma: no cover raise ValueError("Unknown scope type: {!r}".format(scope["type"])) async def get_root_value(self, request: Request | WebSocket) -> RootValue | None: return None async def get_context( self, request: Request | WebSocket, response: Response | WebSocket ) -> Context: return {"request": request, "response": response} # type: ignore async def get_sub_response( self, request: Request | WebSocket, ) -> Response: sub_response = Response() sub_response.status_code = None # type: ignore del sub_response.headers["content-length"] return sub_response async def render_graphql_ide(self, request: Request) -> Response: return HTMLResponse(self.graphql_ide_html) def create_response( self, response_data: GraphQLHTTPResponse | list[GraphQLHTTPResponse], sub_response: Response, ) -> Response: response = Response( self.encode_json(response_data), status_code=status.HTTP_200_OK, media_type="application/json", ) response.headers.raw.extend(sub_response.headers.raw) if sub_response.background: response.background = sub_response.background if sub_response.status_code: response.status_code = sub_response.status_code return response async def create_streaming_response( self, request: Request | WebSocket, stream: Callable[[], AsyncIterator[str]], sub_response: Response, headers: dict[str, str], ) -> Response: return StreamingResponse( stream(), status_code=sub_response.status_code or status.HTTP_200_OK, headers={ **sub_response.headers, **headers, }, ) def is_websocket_request( self, request: Request | WebSocket ) -> TypeGuard[WebSocket]: return request.scope["type"] == "websocket" async def pick_websocket_subprotocol(self, request: WebSocket) -> str | None: protocols = request["subprotocols"] intersection = set(protocols) & set(self.protocols) sorted_intersection = sorted(intersection, key=protocols.index) return next(iter(sorted_intersection), None) async def create_websocket_response( self, request: WebSocket, subprotocol: str | None ) -> WebSocket: await request.accept(subprotocol=subprotocol) return request strawberry-graphql-0.287.0/strawberry/asgi/test/000077500000000000000000000000001511033167500216405ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/asgi/test/__init__.py000066400000000000000000000001071511033167500237470ustar00rootroot00000000000000from .client import GraphQLTestClient __all__ = ["GraphQLTestClient"] strawberry-graphql-0.287.0/strawberry/asgi/test/client.py000066400000000000000000000026401511033167500234720ustar00rootroot00000000000000from __future__ import annotations import json from typing import TYPE_CHECKING, Any from strawberry.test import BaseGraphQLTestClient if TYPE_CHECKING: from collections.abc import Mapping from typing import Literal class GraphQLTestClient(BaseGraphQLTestClient): def _build_body( self, query: str, variables: dict[str, Mapping] | None = None, files: dict[str, object] | None = None, ) -> dict[str, object]: body: dict[str, object] = {"query": query} if variables: body["variables"] = variables if files: assert variables is not None assert files is not None file_map = GraphQLTestClient._build_multipart_file_map(variables, files) body = { "operations": json.dumps(body), "map": json.dumps(file_map), } return body def request( self, body: dict[str, object], headers: dict[str, object] | None = None, files: dict[str, object] | None = None, ) -> Any: return self._client.post( self.url, json=body if not files else None, data=body if files else None, files=files, headers=headers, ) def _decode(self, response: Any, type: Literal["multipart", "json"]) -> Any: return response.json() __all__ = ["GraphQLTestClient"] strawberry-graphql-0.287.0/strawberry/chalice/000077500000000000000000000000001511033167500213265ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/chalice/__init__.py000066400000000000000000000000001511033167500234250ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/chalice/views.py000066400000000000000000000060151511033167500230370ustar00rootroot00000000000000from __future__ import annotations import warnings from typing import TYPE_CHECKING from chalice.app import Request, Response from lia import ChaliceHTTPRequestAdapter, HTTPException from strawberry.http.sync_base_view import SyncBaseHTTPView from strawberry.http.temporal_response import TemporalResponse from strawberry.http.typevars import Context, RootValue if TYPE_CHECKING: from strawberry.http import GraphQLHTTPResponse from strawberry.http.ides import GraphQL_IDE from strawberry.schema import BaseSchema class GraphQLView( SyncBaseHTTPView[Request, Response, TemporalResponse, Context, RootValue] ): allow_queries_via_get: bool = True request_adapter_class = ChaliceHTTPRequestAdapter def __init__( self, schema: BaseSchema, graphiql: bool | None = None, graphql_ide: GraphQL_IDE | None = "graphiql", allow_queries_via_get: bool = True, ) -> None: self.allow_queries_via_get = allow_queries_via_get self.schema = schema if graphiql is not None: warnings.warn( "The `graphiql` argument is deprecated in favor of `graphql_ide`", DeprecationWarning, stacklevel=2, ) self.graphql_ide = "graphiql" if graphiql else None else: self.graphql_ide = graphql_ide def get_root_value(self, request: Request) -> RootValue | None: return None def render_graphql_ide(self, request: Request) -> Response: return Response( self.graphql_ide_html, headers={"Content-Type": "text/html"}, ) def get_sub_response(self, request: Request) -> TemporalResponse: return TemporalResponse() def get_context(self, request: Request, response: TemporalResponse) -> Context: return {"request": request, "response": response} # type: ignore def create_response( self, response_data: GraphQLHTTPResponse | list[GraphQLHTTPResponse], sub_response: TemporalResponse, ) -> Response: status_code = 200 if sub_response.status_code != 200: status_code = sub_response.status_code encoded_data = self.encode_json(response_data) if isinstance(encoded_data, bytes): encoded_data = encoded_data.decode() # Chalice expects str or objects for body unless the content type has been added to the chalice app # list of binary content types return Response( body=encoded_data, status_code=status_code, headers={ "Content-Type": "application/json", **sub_response.headers, }, ) def execute_request(self, request: Request) -> Response: try: return self.run(request=request) except HTTPException as e: return Response( body=e.reason, status_code=e.status_code, headers={"Content-Type": "text/plain"}, ) __all__ = ["GraphQLView"] strawberry-graphql-0.287.0/strawberry/channels/000077500000000000000000000000001511033167500215315ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/channels/__init__.py000066400000000000000000000006611511033167500236450ustar00rootroot00000000000000from .handlers.base import ChannelsConsumer from .handlers.http_handler import ( ChannelsRequest, GraphQLHTTPConsumer, SyncGraphQLHTTPConsumer, ) from .handlers.ws_handler import GraphQLWSConsumer from .router import GraphQLProtocolTypeRouter __all__ = [ "ChannelsConsumer", "ChannelsRequest", "GraphQLHTTPConsumer", "GraphQLProtocolTypeRouter", "GraphQLWSConsumer", "SyncGraphQLHTTPConsumer", ] strawberry-graphql-0.287.0/strawberry/channels/handlers/000077500000000000000000000000001511033167500233315ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/channels/handlers/__init__.py000066400000000000000000000000001511033167500254300ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/channels/handlers/base.py000066400000000000000000000172071511033167500246240ustar00rootroot00000000000000import asyncio import contextlib import warnings from collections import defaultdict from collections.abc import AsyncGenerator, Awaitable, Callable, Sequence from typing import ( Any, Literal, ) from typing_extensions import Protocol, TypedDict from weakref import WeakSet from channels.consumer import AsyncConsumer from channels.generic.websocket import AsyncWebsocketConsumer class ChannelsMessage(TypedDict, total=False): type: str class ChannelsLayer(Protocol): # pragma: no cover """Channels layer spec. Based on: https://channels.readthedocs.io/en/stable/channel_layer_spec.html """ # Default channels API extensions: list[Literal["groups", "flush"]] async def send(self, channel: str, message: dict) -> None: ... async def receive(self, channel: str) -> dict: ... async def new_channel(self, prefix: str = ...) -> str: ... # If groups extension is supported group_expiry: int async def group_add(self, group: str, channel: str) -> None: ... async def group_discard(self, group: str, channel: str) -> None: ... async def group_send(self, group: str, message: dict) -> None: ... # If flush extension is supported async def flush(self) -> None: ... class ChannelsConsumer(AsyncConsumer): """Base channels async consumer.""" channel_name: str channel_layer: ChannelsLayer | None channel_receive: Callable[[], Awaitable[dict]] def __init__(self, *args: str, **kwargs: Any) -> None: self.listen_queues: defaultdict[str, WeakSet[asyncio.Queue]] = defaultdict( WeakSet ) super().__init__(*args, **kwargs) async def dispatch(self, message: ChannelsMessage) -> None: # AsyncConsumer will try to get a function for message["type"] to handle # for both http/websocket types and also for layers communication. # In case the type isn't one of those, pass it to the listen queue so # that it can be consumed by self.channel_listen type_ = message.get("type", "") if type_ and not type_.startswith(("http.", "websocket.")): for queue in self.listen_queues[type_]: queue.put_nowait(message) return await super().dispatch(message) async def channel_listen( self, type: str, *, timeout: float | None = None, groups: Sequence[str] = (), ) -> AsyncGenerator[Any, None]: """Listen for messages sent to this consumer. Utility to listen for channels messages for this consumer inside a resolver (usually inside a subscription). Args: type: The type of the message to wait for. timeout: An optional timeout to wait for each subsequent message groups: An optional sequence of groups to receive messages from. When passing this parameter, the groups will be registered using `self.channel_layer.group_add` at the beggining of the execution and then discarded using `self.channel_layer.group_discard` at the end of the execution. """ warnings.warn("Use listen_to_channel instead", DeprecationWarning, stacklevel=2) if self.channel_layer is None: raise RuntimeError( "Layers integration is required listening for channels.\n" "Check https://channels.readthedocs.io/en/stable/topics/channel_layers.html " "for more information" ) added_groups = [] try: # This queue will receive incoming messages for this generator instance queue: asyncio.Queue = asyncio.Queue() # Create a weak reference to the queue. Once we leave the current scope, it # will be garbage collected self.listen_queues[type].add(queue) for group in groups: await self.channel_layer.group_add(group, self.channel_name) added_groups.append(group) while True: awaitable = queue.get() if timeout is not None: awaitable = asyncio.wait_for(awaitable, timeout) try: yield await awaitable except asyncio.TimeoutError: # TODO: shall we add log here and maybe in the suppress below? return finally: for group in added_groups: with contextlib.suppress(Exception): await self.channel_layer.group_discard(group, self.channel_name) @contextlib.asynccontextmanager async def listen_to_channel( self, type: str, *, timeout: float | None = None, groups: Sequence[str] = (), ) -> AsyncGenerator[Any, None]: """Listen for messages sent to this consumer. Utility to listen for channels messages for this consumer inside a resolver (usually inside a subscription). Args: type: The type of the message to wait for. timeout: An optional timeout to wait for each subsequent message groups: An optional sequence of groups to receive messages from. When passing this parameter, the groups will be registered using `self.channel_layer.group_add` at the beggining of the execution and then discarded using `self.channel_layer.group_discard` at the end of the execution. """ # Code to acquire resource (Channels subscriptions) if self.channel_layer is None: raise RuntimeError( "Layers integration is required listening for channels.\n" "Check https://channels.readthedocs.io/en/stable/topics/channel_layers.html " "for more information" ) added_groups = [] # This queue will receive incoming messages for this generator instance queue: asyncio.Queue = asyncio.Queue() # Create a weak reference to the queue. Once we leave the current scope, it # will be garbage collected self.listen_queues[type].add(queue) # Subscribe to all groups but return generator object to allow user # code to run before blocking on incoming messages for group in groups: await self.channel_layer.group_add(group, self.channel_name) added_groups.append(group) try: yield self._listen_to_channel_generator(queue, timeout) finally: # Code to release resource (Channels subscriptions) for group in added_groups: with contextlib.suppress(Exception): await self.channel_layer.group_discard(group, self.channel_name) async def _listen_to_channel_generator( self, queue: asyncio.Queue, timeout: float | None ) -> AsyncGenerator[Any, None]: """Generator for listen_to_channel method. Seperated to allow user code to be run after subscribing to channels and before blocking to wait for incoming channel messages. """ while True: awaitable = queue.get() if timeout is not None: awaitable = asyncio.wait_for(awaitable, timeout) try: yield await awaitable except asyncio.TimeoutError: # TODO: shall we add log here and maybe in the suppress below? return class ChannelsWSConsumer(ChannelsConsumer, AsyncWebsocketConsumer): """Base channels websocket async consumer.""" __all__ = ["ChannelsConsumer", "ChannelsWSConsumer"] strawberry-graphql-0.287.0/strawberry/channels/handlers/http_handler.py000066400000000000000000000303111511033167500263550ustar00rootroot00000000000000from __future__ import annotations import dataclasses import json import warnings from functools import cached_property from io import BytesIO from typing import TYPE_CHECKING, Any, TypeGuard from typing_extensions import assert_never from urllib.parse import parse_qs from channels.db import database_sync_to_async from channels.generic.http import AsyncHttpConsumer from django.conf import settings from django.core.files import uploadhandler from django.http.multipartparser import MultiPartParser from lia import AsyncHTTPRequestAdapter, FormData, HTTPException, SyncHTTPRequestAdapter from strawberry.http.async_base_view import AsyncBaseHTTPView from strawberry.http.sync_base_view import SyncBaseHTTPView from strawberry.http.temporal_response import TemporalResponse from strawberry.http.typevars import Context, RootValue from strawberry.types.unset import UNSET from .base import ChannelsConsumer if TYPE_CHECKING: from collections.abc import AsyncGenerator, Callable, Mapping from strawberry.http import GraphQLHTTPResponse from strawberry.http.ides import GraphQL_IDE from strawberry.http.types import HTTPMethod, QueryParams from strawberry.schema import BaseSchema @dataclasses.dataclass class ChannelsResponse: content: bytes status: int = 200 content_type: str = "application/json" headers: dict[bytes, bytes] = dataclasses.field(default_factory=dict) @dataclasses.dataclass class MultipartChannelsResponse: stream: Callable[[], AsyncGenerator[str, None]] status: int = 200 content_type: str = "multipart/mixed;boundary=graphql;subscriptionSpec=1.0" headers: dict[bytes, bytes] = dataclasses.field(default_factory=dict) @dataclasses.dataclass class ChannelsRequest: consumer: ChannelsConsumer body: bytes @property def query_params(self) -> QueryParams: query_params_str = self.consumer.scope["query_string"].decode() query_params = {} for key, value in parse_qs(query_params_str, keep_blank_values=True).items(): # Only one argument per key is expected here query_params[key] = value[0] return query_params @property def headers(self) -> Mapping[str, str]: return { header_name.decode().lower(): header_value.decode() for header_name, header_value in self.consumer.scope["headers"] } @property def method(self) -> HTTPMethod: return self.consumer.scope["method"].upper() @property def content_type(self) -> str | None: return self.headers.get("content-type", None) @cached_property def form_data(self) -> FormData: upload_handlers = [ uploadhandler.load_handler(handler) for handler in settings.FILE_UPLOAD_HANDLERS ] parser = MultiPartParser( { "CONTENT_TYPE": self.headers.get("content-type"), "CONTENT_LENGTH": self.headers.get("content-length", "0"), }, BytesIO(self.body), upload_handlers, ) querydict, files = parser.parse() form = { "operations": querydict.get("operations", "{}"), "map": querydict.get("map", "{}"), } return FormData(files=files, form=form) class BaseChannelsRequestAdapter: def __init__(self, request: ChannelsRequest) -> None: self.request = request @property def query_params(self) -> QueryParams: return self.request.query_params @property def method(self) -> HTTPMethod: return self.request.method @property def headers(self) -> Mapping[str, str]: return self.request.headers @property def content_type(self) -> str | None: return self.request.content_type @property def url(self) -> str: scheme = self.request.consumer.scope["scheme"] host = self.headers.get("host", "localhost") path = self.request.consumer.scope["path"] query_string = self.request.consumer.scope["query_string"] url = f"{scheme}://{host}{path}" if query_string: url += f"?{query_string.decode()}" return url @property def cookies(self) -> Mapping[str, str]: cookie_header = self.headers.get("cookie", "") cookies = {} if cookie_header: for cookie in cookie_header.split(";"): if "=" in cookie: key, value = cookie.split("=", 1) cookies[key.strip()] = value.strip() return cookies class ChannelsRequestAdapter(BaseChannelsRequestAdapter, AsyncHTTPRequestAdapter): async def get_body(self) -> bytes: return self.request.body async def get_form_data(self) -> FormData: return self.request.form_data class SyncChannelsRequestAdapter(BaseChannelsRequestAdapter, SyncHTTPRequestAdapter): @property def body(self) -> bytes: return self.request.body @property def post_data(self) -> Mapping[str, str | bytes]: return self.request.form_data.form @property def files(self) -> Mapping[str, Any]: return self.request.form_data.files def get_form_data(self) -> FormData: return self.request.form_data class BaseGraphQLHTTPConsumer(ChannelsConsumer, AsyncHttpConsumer): graphql_ide_html: str graphql_ide: GraphQL_IDE | None = "graphiql" def __init__( self, schema: BaseSchema, graphiql: bool | None = None, graphql_ide: GraphQL_IDE | None = "graphiql", allow_queries_via_get: bool = True, multipart_uploads_enabled: bool = False, **kwargs: Any, ) -> None: self.schema = schema self.allow_queries_via_get = allow_queries_via_get self.multipart_uploads_enabled = multipart_uploads_enabled if graphiql is not None: warnings.warn( "The `graphiql` argument is deprecated in favor of `graphql_ide`", DeprecationWarning, stacklevel=2, ) self.graphql_ide = "graphiql" if graphiql else None else: self.graphql_ide = graphql_ide super().__init__(**kwargs) def create_response( self, response_data: GraphQLHTTPResponse | list[GraphQLHTTPResponse], sub_response: TemporalResponse, ) -> ChannelsResponse: return ChannelsResponse( content=json.dumps(response_data).encode(), status=sub_response.status_code, headers={k.encode(): v.encode() for k, v in sub_response.headers.items()}, ) async def handle(self, body: bytes) -> None: request = ChannelsRequest(consumer=self, body=body) try: response = await self.run(request) if b"Content-Type" not in response.headers: response.headers[b"Content-Type"] = response.content_type.encode() if isinstance(response, MultipartChannelsResponse): response.headers[b"Transfer-Encoding"] = b"chunked" await self.send_headers(headers=response.headers) async for chunk in response.stream(): await self.send_body(chunk.encode("utf-8"), more_body=True) await self.send_body(b"", more_body=False) elif isinstance(response, ChannelsResponse): await self.send_response( response.status, response.content, headers=response.headers, ) else: assert_never(response) except HTTPException as e: await self.send_response( e.status_code, e.reason.encode(), headers=[(b"Content-Type", b"text/plain")], ) class GraphQLHTTPConsumer( BaseGraphQLHTTPConsumer, AsyncBaseHTTPView[ ChannelsRequest, ChannelsResponse | MultipartChannelsResponse, TemporalResponse, ChannelsRequest, TemporalResponse, Context, RootValue, ], ): """A consumer to provide a view for GraphQL over HTTP. To use this, place it in your ProtocolTypeRouter for your channels project: ``` from strawberry.channels import GraphQLHttpRouter from channels.routing import ProtocolTypeRouter from django.core.asgi import get_asgi_application application = ProtocolTypeRouter({ "http": URLRouter([ re_path("^graphql", GraphQLHTTPRouter(schema=schema)), re_path("^", get_asgi_application()), ]), "websocket": URLRouter([ re_path("^ws/graphql", GraphQLWebSocketRouter(schema=schema)), ]), }) ``` """ allow_queries_via_get: bool = True request_adapter_class = ChannelsRequestAdapter async def get_root_value(self, request: ChannelsRequest) -> RootValue | None: return None # pragma: no cover async def get_context( self, request: ChannelsRequest, response: TemporalResponse ) -> Context: return { "request": request, "response": response, } # type: ignore async def get_sub_response(self, request: ChannelsRequest) -> TemporalResponse: return TemporalResponse() async def create_streaming_response( self, request: ChannelsRequest, stream: Callable[[], AsyncGenerator[str, None]], sub_response: TemporalResponse, headers: dict[str, str], ) -> MultipartChannelsResponse: status = sub_response.status_code or 200 response_headers = { k.encode(): v.encode() for k, v in sub_response.headers.items() } response_headers.update({k.encode(): v.encode() for k, v in headers.items()}) return MultipartChannelsResponse( stream=stream, status=status, headers=response_headers ) async def render_graphql_ide(self, request: ChannelsRequest) -> ChannelsResponse: return ChannelsResponse( content=self.graphql_ide_html.encode(), content_type="text/html; charset=utf-8", ) def is_websocket_request( self, request: ChannelsRequest ) -> TypeGuard[ChannelsRequest]: return False async def pick_websocket_subprotocol(self, request: ChannelsRequest) -> str | None: return None async def create_websocket_response( self, request: ChannelsRequest, subprotocol: str | None ) -> TemporalResponse: raise NotImplementedError class SyncGraphQLHTTPConsumer( BaseGraphQLHTTPConsumer, SyncBaseHTTPView[ ChannelsRequest, ChannelsResponse, TemporalResponse, Context, RootValue, ], ): """Synchronous version of the HTTPConsumer. This is the same as `GraphQLHTTPConsumer`, but it can be used with synchronous schemas (i.e. the schema's resolvers are expected to be synchronous and not asynchronous). """ allow_queries_via_get: bool = True request_adapter_class = SyncChannelsRequestAdapter def get_root_value(self, request: ChannelsRequest) -> RootValue | None: return None # pragma: no cover def get_context( self, request: ChannelsRequest, response: TemporalResponse ) -> Context: return { "request": request, "response": response, } # type: ignore def get_sub_response(self, request: ChannelsRequest) -> TemporalResponse: return TemporalResponse() def render_graphql_ide(self, request: ChannelsRequest) -> ChannelsResponse: return ChannelsResponse( content=self.graphql_ide_html.encode(), content_type="text/html; charset=utf-8", ) # Sync channels is actually async, but it uses database_sync_to_async to call # handlers in a threadpool. Check SyncConsumer's documentation for more info: # https://github.com/django/channels/blob/main/channels/consumer.py#L104 @database_sync_to_async # pyright: ignore[reportIncompatibleMethodOverride] def run( self, request: ChannelsRequest, context: Context = UNSET, root_value: RootValue | None = UNSET, ) -> ChannelsResponse | MultipartChannelsResponse: return super().run(request, context, root_value) __all__ = ["GraphQLHTTPConsumer", "SyncGraphQLHTTPConsumer"] strawberry-graphql-0.287.0/strawberry/channels/handlers/ws_handler.py000066400000000000000000000137311511033167500260360ustar00rootroot00000000000000from __future__ import annotations import asyncio import datetime import json from typing import ( TYPE_CHECKING, TypedDict, TypeGuard, ) from strawberry.http.async_base_view import AsyncBaseHTTPView, AsyncWebSocketAdapter from strawberry.http.exceptions import NonJsonMessageReceived, NonTextMessageReceived from strawberry.http.typevars import Context, RootValue from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL from .base import ChannelsWSConsumer if TYPE_CHECKING: from collections.abc import AsyncGenerator, Mapping, Sequence from strawberry.http import GraphQLHTTPResponse from strawberry.schema import BaseSchema class ChannelsWebSocketAdapter(AsyncWebSocketAdapter): def __init__( self, view: AsyncBaseHTTPView, request: GraphQLWSConsumer, response: GraphQLWSConsumer, ) -> None: super().__init__(view) self.ws_consumer = response async def iter_json( self, *, ignore_parsing_errors: bool = False ) -> AsyncGenerator[object, None]: while True: message = await self.ws_consumer.message_queue.get() if message["disconnected"]: break if message["message"] is None: raise NonTextMessageReceived try: yield self.view.decode_json(message["message"]) except json.JSONDecodeError as e: if not ignore_parsing_errors: raise NonJsonMessageReceived from e async def send_json(self, message: Mapping[str, object]) -> None: serialized_message = self.view.encode_json(message) await self.ws_consumer.send(serialized_message) async def close(self, code: int, reason: str) -> None: await self.ws_consumer.close(code=code, reason=reason) class MessageQueueData(TypedDict): message: str | None disconnected: bool class GraphQLWSConsumer( ChannelsWSConsumer, AsyncBaseHTTPView[ "GraphQLWSConsumer", "GraphQLWSConsumer", "GraphQLWSConsumer", "GraphQLWSConsumer", "GraphQLWSConsumer", Context, RootValue, ], ): """A channels websocket consumer for GraphQL. This handles the connections, then hands off to the appropriate handler based on the subprotocol. To use this, place it in your ProtocolTypeRouter for your channels project, e.g: ``` from strawberry.channels import GraphQLHttpRouter from channels.routing import ProtocolTypeRouter from django.core.asgi import get_asgi_application application = ProtocolTypeRouter({ "http": URLRouter([ re_path("^graphql", GraphQLHTTPRouter(schema=schema)), re_path("^", get_asgi_application()), ]), "websocket": URLRouter([ re_path("^ws/graphql", GraphQLWebSocketRouter(schema=schema)), ]), }) ``` """ websocket_adapter_class = ChannelsWebSocketAdapter # type: ignore def __init__( self, schema: BaseSchema, keep_alive: bool = False, keep_alive_interval: float = 1, subscription_protocols: Sequence[str] = ( GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL, ), connection_init_wait_timeout: datetime.timedelta | None = None, ) -> None: if connection_init_wait_timeout is None: connection_init_wait_timeout = datetime.timedelta(minutes=1) self.connection_init_wait_timeout = connection_init_wait_timeout self.schema = schema self.keep_alive = keep_alive self.keep_alive_interval = keep_alive_interval self.protocols = subscription_protocols self.message_queue: asyncio.Queue[MessageQueueData] = asyncio.Queue() self.run_task: asyncio.Task | None = None super().__init__() async def connect(self) -> None: self.run_task = asyncio.create_task(self.run(self)) async def receive( self, text_data: str | None = None, bytes_data: bytes | None = None ) -> None: if text_data: self.message_queue.put_nowait({"message": text_data, "disconnected": False}) else: self.message_queue.put_nowait({"message": None, "disconnected": False}) async def disconnect(self, code: int) -> None: self.message_queue.put_nowait({"message": None, "disconnected": True}) assert self.run_task await self.run_task async def get_root_value(self, request: GraphQLWSConsumer) -> RootValue | None: return None async def get_context( self, request: GraphQLWSConsumer, response: GraphQLWSConsumer ) -> Context: return { "request": request, "ws": request, } # type: ignore @property def allow_queries_via_get(self) -> bool: return False async def get_sub_response(self, request: GraphQLWSConsumer) -> GraphQLWSConsumer: raise NotImplementedError def create_response( self, response_data: GraphQLHTTPResponse | list[GraphQLHTTPResponse], sub_response: GraphQLWSConsumer, ) -> GraphQLWSConsumer: raise NotImplementedError async def render_graphql_ide(self, request: GraphQLWSConsumer) -> GraphQLWSConsumer: raise NotImplementedError def is_websocket_request( self, request: GraphQLWSConsumer ) -> TypeGuard[GraphQLWSConsumer]: return True async def pick_websocket_subprotocol( self, request: GraphQLWSConsumer ) -> str | None: protocols = request.scope["subprotocols"] intersection = set(protocols) & set(self.protocols) sorted_intersection = sorted(intersection, key=protocols.index) return next(iter(sorted_intersection), None) async def create_websocket_response( self, request: GraphQLWSConsumer, subprotocol: str | None ) -> GraphQLWSConsumer: await request.accept(subprotocol=subprotocol) return request __all__ = ["GraphQLWSConsumer"] strawberry-graphql-0.287.0/strawberry/channels/router.py000066400000000000000000000037321511033167500234300ustar00rootroot00000000000000"""GraphQLWebSocketRouter. This is a simple router class that might be better placed as part of Channels itself. It's a simple "SubProtocolRouter" that selects the websocket subprotocol based on preferences and client support. Then it hands off to the appropriate consumer. """ from __future__ import annotations from typing import TYPE_CHECKING from channels.routing import ProtocolTypeRouter, URLRouter from django.urls import re_path from .handlers.http_handler import GraphQLHTTPConsumer from .handlers.ws_handler import GraphQLWSConsumer if TYPE_CHECKING: from strawberry.schema import BaseSchema class GraphQLProtocolTypeRouter(ProtocolTypeRouter): """HTTP and Websocket GraphQL type router. Convenience class to set up GraphQL on both HTTP and Websocket, optionally with a Django application for all other HTTP routes. ```python from strawberry.channels import GraphQLProtocolTypeRouter from django.core.asgi import get_asgi_application django_asgi = get_asgi_application() from myapi import schema application = GraphQLProtocolTypeRouter( schema, django_application=django_asgi, ) ``` This will route all requests to /graphql on either HTTP or websockets to us, and everything else to the Django application. """ def __init__( self, schema: BaseSchema, django_application: str | None = None, url_pattern: str = "^graphql", ) -> None: http_urls = [re_path(url_pattern, GraphQLHTTPConsumer.as_asgi(schema=schema))] if django_application is not None: http_urls.append(re_path("^", django_application)) super().__init__( { "http": URLRouter(http_urls), "websocket": URLRouter( [ re_path(url_pattern, GraphQLWSConsumer.as_asgi(schema=schema)), ] ), } ) __all__ = ["GraphQLProtocolTypeRouter"] strawberry-graphql-0.287.0/strawberry/channels/testing.py000066400000000000000000000145601511033167500235660ustar00rootroot00000000000000from __future__ import annotations import uuid from typing import ( TYPE_CHECKING, Any, ) from channels.testing.websocket import WebsocketCommunicator from graphql import GraphQLError, GraphQLFormattedError from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL from strawberry.subscriptions.protocols.graphql_transport_ws import ( types as transport_ws_types, ) from strawberry.subscriptions.protocols.graphql_ws import types as ws_types from strawberry.types import ExecutionResult if TYPE_CHECKING: from collections.abc import AsyncIterator from types import TracebackType from typing_extensions import Self from asgiref.typing import ASGIApplication class GraphQLWebsocketCommunicator(WebsocketCommunicator): """A test communicator for GraphQL over Websockets. ```python import pytest from strawberry.channels.testing import GraphQLWebsocketCommunicator from myapp.asgi import application @pytest.fixture async def gql_communicator(): async with GraphQLWebsocketCommunicator(application, path="/graphql") as client: yield client async def test_subscribe_echo(gql_communicator): async for res in gql_communicator.subscribe( query='subscription { echo(message: "Hi") }' ): assert res.data == {"echo": "Hi"} ``` """ def __init__( self, application: ASGIApplication, path: str, headers: list[tuple[bytes, bytes]] | None = None, protocol: str = GRAPHQL_TRANSPORT_WS_PROTOCOL, connection_params: dict | None = None, **kwargs: Any, ) -> None: """Create a new communicator. Args: application: Your asgi application that encapsulates the strawberry schema. path: the url endpoint for the schema. protocol: currently this supports `graphql-transport-ws` only. connection_params: a dictionary of connection parameters to send to the server. headers: a list of tuples to be sent as headers to the server. subprotocols: an ordered list of preferred subprotocols to be sent to the server. **kwargs: additional arguments to be passed to the `WebsocketCommunicator` constructor. """ if connection_params is None: connection_params = {} self.protocol = protocol subprotocols = kwargs.get("subprotocols", []) subprotocols.append(protocol) self.connection_params = connection_params super().__init__(application, path, headers, subprotocols=subprotocols) async def __aenter__(self) -> Self: await self.gql_init() return self async def __aexit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: await self.disconnect() async def gql_init(self) -> None: res = await self.connect() if self.protocol == GRAPHQL_TRANSPORT_WS_PROTOCOL: assert res == (True, GRAPHQL_TRANSPORT_WS_PROTOCOL) await self.send_json_to( transport_ws_types.ConnectionInitMessage( {"type": "connection_init", "payload": self.connection_params} ) ) transport_ws_connection_ack_message: transport_ws_types.ConnectionAckMessage = await self.receive_json_from() assert transport_ws_connection_ack_message == {"type": "connection_ack"} else: assert res == (True, GRAPHQL_WS_PROTOCOL) await self.send_json_to( ws_types.ConnectionInitMessage({"type": "connection_init"}) ) ws_connection_ack_message: ws_types.ConnectionAckMessage = ( await self.receive_json_from() ) assert ws_connection_ack_message["type"] == "connection_ack" # Actual `ExecutionResult`` objects are not available client-side, since they # get transformed into `FormattedExecutionResult` on the wire, but we attempt # to do a limited representation of them here, to make testing simpler. async def subscribe( self, query: str, variables: dict | None = None ) -> ExecutionResult | AsyncIterator[ExecutionResult]: id_ = uuid.uuid4().hex if self.protocol == GRAPHQL_TRANSPORT_WS_PROTOCOL: await self.send_json_to( transport_ws_types.SubscribeMessage( { "id": id_, "type": "subscribe", "payload": {"query": query, "variables": variables}, } ) ) else: start_message: ws_types.StartMessage = { "type": "start", "id": id_, "payload": { "query": query, }, } if variables is not None: start_message["payload"]["variables"] = variables await self.send_json_to(start_message) while True: message: transport_ws_types.Message = await self.receive_json_from( timeout=5 ) if message["type"] == "next": payload = message["payload"] ret = ExecutionResult(payload.get("data"), None) if "errors" in payload: ret.errors = self.process_errors(payload.get("errors") or []) ret.extensions = payload.get("extensions", None) yield ret elif message["type"] == "error": error_payload = message["payload"] yield ExecutionResult( data=None, errors=self.process_errors(error_payload) ) return # an error message is the last message for a subscription else: return def process_errors(self, errors: list[GraphQLFormattedError]) -> list[GraphQLError]: """Reconstructs a GraphQLError from a FormattedGraphQLError.""" result = [] for f_error in errors: error = GraphQLError( message=f_error["message"], extensions=f_error.get("extensions", None), ) error.path = f_error.get("path", None) result.append(error) return result __all__ = ["GraphQLWebsocketCommunicator"] strawberry-graphql-0.287.0/strawberry/cli/000077500000000000000000000000001511033167500205055ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/cli/__init__.py000066400000000000000000000012771511033167500226250ustar00rootroot00000000000000try: from .app import app from .commands.codegen import codegen as codegen from .commands.dev import dev as dev from .commands.export_schema import export_schema as export_schema from .commands.locate_definition import ( locate_definition as locate_definition, ) from .commands.schema_codegen import ( schema_codegen as schema_codegen, ) from .commands.server import server as server from .commands.upgrade import upgrade as upgrade def run() -> None: app() except ModuleNotFoundError as exc: from strawberry.exceptions import MissingOptionalDependenciesError raise MissingOptionalDependenciesError(extras=["cli"]) from exc strawberry-graphql-0.287.0/strawberry/cli/app.py000066400000000000000000000000661511033167500216410ustar00rootroot00000000000000import typer app = typer.Typer(no_args_is_help=True) strawberry-graphql-0.287.0/strawberry/cli/commands/000077500000000000000000000000001511033167500223065ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/cli/commands/__init__.py000066400000000000000000000000001511033167500244050ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/cli/commands/codegen.py000066400000000000000000000072731511033167500242750ustar00rootroot00000000000000from __future__ import annotations import functools import importlib import inspect from pathlib import Path # noqa: TC003 from typing import cast import rich import typer from strawberry.cli.app import app from strawberry.cli.utils import load_schema from strawberry.codegen import ConsolePlugin, QueryCodegen, QueryCodegenPlugin def _is_codegen_plugin(obj: object) -> bool: return ( inspect.isclass(obj) and issubclass(obj, (QueryCodegenPlugin, ConsolePlugin)) and obj is not QueryCodegenPlugin ) def _import_plugin(plugin: str) -> type[QueryCodegenPlugin] | None: module_name = plugin symbol_name: str | None = None if ":" in plugin: module_name, symbol_name = plugin.split(":", 1) try: module = importlib.import_module(module_name) except ModuleNotFoundError: return None if symbol_name: obj = getattr(module, symbol_name) assert _is_codegen_plugin(obj) return obj symbols = { key: value for key, value in module.__dict__.items() if not key.startswith("__") } if "__all__" in module.__dict__: symbols = { name: symbol for name, symbol in symbols.items() if name in module.__dict__["__all__"] } for obj in symbols.values(): if _is_codegen_plugin(obj): return obj return None @functools.lru_cache def _load_plugin( plugin_path: str, ) -> type[QueryCodegenPlugin | ConsolePlugin]: # try to import plugin_name from current folder # then try to import from strawberry.codegen.plugins plugin = _import_plugin(plugin_path) if plugin is None and "." not in plugin_path: plugin = _import_plugin(f"strawberry.codegen.plugins.{plugin_path}") if plugin is None: rich.print(f"[red]Error: Plugin {plugin_path} not found") raise typer.Exit(1) return plugin def _load_plugins( plugin_ids: list[str], query: Path ) -> list[QueryCodegenPlugin | ConsolePlugin]: plugins = [] for ptype_id in plugin_ids: ptype = _load_plugin(ptype_id) plugin = ptype(query) plugins.append(plugin) return plugins @app.command(help="Generate code from a query") def codegen( query: list[Path] | None = typer.Argument( default=None, exists=True, dir_okay=False ), schema: str = typer.Option(..., help="Python path to the schema file"), app_dir: str = typer.Option( ".", "--app-dir", show_default=True, help=( "Look for the module in the specified directory, by adding this to the " "PYTHONPATH. Defaults to the current working directory. " "Works the same as `--app-dir` in uvicorn." ), ), output_dir: Path = typer.Option( ..., "-o", "--output-dir", exists=True, file_okay=False, dir_okay=True, writable=True, resolve_path=True, ), selected_plugins: list[str] = typer.Option( ..., "-p", "--plugins", ), cli_plugin: str | None = None, ) -> None: if not query: return schema_symbol = load_schema(schema, app_dir) console_plugin_type = _load_plugin(cli_plugin) if cli_plugin else ConsolePlugin console_plugin = console_plugin_type(output_dir) assert isinstance(console_plugin, ConsolePlugin) console_plugin.before_any_start() for q in query: plugins = cast("list[QueryCodegenPlugin]", _load_plugins(selected_plugins, q)) code_generator = QueryCodegen( schema_symbol, plugins=plugins, console_plugin=console_plugin ) code_generator.run(q.read_text()) console_plugin.after_all_finished() strawberry-graphql-0.287.0/strawberry/cli/commands/dev.py000066400000000000000000000036601511033167500234430ustar00rootroot00000000000000import os import sys from enum import Enum from typing import Annotated import rich import typer from strawberry.cli.app import app from strawberry.cli.constants import DEV_SERVER_SCHEMA_ENV_VAR_KEY from strawberry.cli.utils import load_schema class LogLevel(str, Enum): critical = "critical" error = "error" warning = "warning" info = "info" debug = "debug" trace = "trace" @app.command(help="Starts the dev server") def dev( schema: str, host: Annotated[ str, typer.Option("-h", "--host", help="Host to bind the server to.") ] = "0.0.0.0", # noqa: S104 port: Annotated[ int, typer.Option("-p", "--port", help="Port to bind the server to.") ] = 8000, log_level: Annotated[ LogLevel, typer.Option( "--log-level", help="Passed to uvicorn to determine the server log level." ), ] = LogLevel.error, app_dir: Annotated[ str, typer.Option( "--app-dir", help="Look for the schema module in the specified directory, by adding this to the PYTHONPATH. Defaults to the current working directory.", ), ] = ".", ) -> None: try: import starlette # noqa: F401 import uvicorn except ImportError: rich.print( "[red]Error: The dev server requires additional packages, install them by running:\n" r"pip install 'strawberry-graphql\[cli]'" ) raise typer.Exit(1) from None sys.path.insert(0, app_dir) load_schema(schema, app_dir=app_dir) os.environ[DEV_SERVER_SCHEMA_ENV_VAR_KEY] = schema asgi_app = "strawberry.cli.dev_server:app" end = " 🍓\n" if sys.platform != "win32" else "\n" rich.print(f"Running strawberry on http://{host}:{port}/graphql", end=end) uvicorn.run( asgi_app, host=host, port=port, log_level=log_level, reload=True, reload_dirs=[app_dir], ) strawberry-graphql-0.287.0/strawberry/cli/commands/export_schema.py000066400000000000000000000020311511033167500255150ustar00rootroot00000000000000from pathlib import Path import typer from strawberry.cli.app import app from strawberry.cli.utils import load_schema from strawberry.printer import print_schema @app.command(help="Exports the schema") def export_schema( schema: str, app_dir: str = typer.Option( ".", "--app-dir", show_default=True, help=( "Look for the module in the specified directory, by adding this to the " "PYTHONPATH. Defaults to the current working directory. " "Works the same as `--app-dir` in uvicorn." ), ), output: Path = typer.Option( None, "--output", "-o", help="File to save the exported schema. If not provided, prints to console.", ), ) -> None: schema_symbol = load_schema(schema, app_dir) schema_text = print_schema(schema_symbol) if output: Path(output).write_text(schema_text + "\n", encoding="utf-8") typer.echo(f"Schema exported to {output}") else: print(schema_text) # noqa: T201 strawberry-graphql-0.287.0/strawberry/cli/commands/locate_definition.py000066400000000000000000000017511511033167500263430ustar00rootroot00000000000000import sys import typer from rich.console import Console from strawberry.cli.app import app from strawberry.cli.utils import load_schema from strawberry.utils.locate_definition import ( locate_definition as locate_definition_util, ) err_console = Console(stderr=True) @app.command(help="Locate a definition in the schema (output: path:line:column)") def locate_definition( schema: str, symbol: str, app_dir: str = typer.Option( ".", "--app-dir", show_default=True, help=( "Look for the module in the specified directory, by adding this to the " "PYTHONPATH. Defaults to the current working directory. " "Works the same as `--app-dir` in uvicorn." ), ), ) -> None: schema_symbol = load_schema(schema, app_dir) if location := locate_definition_util(schema_symbol, symbol): typer.echo(location) else: err_console.print(f"Definition not found: {symbol}") sys.exit(1) strawberry-graphql-0.287.0/strawberry/cli/commands/schema_codegen.py000066400000000000000000000013161511033167500256050ustar00rootroot00000000000000from pathlib import Path import typer from strawberry.cli.app import app from strawberry.schema_codegen import codegen @app.command(help="Generate code from a query") def schema_codegen( schema: Path = typer.Argument(exists=True), output: Path | None = typer.Option( None, "-o", "--output", file_okay=True, dir_okay=False, writable=True, resolve_path=True, ), ) -> None: generated_output = codegen(schema.read_text()) if output is None: typer.echo(generated_output) return output.parent.mkdir(parents=True, exist_ok=True) output.write_text(generated_output) typer.echo(f"Code generated at `{output.name}`") strawberry-graphql-0.287.0/strawberry/cli/commands/server.py000066400000000000000000000020521511033167500241650ustar00rootroot00000000000000from enum import Enum import typer from strawberry.cli.app import app class LogLevel(str, Enum): debug = "debug" info = "info" warning = "warning" error = "error" __slots__ = () @app.command(help="Starts debug server") def server( schema: str, host: str = typer.Option("0.0.0.0", "-h", "--host", show_default=True), # noqa: S104 port: int = typer.Option(8000, "-p", "--port", show_default=True), log_level: LogLevel = typer.Option( "error", "--log-level", help="passed to uvicorn to determine the log level", ), app_dir: str = typer.Option( ".", "--app-dir", show_default=True, help=( "Look for the module in the specified directory, by adding this to the " "PYTHONPATH. Defaults to the current working directory. " "Works the same as `--app-dir` in uvicorn." ), ), ) -> None: typer.echo( "The `strawberry server` command is deprecated, use `strawberry dev` instead." ) raise typer.Exit(1) strawberry-graphql-0.287.0/strawberry/cli/commands/upgrade/000077500000000000000000000000001511033167500237355ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/cli/commands/upgrade/__init__.py000066400000000000000000000046731511033167500260600ustar00rootroot00000000000000from __future__ import annotations import glob import pathlib # noqa: TC003 import sys import rich import typer from libcst.codemod import CodemodContext from strawberry.cli.app import app from strawberry.codemods.annotated_unions import ConvertUnionToAnnotatedUnion from strawberry.codemods.maybe_optional import ConvertMaybeToOptional from strawberry.codemods.update_imports import UpdateImportsCodemod from ._run_codemod import run_codemod codemods = { "annotated-union": ConvertUnionToAnnotatedUnion, "update-imports": UpdateImportsCodemod, "maybe-optional": ConvertMaybeToOptional, } # TODO: add support for running all of them @app.command(help="Upgrades a Strawberry project to the latest version") def upgrade( codemod: str = typer.Argument( ..., autocompletion=lambda: list(codemods.keys()), help="Name of the upgrade to run", ), paths: list[pathlib.Path] = typer.Argument(..., file_okay=True, dir_okay=True), python_target: str = typer.Option( ".".join(str(x) for x in sys.version_info[:2]), "--python-target", help="Python version to target", ), use_typing_extensions: bool = typer.Option( False, "--use-typing-extensions", help="Use typing_extensions instead of typing for newer features", ), ) -> None: if codemod not in codemods: rich.print(f'[red]Upgrade named "{codemod}" does not exist') raise typer.Exit(2) transformer: ConvertUnionToAnnotatedUnion | UpdateImportsCodemod if codemod == "update-imports": transformer = UpdateImportsCodemod(context=CodemodContext()) else: transformer = ConvertUnionToAnnotatedUnion( CodemodContext(), use_pipe_syntax=True, use_typing_extensions=use_typing_extensions, ) files: list[str] = [] for path in paths: if path.is_dir(): glob_path = str(path / "**/*.py") files.extend(glob.glob(glob_path, recursive=True)) # noqa: PTH207 else: files.append(str(path)) files = list(set(files)) results = list(run_codemod(transformer, files)) changed = [result for result in results if result.changed] rich.print() rich.print("[green]Upgrade completed successfully, here's a summary:") rich.print(f" - {len(changed)} files changed") rich.print(f" - {len(results) - len(changed)} files skipped") if changed: raise typer.Exit(1) strawberry-graphql-0.287.0/strawberry/cli/commands/upgrade/_fake_progress.py000066400000000000000000000007441511033167500273050ustar00rootroot00000000000000from typing import Any from rich.progress import TaskID class FakeProgress: """A fake progress bar that does nothing. This is used when the user has only one file to process. """ def advance(self, task_id: TaskID) -> None: pass def add_task(self, *args: Any, **kwargs: Any) -> TaskID: return TaskID(0) def __enter__(self) -> "FakeProgress": return self def __exit__(self, *args: object, **kwargs: Any) -> None: pass strawberry-graphql-0.287.0/strawberry/cli/commands/upgrade/_run_codemod.py000066400000000000000000000042011511033167500267410ustar00rootroot00000000000000from __future__ import annotations import contextlib import os from importlib.metadata import version from multiprocessing import Pool, cpu_count from typing import TYPE_CHECKING, Any, TypeAlias from libcst.codemod._cli import ExecutionConfig, ExecutionResult, _execute_transform from rich.progress import Progress from ._fake_progress import FakeProgress if TYPE_CHECKING: from collections.abc import Generator, Sequence from libcst.codemod import Codemod ProgressType: TypeAlias = type[Progress] | type[FakeProgress] def _get_libcst_version() -> tuple[int, int, int]: package_version_str = version("libcst") try: major, minor, patch = map(int, package_version_str.split(".")) except ValueError: major, minor, patch = (0, 0, 0) return major, minor, patch def _execute_transform_wrap( job: dict[str, Any], ) -> ExecutionResult: additional_kwargs: dict[str, Any] = {} libcst_version = _get_libcst_version() if (1, 4, 0) <= libcst_version < (1, 8, 0): additional_kwargs["scratch"] = {} if libcst_version >= (1, 8, 0): additional_kwargs["original_scratch"] = {} additional_kwargs["codemod_args"] = {} additional_kwargs["repo_manager"] = None # TODO: maybe capture warnings? with open(os.devnull, "w") as null, contextlib.redirect_stderr(null): # noqa: PTH123 return _execute_transform(**job, **additional_kwargs) def run_codemod( codemod: Codemod, files: Sequence[str], ) -> Generator[ExecutionResult, None, None]: chunk_size = 4 total = len(files) jobs = min(cpu_count(), (total + chunk_size - 1) // chunk_size) config = ExecutionConfig() tasks = [ { "transformer": codemod, "filename": filename, "config": config, } for filename in files ] with Pool(processes=jobs) as p, Progress() as progress: task_id = progress.add_task("[cyan]Updating...", total=len(tasks)) for result in p.imap_unordered( _execute_transform_wrap, tasks, chunksize=chunk_size ): progress.advance(task_id) yield result strawberry-graphql-0.287.0/strawberry/cli/constants.py000066400000000000000000000000771511033167500230770ustar00rootroot00000000000000DEV_SERVER_SCHEMA_ENV_VAR_KEY = "STRAWBERRY_DEV_SERVER_SCHEMA" strawberry-graphql-0.287.0/strawberry/cli/dev_server.py000066400000000000000000000015401511033167500232230ustar00rootroot00000000000000import os from typing import Any from starlette.applications import Starlette from starlette.middleware.cors import CORSMiddleware from strawberry import Schema from strawberry.asgi import GraphQL from strawberry.cli.constants import DEV_SERVER_SCHEMA_ENV_VAR_KEY from strawberry.utils.importer import import_module_symbol app = Starlette(debug=True) app.add_middleware( CORSMiddleware, allow_headers=["*"], allow_origins=["*"], allow_methods=["*"] ) schema_import_string = os.environ[DEV_SERVER_SCHEMA_ENV_VAR_KEY] schema_symbol = import_module_symbol(schema_import_string, default_symbol_name="schema") assert isinstance(schema_symbol, Schema) graphql_app = GraphQL[Any, Any](schema_symbol) paths = ["/", "/graphql"] for path in paths: app.add_route(path, graphql_app) # type: ignore app.add_websocket_route(path, graphql_app) # type: ignore strawberry-graphql-0.287.0/strawberry/cli/utils/000077500000000000000000000000001511033167500216455ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/cli/utils/__init__.py000066400000000000000000000017331511033167500237620ustar00rootroot00000000000000import sys import rich import typer from strawberry import Schema from strawberry.utils.importer import import_module_symbol def load_schema(schema: str, app_dir: str) -> Schema: sys.path.insert(0, app_dir) try: schema_symbol = import_module_symbol(schema, default_symbol_name="schema") except (ImportError, AttributeError) as exc: message = str(exc) rich.print(f"[red]Error: {message}") raise typer.Exit(2) # noqa: B904 if callable(schema_symbol): try: schema_symbol = schema_symbol() except Exception as exc: # noqa: BLE001 message = f"Error invoking schema_symbol: {exc}" rich.print(f"[red]Error: {message}") raise typer.Exit(2) # noqa: B904 if not isinstance(schema_symbol, Schema): message = "The `schema` must be an instance of strawberry.Schema" rich.print(f"[red]Error: {message}") raise typer.Exit(2) return schema_symbol strawberry-graphql-0.287.0/strawberry/cli/utils/load_schema.py000066400000000000000000000000001511033167500244440ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/codegen/000077500000000000000000000000001511033167500213425ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/codegen/__init__.py000066400000000000000000000003721511033167500234550ustar00rootroot00000000000000from .query_codegen import ( CodegenFile, CodegenResult, ConsolePlugin, QueryCodegen, QueryCodegenPlugin, ) __all__ = [ "CodegenFile", "CodegenResult", "ConsolePlugin", "QueryCodegen", "QueryCodegenPlugin", ] strawberry-graphql-0.287.0/strawberry/codegen/exceptions.py000066400000000000000000000005551511033167500241020ustar00rootroot00000000000000class CodegenError(Exception): pass class NoOperationProvidedError(CodegenError): pass class NoOperationNameProvidedError(CodegenError): pass class MultipleOperationsProvidedError(CodegenError): pass __all__ = [ "CodegenError", "MultipleOperationsProvidedError", "NoOperationNameProvidedError", "NoOperationProvidedError", ] strawberry-graphql-0.287.0/strawberry/codegen/plugins/000077500000000000000000000000001511033167500230235ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/codegen/plugins/__init__.py000066400000000000000000000000001511033167500251220ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/codegen/plugins/print_operation.py000066400000000000000000000152021511033167500266110ustar00rootroot00000000000000from __future__ import annotations import textwrap from typing import TYPE_CHECKING from strawberry.codegen import CodegenFile, QueryCodegenPlugin from strawberry.codegen.types import ( GraphQLBoolValue, GraphQLEnumValue, GraphQLField, GraphQLFieldSelection, GraphQLFragmentSpread, GraphQLFragmentType, GraphQLInlineFragment, GraphQLIntValue, GraphQLList, GraphQLListValue, GraphQLObjectType, GraphQLObjectValue, GraphQLOptional, GraphQLStringValue, GraphQLVariableReference, ) if TYPE_CHECKING: from strawberry.codegen.types import ( GraphQLArgument, GraphQLArgumentValue, GraphQLDirective, GraphQLOperation, GraphQLSelection, GraphQLType, ) class PrintOperationPlugin(QueryCodegenPlugin): def generate_code( self, types: list[GraphQLType], operation: GraphQLOperation ) -> list[CodegenFile]: code_lines = [] for t in types: if not isinstance(t, GraphQLFragmentType): continue code_lines.append(self._print_fragment(t)) code = "\n".join( [ *code_lines, ( f"{operation.kind} {operation.name}" f"{self._print_operation_variables(operation)}" f"{self._print_directives(operation.directives)} {{" ), self._print_selections(operation.selections), "}", ] ) return [CodegenFile("query.graphql", code)] def _print_fragment_field(self, field: GraphQLField, indent: str = "") -> str: code_lines = [] if isinstance(field.type, GraphQLObjectType): code_lines.append(f"{indent}{field.name} {{") for subfield in field.type.fields: code_lines.append( # noqa: PERF401 self._print_fragment_field(subfield, indent=indent + " ") ) code_lines.append(f"{indent}}}") else: code_lines.append(f"{indent}{field.name}") return "\n".join(code_lines) def _print_fragment(self, fragment: GraphQLFragmentType) -> str: code_lines = [] code_lines.append(f"fragment {fragment.name} on {fragment.on} {{") for field in fragment.fields: code_lines.append( # noqa: PERF401 self._print_fragment_field(field, indent=" ") ) code_lines.append("}") code_lines.append("") return "\n".join(code_lines) def _print_operation_variables(self, operation: GraphQLOperation) -> str: if not operation.variables: return "" variables = ", ".join( f"${v.name}: {self._print_graphql_type(v.type)}" for v in operation.variables ) return f"({variables})" def _print_graphql_type( self, type: GraphQLType, parent_type: GraphQLType | None = None ) -> str: if isinstance(type, GraphQLOptional): return self._print_graphql_type(type.of_type, type) if isinstance(type, GraphQLList): type_name = f"[{self._print_graphql_type(type.of_type, type)}]" else: type_name = type.name if parent_type and isinstance(parent_type, GraphQLOptional): return type_name return f"{type_name}!" def _print_argument_value(self, value: GraphQLArgumentValue) -> str: if isinstance(value, GraphQLStringValue): return f'"{value.value}"' if isinstance(value, GraphQLIntValue): return str(value.value) if isinstance(value, GraphQLVariableReference): return f"${value.value}" if isinstance(value, GraphQLListValue): return f"[{', '.join(self._print_argument_value(v) for v in value.values)}]" if isinstance(value, GraphQLEnumValue): return value.name if isinstance(value, GraphQLBoolValue): return str(value.value).lower() if isinstance(value, GraphQLObjectValue): return ( "{" + ", ".join( f"{name}: {self._print_argument_value(v)}" for name, v in value.values.items() ) + "}" ) raise ValueError(f"not supported: {type(value)}") # pragma: no cover def _print_arguments(self, arguments: list[GraphQLArgument]) -> str: if not arguments: return "" return ( "(" + ", ".join( [ f"{argument.name}: {self._print_argument_value(argument.value)}" for argument in arguments ] ) + ")" ) def _print_directives(self, directives: list[GraphQLDirective]) -> str: if not directives: return "" return " " + " ".join( [ f"@{directive.name}{self._print_arguments(directive.arguments)}" for directive in directives ] ) def _print_field_selection(self, selection: GraphQLFieldSelection) -> str: field = ( f"{selection.field}" f"{self._print_arguments(selection.arguments)}" f"{self._print_directives(selection.directives)}" ) if selection.alias: field = f"{selection.alias}: {field}" if selection.selections: return field + f" {{\n{self._print_selections(selection.selections)}\n}}" return field def _print_inline_fragment(self, fragment: GraphQLInlineFragment) -> str: return "\n".join( [ f"... on {fragment.type_condition} {{", self._print_selections(fragment.selections), "}", ] ) def _print_fragment_spread(self, fragment: GraphQLFragmentSpread) -> str: return f"...{fragment.name}" def _print_selection(self, selection: GraphQLSelection) -> str: if isinstance(selection, GraphQLFieldSelection): return self._print_field_selection(selection) if isinstance(selection, GraphQLInlineFragment): return self._print_inline_fragment(selection) if isinstance(selection, GraphQLFragmentSpread): return self._print_fragment_spread(selection) raise ValueError(f"Unsupported selection: {selection}") # pragma: no cover def _print_selections(self, selections: list[GraphQLSelection]) -> str: selections_text = "\n".join( [self._print_selection(selection) for selection in selections] ) return textwrap.indent(selections_text, " " * 2) strawberry-graphql-0.287.0/strawberry/codegen/plugins/python.py000066400000000000000000000153521511033167500247240ustar00rootroot00000000000000from __future__ import annotations import textwrap from collections import defaultdict from dataclasses import dataclass from typing import TYPE_CHECKING, ClassVar from strawberry.codegen import CodegenFile, QueryCodegenPlugin from strawberry.codegen.types import ( GraphQLEnum, GraphQLEnumValue, GraphQLList, GraphQLNullValue, GraphQLObjectType, GraphQLOptional, GraphQLScalar, GraphQLUnion, ) if TYPE_CHECKING: from pathlib import Path from strawberry.codegen.types import ( GraphQLArgumentValue, GraphQLField, GraphQLOperation, GraphQLType, ) @dataclass class PythonType: type: str module: str | None = None class PythonPlugin(QueryCodegenPlugin): SCALARS_TO_PYTHON_TYPES: ClassVar[dict[str, PythonType]] = { "ID": PythonType("str"), "Int": PythonType("int"), "String": PythonType("str"), "Float": PythonType("float"), "Boolean": PythonType("bool"), "UUID": PythonType("UUID", "uuid"), "Date": PythonType("date", "datetime"), "DateTime": PythonType("datetime", "datetime"), "Time": PythonType("time", "datetime"), "Decimal": PythonType("Decimal", "decimal"), } def __init__(self, query: Path) -> None: self.imports: dict[str, set[str]] = defaultdict(set) self.outfile_name: str = query.with_suffix(".py").name self.query = query def generate_code( self, types: list[GraphQLType], operation: GraphQLOperation ) -> list[CodegenFile]: printed_types = list(filter(None, (self._print_type(type) for type in types))) imports = self._print_imports() code = imports + "\n\n" + "\n\n".join(printed_types) return [CodegenFile(self.outfile_name, code.strip())] def _print_imports(self) -> str: imports = [ f"from {import_} import {', '.join(sorted(types))}" for import_, types in self.imports.items() ] return "\n".join(imports) def _get_type_name(self, type_: GraphQLType) -> str: if isinstance(type_, GraphQLOptional): self.imports["typing"].add("Optional") return f"Optional[{self._get_type_name(type_.of_type)}]" if isinstance(type_, GraphQLList): self.imports["typing"].add("List") return f"list[{self._get_type_name(type_.of_type)}]" if isinstance(type_, GraphQLUnion): # TODO: wrong place for this self.imports["typing"].add("Union") return type_.name if isinstance(type_, (GraphQLObjectType, GraphQLEnum)): if isinstance(type_, GraphQLEnum): self.imports["enum"].add("Enum") return type_.name if ( isinstance(type_, GraphQLScalar) and type_.name in self.SCALARS_TO_PYTHON_TYPES ): python_type = self.SCALARS_TO_PYTHON_TYPES[type_.name] if python_type.module is not None: self.imports[python_type.module].add(python_type.type) return python_type.type self.imports["typing"].add("NewType") return type_.name def _print_field(self, field: GraphQLField) -> str: name = field.name if field.alias: name = f"# alias for {field.name}\n{field.alias}" default_value = "" if field.default_value is not None: default_value = f" = {self._print_argument_value(field.default_value)}" return f"{name}: {self._get_type_name(field.type)}{default_value}" def _print_argument_value(self, argval: GraphQLArgumentValue) -> str: if hasattr(argval, "values"): if isinstance(argval.values, list): return ( "[" + ", ".join(self._print_argument_value(v) for v in argval.values) + "]" ) if isinstance(argval.values, dict): return ( "{" + ", ".join( f"{k!r}: {self._print_argument_value(v)}" for k, v in argval.values.items() ) + "}" ) raise TypeError(f"Unrecognized values type: {argval}") if isinstance(argval, GraphQLEnumValue): # This is an enum. It needs the namespace alongside the name. if argval.enum_type is None: raise ValueError( "GraphQLEnumValue must have a type for python code gen. {argval}" ) return f"{argval.enum_type}.{argval.name}" if isinstance(argval, GraphQLNullValue): return "None" if not hasattr(argval, "value"): raise TypeError(f"Unrecognized values type: {argval}") return repr(argval.value) def _print_enum_value(self, value: str) -> str: return f'{value} = "{value}"' def _print_object_type(self, type_: GraphQLObjectType) -> str: fields = "\n".join( self._print_field(field) for field in type_.fields if field.name != "__typename" ) indent = 4 * " " lines = [ f"class {type_.name}:", ] if type_.graphql_typename: lines.append( textwrap.indent(f"# typename: {type_.graphql_typename}", indent) ) lines.append(textwrap.indent(fields, indent)) return "\n".join(lines) def _print_enum_type(self, type_: GraphQLEnum) -> str: values = "\n".join(self._print_enum_value(value) for value in type_.values) return "\n".join( [ f"class {type_.name}(Enum):", textwrap.indent(values, " " * 4), ] ) def _print_scalar_type(self, type_: GraphQLScalar) -> str: if type_.name in self.SCALARS_TO_PYTHON_TYPES: return "" assert type_.python_type is not None, ( f"Scalar type must have a python type: {type_.name}" ) return f'{type_.name} = NewType("{type_.name}", {type_.python_type.__name__})' def _print_union_type(self, type_: GraphQLUnion) -> str: return f"{type_.name} = Union[{', '.join([t.name for t in type_.types])}]" def _print_type(self, type_: GraphQLType) -> str: if isinstance(type_, GraphQLUnion): return self._print_union_type(type_) if isinstance(type_, GraphQLObjectType): return self._print_object_type(type_) if isinstance(type_, GraphQLEnum): return self._print_enum_type(type_) if isinstance(type_, GraphQLScalar): return self._print_scalar_type(type_) raise ValueError(f"Unknown type: {type}") # pragma: no cover __all__ = ["PythonPlugin"] strawberry-graphql-0.287.0/strawberry/codegen/plugins/typescript.py000066400000000000000000000074311511033167500256100ustar00rootroot00000000000000from __future__ import annotations import textwrap from typing import TYPE_CHECKING, ClassVar from strawberry.codegen import CodegenFile, QueryCodegenPlugin from strawberry.codegen.types import ( GraphQLEnum, GraphQLList, GraphQLObjectType, GraphQLOptional, GraphQLScalar, GraphQLUnion, ) if TYPE_CHECKING: from pathlib import Path from strawberry.codegen.types import GraphQLField, GraphQLOperation, GraphQLType class TypeScriptPlugin(QueryCodegenPlugin): SCALARS_TO_TS_TYPE: ClassVar[dict[str | type, str]] = { "ID": "string", "Int": "number", "String": "string", "Float": "number", "Boolean": "boolean", "UUID": "string", "Date": "string", "DateTime": "string", "Time": "string", "Decimal": "string", str: "string", float: "number", } def __init__(self, query: Path) -> None: self.outfile_name: str = query.with_suffix(".ts").name self.query = query def generate_code( self, types: list[GraphQLType], operation: GraphQLOperation ) -> list[CodegenFile]: printed_types = list(filter(None, (self._print_type(type) for type in types))) return [CodegenFile(self.outfile_name, "\n\n".join(printed_types))] def _get_type_name(self, type_: GraphQLType) -> str: if isinstance(type_, GraphQLOptional): return f"{self._get_type_name(type_.of_type)} | undefined" if isinstance(type_, GraphQLList): child_type = self._get_type_name(type_.of_type) if "|" in child_type: child_type = f"({child_type})" return f"{child_type}[]" if isinstance(type_, GraphQLUnion): return type_.name if isinstance(type_, (GraphQLObjectType, GraphQLEnum)): return type_.name if isinstance(type_, GraphQLScalar) and type_.name in self.SCALARS_TO_TS_TYPE: return self.SCALARS_TO_TS_TYPE[type_.name] return type_.name def _print_field(self, field: GraphQLField) -> str: name = field.name if field.alias: name = f"// alias for {field.name}\n{field.alias}" return f"{name}: {self._get_type_name(field.type)}" def _print_enum_value(self, value: str) -> str: return f'{value} = "{value}",' def _print_object_type(self, type_: GraphQLObjectType) -> str: fields = "\n".join(self._print_field(field) for field in type_.fields) return "\n".join( [f"type {type_.name} = {{", textwrap.indent(fields, " " * 4), "}"], ) def _print_enum_type(self, type_: GraphQLEnum) -> str: values = "\n".join(self._print_enum_value(value) for value in type_.values) return "\n".join( [ f"enum {type_.name} {{", textwrap.indent(values, " " * 4), "}", ] ) def _print_scalar_type(self, type_: GraphQLScalar) -> str: if type_.name in self.SCALARS_TO_TS_TYPE: return "" assert type_.python_type is not None return f"type {type_.name} = {self.SCALARS_TO_TS_TYPE[type_.python_type]}" def _print_union_type(self, type_: GraphQLUnion) -> str: return f"type {type_.name} = {' | '.join([t.name for t in type_.types])}" def _print_type(self, type_: GraphQLType) -> str: if isinstance(type_, GraphQLUnion): return self._print_union_type(type_) if isinstance(type_, GraphQLObjectType): return self._print_object_type(type_) if isinstance(type_, GraphQLEnum): return self._print_enum_type(type_) if isinstance(type_, GraphQLScalar): return self._print_scalar_type(type_) raise ValueError(f"Unknown type: {type}") # pragma: no cover strawberry-graphql-0.287.0/strawberry/codegen/query_codegen.py000066400000000000000000000731501511033167500245530ustar00rootroot00000000000000from __future__ import annotations from collections.abc import Callable, Iterable, Mapping, Sequence from dataclasses import MISSING, dataclass from enum import Enum from functools import cmp_to_key, partial from pathlib import Path from typing import ( TYPE_CHECKING, Any, cast, ) from typing_extensions import Protocol import rich from graphql import ( BooleanValueNode, EnumValueNode, FieldNode, FloatValueNode, FragmentDefinitionNode, FragmentSpreadNode, InlineFragmentNode, IntValueNode, ListTypeNode, ListValueNode, NamedTypeNode, NonNullTypeNode, NullValueNode, ObjectValueNode, OperationDefinitionNode, StringValueNode, VariableNode, parse, ) from strawberry.types.base import ( StrawberryList, StrawberryObjectDefinition, StrawberryOptional, StrawberryType, get_object_definition, has_object_definition, ) from strawberry.types.enum import StrawberryEnumDefinition from strawberry.types.lazy_type import LazyType from strawberry.types.scalar import ScalarDefinition, ScalarWrapper from strawberry.types.union import StrawberryUnion from strawberry.types.unset import UNSET from strawberry.utils.str_converters import capitalize_first, to_camel_case from .exceptions import ( MultipleOperationsProvidedError, NoOperationNameProvidedError, NoOperationProvidedError, ) from .types import ( GraphQLArgument, GraphQLBoolValue, GraphQLDirective, GraphQLEnum, GraphQLEnumValue, GraphQLField, GraphQLFieldSelection, GraphQLFloatValue, GraphQLFragmentSpread, GraphQLFragmentType, GraphQLInlineFragment, GraphQLIntValue, GraphQLList, GraphQLListValue, GraphQLNullValue, GraphQLObjectType, GraphQLObjectValue, GraphQLOperation, GraphQLOptional, GraphQLScalar, GraphQLStringValue, GraphQLUnion, GraphQLVariable, GraphQLVariableReference, ) if TYPE_CHECKING: from graphql import ( ArgumentNode, DirectiveNode, DocumentNode, SelectionNode, SelectionSetNode, TypeNode, ValueNode, VariableDefinitionNode, ) from strawberry.schema import Schema from .types import GraphQLArgumentValue, GraphQLSelection, GraphQLType @dataclass class CodegenFile: path: str content: str @dataclass class CodegenResult: files: list[CodegenFile] def to_string(self) -> str: return "\n".join(f.content for f in self.files) + "\n" def write(self, folder: Path) -> None: for file in self.files: destination = folder / file.path destination.parent.mkdir(exist_ok=True, parents=True) destination.write_text(file.content) class HasSelectionSet(Protocol): selection_set: SelectionSetNode | None class QueryCodegenPlugin: def __init__(self, query: Path) -> None: """Initialize the plugin. The singular argument is the path to the file that is being processed by this plugin. """ self.query = query def on_start(self) -> None: ... def on_end(self, result: CodegenResult) -> None: ... def generate_code( self, types: list[GraphQLType], operation: GraphQLOperation ) -> list[CodegenFile]: return [] class ConsolePlugin: def __init__(self, output_dir: Path) -> None: self.output_dir = output_dir self.files_generated: list[Path] = [] def before_any_start(self) -> None: rich.print( "[bold yellow]The codegen is experimental. Please submit any bug at " "https://github.com/strawberry-graphql/strawberry\n", ) def after_all_finished(self) -> None: rich.print("[green]Generated:") for fname in self.files_generated: rich.print(f" {fname}") def on_start(self, plugins: Iterable[QueryCodegenPlugin], query: Path) -> None: plugin_names = [plugin.__class__.__name__ for plugin in plugins] rich.print( f"[green]Generating code for {query} using " f"{', '.join(plugin_names)} plugin(s)", ) def on_end(self, result: CodegenResult) -> None: self.output_dir.mkdir(parents=True, exist_ok=True) result.write(self.output_dir) self.files_generated.extend(Path(cf.path) for cf in result.files) rich.print( f"[green] Generated {len(result.files)} files in {self.output_dir}", ) def _get_deps(t: GraphQLType) -> Iterable[GraphQLType]: """Get all the types that `t` depends on. To keep things simple, `t` depends on itself. """ yield t if isinstance(t, GraphQLObjectType): for fld in t.fields: yield from _get_deps(fld.type) elif isinstance(t, (GraphQLEnum, GraphQLScalar)): # enums and scalars have no dependent types pass elif isinstance(t, (GraphQLOptional, GraphQLList)): yield from _get_deps(t.of_type) elif isinstance(t, GraphQLUnion): for gql_type in t.types: yield from _get_deps(gql_type) else: # Want to make sure that all types are covered. raise ValueError(f"Unknown GraphQLType: {t}") # noqa: TRY004 _TYPE_TO_GRAPHQL_TYPE = { int: GraphQLIntValue, float: GraphQLFloatValue, str: GraphQLStringValue, bool: GraphQLBoolValue, } def _py_to_graphql_value(obj: Any) -> GraphQLArgumentValue: """Convert a python object to a GraphQLArgumentValue.""" if obj is None or obj is UNSET: return GraphQLNullValue(value=obj) obj_type = type(obj) if obj_type in _TYPE_TO_GRAPHQL_TYPE: return _TYPE_TO_GRAPHQL_TYPE[obj_type](obj) if issubclass(obj_type, Enum): return GraphQLEnumValue(obj.name, enum_type=obj_type.__name__) if issubclass(obj_type, Sequence): return GraphQLListValue([_py_to_graphql_value(v) for v in obj]) if issubclass(obj_type, Mapping): return GraphQLObjectValue({k: _py_to_graphql_value(v) for k, v in obj.items()}) raise ValueError(f"Cannot convet {obj!r} into a GraphQLArgumentValue") class QueryCodegenPluginManager: def __init__( self, plugins: list[QueryCodegenPlugin], console_plugin: ConsolePlugin | None = None, ) -> None: self.plugins = plugins self.console_plugin = console_plugin def _sort_types(self, types: list[GraphQLType]) -> list[GraphQLType]: """Sort the types. t1 < t2 iff t2 has a dependency on t1. t1 == t2 iff neither type has a dependency on the other. """ def type_cmp(t1: GraphQLType, t2: GraphQLType) -> int: """Compare the types.""" if t1 is t2: retval = 0 elif t1 in _get_deps(t2): retval = -1 elif t2 in _get_deps(t1): retval = 1 else: retval = 0 return retval return sorted(types, key=cmp_to_key(type_cmp)) def generate_code( self, types: list[GraphQLType], operation: GraphQLOperation ) -> CodegenResult: result = CodegenResult(files=[]) types = self._sort_types(types) for plugin in self.plugins: files = plugin.generate_code(types, operation) result.files.extend(files) return result def on_start(self) -> None: if self.console_plugin and self.plugins: # We need the query that we're processing # just pick it off the first plugin query = self.plugins[0].query self.console_plugin.on_start(self.plugins, query) for plugin in self.plugins: plugin.on_start() def on_end(self, result: CodegenResult) -> None: for plugin in self.plugins: plugin.on_end(result) if self.console_plugin: self.console_plugin.on_end(result) class QueryCodegen: def __init__( self, schema: Schema, plugins: list[QueryCodegenPlugin], console_plugin: ConsolePlugin | None = None, ) -> None: self.schema = schema self.plugin_manager = QueryCodegenPluginManager(plugins, console_plugin) self.types: list[GraphQLType] = [] def run(self, query: str) -> CodegenResult: self.plugin_manager.on_start() ast = parse(query) operations = self._get_operations(ast) if not operations: raise NoOperationProvidedError if len(operations) > 1: raise MultipleOperationsProvidedError operation = operations[0] if operation.name is None: raise NoOperationNameProvidedError # Look for any free-floating fragments and create types out of them # These types can then be referenced and included later via the # fragment spread operator. self._populate_fragment_types(ast) self.operation = self._convert_operation(operation) result = self.generate_code() self.plugin_manager.on_end(result) return result def _collect_type(self, type_: GraphQLType) -> None: if type_ in self.types: return self.types.append(type_) def _populate_fragment_types(self, ast: DocumentNode) -> None: fragment_definitions = ( definition for definition in ast.definitions if isinstance(definition, FragmentDefinitionNode) ) for fd in fragment_definitions: query_type = self.schema.get_type_by_name(fd.type_condition.name.value) assert isinstance(query_type, StrawberryObjectDefinition), ( f"{fd.type_condition.name.value!r} is not a type in the graphql schema!" ) typename = fd.type_condition.name.value graph_ql_object_type_factory = partial( GraphQLFragmentType, on=typename, graphql_typename=typename, ) self._collect_types( # The FragmentDefinitionNode has a non-Optional `SelectionSetNode` but # the Protocol wants an `Optional[SelectionSetNode]` so this doesn't # quite conform. cast("HasSelectionSet", fd), parent_type=query_type, class_name=fd.name.value, graph_ql_object_type_factory=graph_ql_object_type_factory, ) def _convert_selection(self, selection: SelectionNode) -> GraphQLSelection: if isinstance(selection, FieldNode): return GraphQLFieldSelection( selection.name.value, selection.alias.value if selection.alias else None, selections=self._convert_selection_set(selection.selection_set), directives=self._convert_directives(selection.directives), arguments=self._convert_arguments(selection.arguments), ) if isinstance(selection, InlineFragmentNode): return GraphQLInlineFragment( selection.type_condition.name.value, self._convert_selection_set(selection.selection_set), ) if isinstance(selection, FragmentSpreadNode): return GraphQLFragmentSpread(selection.name.value) raise ValueError(f"Unsupported type: {type(selection)}") # pragma: no cover def _convert_selection_set( self, selection_set: SelectionSetNode | None ) -> list[GraphQLSelection]: if selection_set is None: return [] return [ self._convert_selection(selection) for selection in selection_set.selections ] def _convert_value(self, value: ValueNode) -> GraphQLArgumentValue: if isinstance(value, StringValueNode): return GraphQLStringValue(value.value) if isinstance(value, IntValueNode): return GraphQLIntValue(int(value.value)) if isinstance(value, FloatValueNode): return GraphQLFloatValue(float(value.value)) if isinstance(value, NullValueNode): return GraphQLNullValue() if isinstance(value, VariableNode): return GraphQLVariableReference(value.name.value) if isinstance(value, ListValueNode): return GraphQLListValue( [self._convert_value(item) for item in value.values] ) if isinstance(value, EnumValueNode): return GraphQLEnumValue(value.value) if isinstance(value, BooleanValueNode): return GraphQLBoolValue(value.value) if isinstance(value, ObjectValueNode): return GraphQLObjectValue( { field.name.value: self._convert_value(field.value) for field in value.fields } ) raise ValueError(f"Unsupported type: {type(value)}") # pragma: no cover def _convert_arguments( self, arguments: Iterable[ArgumentNode] ) -> list[GraphQLArgument]: return [ GraphQLArgument(argument.name.value, self._convert_value(argument.value)) for argument in arguments ] def _convert_directives( self, directives: Iterable[DirectiveNode] ) -> list[GraphQLDirective]: return [ GraphQLDirective( directive.name.value, self._convert_arguments(directive.arguments), ) for directive in directives ] def _convert_operation( self, operation_definition: OperationDefinitionNode ) -> GraphQLOperation: query_type = self.schema.get_type_by_name( operation_definition.operation.value.title() ) assert isinstance(query_type, StrawberryObjectDefinition) assert operation_definition.name is not None operation_name = operation_definition.name.value result_class_name = f"{operation_name}Result" operation_type = self._collect_types( cast("HasSelectionSet", operation_definition), parent_type=query_type, class_name=result_class_name, ) variables, variables_type = self._convert_variable_definitions( operation_definition.variable_definitions, operation_name=operation_name ) return GraphQLOperation( operation_definition.name.value, kind=operation_definition.operation.value, selections=self._convert_selection_set(operation_definition.selection_set), directives=self._convert_directives(operation_definition.directives), variables=variables, type=cast("GraphQLObjectType", operation_type), variables_type=variables_type, ) def _convert_variable_definitions( self, variable_definitions: Iterable[VariableDefinitionNode] | None, operation_name: str, ) -> tuple[list[GraphQLVariable], GraphQLObjectType | None]: if not variable_definitions: return [], None type_ = GraphQLObjectType(f"{operation_name}Variables", []) self._collect_type(type_) variables: list[GraphQLVariable] = [] for variable_definition in variable_definitions: variable_type = self._collect_type_from_variable(variable_definition.type) variable = GraphQLVariable( variable_definition.variable.name.value, variable_type, ) type_.fields.append(GraphQLField(variable.name, None, variable_type)) variables.append(variable) return variables, type_ def _get_operations(self, ast: DocumentNode) -> list[OperationDefinitionNode]: return [ definition for definition in ast.definitions if isinstance(definition, OperationDefinitionNode) ] def _get_field_type( self, field_type: StrawberryType | type, ) -> GraphQLType: if isinstance(field_type, StrawberryOptional): return GraphQLOptional(self._get_field_type(field_type.of_type)) if isinstance(field_type, StrawberryList): return GraphQLList(self._get_field_type(field_type.of_type)) if ( not isinstance(field_type, StrawberryType) and field_type in self.schema.schema_converter.scalar_registry ): field_type = self.schema.schema_converter.scalar_registry[field_type] # type: ignore if isinstance(field_type, ScalarWrapper): python_type = field_type.wrap if hasattr(python_type, "__supertype__"): python_type = python_type.__supertype__ return self._collect_scalar(field_type._scalar_definition, python_type) # type: ignore if isinstance(field_type, ScalarDefinition): return self._collect_scalar(field_type, None) if isinstance(field_type, StrawberryEnumDefinition): return self._collect_enum(field_type) raise ValueError(f"Unsupported type: {field_type}") # pragma: no cover def _collect_type_from_strawberry_type( self, strawberry_type: type | StrawberryType ) -> GraphQLType: type_: GraphQLType if isinstance(strawberry_type, StrawberryOptional): return GraphQLOptional( self._collect_type_from_strawberry_type(strawberry_type.of_type) ) if isinstance(strawberry_type, StrawberryList): return GraphQLList( self._collect_type_from_strawberry_type(strawberry_type.of_type) ) if has_object_definition(strawberry_type): strawberry_type = strawberry_type.__strawberry_definition__ if isinstance(strawberry_type, StrawberryObjectDefinition): type_ = GraphQLObjectType( strawberry_type.name, [], ) for field in strawberry_type.fields: field_type = self._collect_type_from_strawberry_type(field.type) default = None if field.default is not MISSING: default = _py_to_graphql_value(field.default) type_.fields.append( GraphQLField(field.name, None, field_type, default_value=default) ) self._collect_type(type_) else: type_ = self._get_field_type(strawberry_type) return type_ def _collect_type_from_variable( self, variable_type: TypeNode, parent_type: TypeNode | None = None ) -> GraphQLType: type_: GraphQLType | None = None if isinstance(variable_type, ListTypeNode): type_ = GraphQLList( self._collect_type_from_variable(variable_type.type, variable_type) ) elif isinstance(variable_type, NonNullTypeNode): return self._collect_type_from_variable(variable_type.type, variable_type) elif isinstance(variable_type, NamedTypeNode): strawberry_type = self.schema.get_type_by_name(variable_type.name.value) assert strawberry_type type_ = self._collect_type_from_strawberry_type(strawberry_type) assert type_ if parent_type is not None and isinstance(parent_type, NonNullTypeNode): return type_ return GraphQLOptional(type_) def _field_from_selection( self, selection: FieldNode, parent_type: StrawberryObjectDefinition ) -> GraphQLField: if selection.name.value == "__typename": return GraphQLField("__typename", None, GraphQLScalar("String", None)) field = self.schema.get_field_for_type(selection.name.value, parent_type.name) assert field, f"{parent_type.name},{selection.name.value}" field_type = self._get_field_type(field.type) return GraphQLField( field.name, selection.alias.value if selection.alias else None, field_type ) def _unwrap_type( self, type_: type | StrawberryType ) -> tuple[type | StrawberryType, Callable[[GraphQLType], GraphQLType] | None]: wrapper: Callable[[GraphQLType], GraphQLType] | None = None if isinstance(type_, StrawberryOptional): type_, previous_wrapper = self._unwrap_type(type_.of_type) wrapper = ( GraphQLOptional if previous_wrapper is None else lambda t: GraphQLOptional(previous_wrapper(t)) # type: ignore[misc] ) elif isinstance(type_, StrawberryList): type_, previous_wrapper = self._unwrap_type(type_.of_type) wrapper = ( GraphQLList if previous_wrapper is None else lambda t: GraphQLList(previous_wrapper(t)) ) elif isinstance(type_, LazyType): return self._unwrap_type(type_.resolve_type()) return type_, wrapper def _field_from_selection_set( self, selection: FieldNode, class_name: str, parent_type: StrawberryObjectDefinition, ) -> GraphQLField: assert selection.selection_set is not None parent_type_name = parent_type.name # Check if the parent type is generic. # This seems to be tracked by `strawberry` in the `type_var_map` # If the type is generic, then the strawberry generated schema # naming convention is # The implementation here assumes that the `type_var_map` is ordered, # but insertion order is maintained in python3.6+ (for CPython) and # guaranteed for all python implementations in python3.7+, so that # should be pretty safe. if parent_type.type_var_map: parent_type_name = ( "".join( c.__name__ # type: ignore[union-attr] for c in parent_type.type_var_map.values() ) + parent_type.name ) selected_field = self.schema.get_field_for_type( selection.name.value, parent_type_name ) assert selected_field, ( f"Couldn't find {parent_type_name}.{selection.name.value}" ) selected_field_type, wrapper = self._unwrap_type(selected_field.type) name = capitalize_first(to_camel_case(selection.name.value)) class_name = f"{class_name}{(name)}" field_type: GraphQLType if isinstance(selected_field_type, StrawberryUnion): field_type = self._collect_types_with_inline_fragments( selection, parent_type, class_name ) else: parent_type = get_object_definition(selected_field_type, strict=True) field_type = self._collect_types(selection, parent_type, class_name) if wrapper: field_type = wrapper(field_type) return GraphQLField( selected_field.name, selection.alias.value if selection.alias else None, field_type, ) def _get_field( self, selection: FieldNode, class_name: str, parent_type: StrawberryObjectDefinition, ) -> GraphQLField: if selection.selection_set: return self._field_from_selection_set(selection, class_name, parent_type) return self._field_from_selection(selection, parent_type) def _collect_types_with_inline_fragments( self, selection: HasSelectionSet, parent_type: StrawberryObjectDefinition, class_name: str, ) -> GraphQLObjectType | GraphQLUnion: sub_types = self._collect_types_using_fragments( selection, parent_type, class_name ) if len(sub_types) == 1: return sub_types[0] union = GraphQLUnion(class_name, sub_types) self._collect_type(union) return union def _collect_types( self, selection: HasSelectionSet, parent_type: StrawberryObjectDefinition, class_name: str, graph_ql_object_type_factory: Callable[ [str], GraphQLObjectType ] = GraphQLObjectType, ) -> GraphQLType: assert selection.selection_set is not None selection_set = selection.selection_set if any( isinstance(selection, InlineFragmentNode) for selection in selection_set.selections ): return self._collect_types_with_inline_fragments( selection, parent_type, class_name ) current_type = graph_ql_object_type_factory(class_name) fields: list[GraphQLFragmentSpread | GraphQLField] = [] for sub_selection in selection_set.selections: if isinstance(sub_selection, FragmentSpreadNode): fields.append(GraphQLFragmentSpread(sub_selection.name.value)) continue assert isinstance(sub_selection, FieldNode) field = self._get_field(sub_selection, class_name, parent_type) fields.append(field) if any(isinstance(f, GraphQLFragmentSpread) for f in fields): if len(fields) > 1: raise ValueError( "Queries with Fragments cannot currently include separate fields." ) spread_field = fields[0] assert isinstance(spread_field, GraphQLFragmentSpread) return next( t for t in self.types if isinstance(t, GraphQLObjectType) and t.name == spread_field.name ) # This cast is safe because all the fields are either # `GraphQLField` or `GraphQLFragmentSpread` # and the suite above will cause this statement to be # skipped if there are any `GraphQLFragmentSpread`. current_type.fields = cast("list[GraphQLField]", fields) self._collect_type(current_type) return current_type def generate_code(self) -> CodegenResult: return self.plugin_manager.generate_code( types=self.types, operation=self.operation ) def _collect_types_using_fragments( self, selection: HasSelectionSet, parent_type: StrawberryObjectDefinition, class_name: str, ) -> list[GraphQLObjectType]: assert selection.selection_set common_fields: list[GraphQLField] = [] fragments: list[InlineFragmentNode] = [] sub_types: list[GraphQLObjectType] = [] for sub_selection in selection.selection_set.selections: if isinstance(sub_selection, FieldNode): common_fields.append( self._get_field(sub_selection, class_name, parent_type) ) if isinstance(sub_selection, InlineFragmentNode): fragments.append(sub_selection) all_common_fields_typename = all(f.name == "__typename" for f in common_fields) for fragment in fragments: type_condition_name = fragment.type_condition.name.value fragment_class_name = class_name + type_condition_name current_type = GraphQLObjectType( fragment_class_name, list(common_fields), graphql_typename=type_condition_name, ) fields: list[GraphQLFragmentSpread | GraphQLField] = [] for sub_selection in fragment.selection_set.selections: if isinstance(sub_selection, FragmentSpreadNode): fields.append(GraphQLFragmentSpread(sub_selection.name.value)) continue assert isinstance(sub_selection, FieldNode) parent_type = cast( "StrawberryObjectDefinition", self.schema.get_type_by_name(type_condition_name), ) assert parent_type, type_condition_name fields.append( self._get_field( selection=sub_selection, class_name=fragment_class_name, parent_type=parent_type, ) ) if any(isinstance(f, GraphQLFragmentSpread) for f in fields): if len(fields) > 1: raise ValueError( "Queries with Fragments cannot include separate fields." ) spread_field = fields[0] assert isinstance(spread_field, GraphQLFragmentSpread) sub_type = next( t for t in self.types if isinstance(t, GraphQLObjectType) and t.name == spread_field.name ) fields = [*sub_type.fields] if all_common_fields_typename: # No need to create a new type. sub_types.append(sub_type) continue # This cast is safe because all the fields are either # `GraphQLField` or `GraphQLFragmentSpread` # and the suite above will cause this statement to be # skipped if there are any `GraphQLFragmentSpread`. current_type.fields.extend(cast("list[GraphQLField]", fields)) sub_types.append(current_type) for sub_type in sub_types: self._collect_type(sub_type) return sub_types def _collect_scalar( self, scalar_definition: ScalarDefinition, python_type: type | None ) -> GraphQLScalar: graphql_scalar = GraphQLScalar(scalar_definition.name, python_type=python_type) self._collect_type(graphql_scalar) return graphql_scalar def _collect_enum(self, enum: StrawberryEnumDefinition) -> GraphQLEnum: graphql_enum = GraphQLEnum( enum.name, [value.name for value in enum.values], python_type=enum.wrapped_cls, ) self._collect_type(graphql_enum) return graphql_enum __all__ = [ "CodegenFile", "CodegenResult", "ConsolePlugin", "QueryCodegen", "QueryCodegenPlugin", ] strawberry-graphql-0.287.0/strawberry/codegen/types.py000066400000000000000000000101131511033167500230540ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass, field from typing import TYPE_CHECKING, TypeAlias if TYPE_CHECKING: from collections.abc import Mapping from enum import EnumMeta from typing import Literal from strawberry.types.unset import UnsetType @dataclass class GraphQLOptional: of_type: GraphQLType @dataclass class GraphQLList: of_type: GraphQLType @dataclass class GraphQLUnion: name: str types: list[GraphQLObjectType] @dataclass class GraphQLField: name: str alias: str | None type: GraphQLType default_value: GraphQLArgumentValue | None = None @dataclass class GraphQLFragmentSpread: name: str @dataclass class GraphQLObjectType: name: str fields: list[GraphQLField] = field(default_factory=list) graphql_typename: str | None = None # Subtype of GraphQLObjectType. # Because dataclass inheritance is a little odd, the fields are # repeated here. @dataclass class GraphQLFragmentType(GraphQLObjectType): name: str fields: list[GraphQLField] = field(default_factory=list) graphql_typename: str | None = None on: str = "" def __post_init__(self) -> None: if not self.on: raise ValueError( "GraphQLFragmentType must be constructed with a valid 'on'" ) @dataclass class GraphQLEnum: name: str values: list[str] python_type: EnumMeta @dataclass class GraphQLScalar: name: str python_type: type | None GraphQLType: TypeAlias = ( GraphQLObjectType | GraphQLEnum | GraphQLScalar | GraphQLOptional | GraphQLList | GraphQLUnion ) @dataclass class GraphQLFieldSelection: field: str alias: str | None selections: list[GraphQLSelection] directives: list[GraphQLDirective] arguments: list[GraphQLArgument] @dataclass class GraphQLInlineFragment: type_condition: str selections: list[GraphQLSelection] GraphQLSelection: TypeAlias = ( GraphQLFieldSelection | GraphQLInlineFragment | GraphQLFragmentSpread ) @dataclass class GraphQLStringValue: value: str @dataclass class GraphQLIntValue: value: int @dataclass class GraphQLFloatValue: value: float @dataclass class GraphQLEnumValue: name: str enum_type: str | None = None @dataclass class GraphQLBoolValue: value: bool @dataclass class GraphQLNullValue: """A class that represents a GraphQLNull value.""" value: None | UnsetType = None @dataclass class GraphQLListValue: values: list[GraphQLArgumentValue] @dataclass class GraphQLObjectValue: values: Mapping[str, GraphQLArgumentValue] @dataclass class GraphQLVariableReference: value: str GraphQLArgumentValue: TypeAlias = ( GraphQLStringValue | GraphQLNullValue | GraphQLIntValue | GraphQLVariableReference | GraphQLFloatValue | GraphQLListValue | GraphQLEnumValue | GraphQLBoolValue | GraphQLObjectValue ) @dataclass class GraphQLArgument: name: str value: GraphQLArgumentValue @dataclass class GraphQLDirective: name: str arguments: list[GraphQLArgument] @dataclass class GraphQLVariable: name: str type: GraphQLType @dataclass class GraphQLOperation: name: str kind: Literal["query", "mutation", "subscription"] selections: list[GraphQLSelection] directives: list[GraphQLDirective] variables: list[GraphQLVariable] type: GraphQLObjectType variables_type: GraphQLObjectType | None __all__ = [ "GraphQLArgument", "GraphQLArgumentValue", "GraphQLBoolValue", "GraphQLDirective", "GraphQLEnum", "GraphQLEnumValue", "GraphQLField", "GraphQLFieldSelection", "GraphQLFloatValue", "GraphQLFragmentSpread", "GraphQLFragmentType", "GraphQLInlineFragment", "GraphQLIntValue", "GraphQLList", "GraphQLListValue", "GraphQLNullValue", "GraphQLObjectType", "GraphQLObjectValue", "GraphQLOperation", "GraphQLOptional", "GraphQLScalar", "GraphQLSelection", "GraphQLStringValue", "GraphQLType", "GraphQLUnion", "GraphQLVariable", "GraphQLVariableReference", ] strawberry-graphql-0.287.0/strawberry/codemods/000077500000000000000000000000001511033167500215335ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/codemods/__init__.py000066400000000000000000000004141511033167500236430ustar00rootroot00000000000000from .annotated_unions import ConvertUnionToAnnotatedUnion from .maybe_optional import ConvertMaybeToOptional from .update_imports import UpdateImportsCodemod __all__ = [ "ConvertMaybeToOptional", "ConvertUnionToAnnotatedUnion", "UpdateImportsCodemod", ] strawberry-graphql-0.287.0/strawberry/codemods/annotated_unions.py000066400000000000000000000135271511033167500254650ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING import libcst as cst import libcst.matchers as m from libcst._nodes.expression import BaseExpression, Call # noqa: TC002 from libcst.codemod import CodemodContext, VisitorBasedCodemodCommand from libcst.codemod.visitors import AddImportsVisitor, RemoveImportsVisitor if TYPE_CHECKING: from collections.abc import Sequence def _find_named_argument(args: Sequence[cst.Arg], name: str) -> cst.Arg | None: return next( (arg for arg in args if arg.keyword and arg.keyword.value == name), None, ) def _find_positional_argument( args: Sequence[cst.Arg], search_index: int ) -> cst.Arg | None: for index, arg in enumerate(args): if index > search_index: return None if index == search_index and arg.keyword is None: return arg return None class ConvertUnionToAnnotatedUnion(VisitorBasedCodemodCommand): DESCRIPTION: str = ( "Converts strawberry.union(..., types=(...)) to " "Annotated[Union[...], strawberry.union(...)]" ) def __init__( self, context: CodemodContext, use_pipe_syntax: bool = True, use_typing_extensions: bool = False, ) -> None: self._is_using_named_import = False self.use_pipe_syntax = use_pipe_syntax self.use_typing_extensions = use_typing_extensions super().__init__(context) def visit_Module(self, node: cst.Module) -> bool | None: # noqa: N802 self._is_using_named_import = False return super().visit_Module(node) @m.visit( m.ImportFrom( m.Name("strawberry"), [ m.ZeroOrMore(), m.ImportAlias(m.Name("union")), m.ZeroOrMore(), ], ) ) def visit_import_from(self, original_node: cst.ImportFrom) -> None: self._is_using_named_import = True @m.leave( m.Call( func=m.Attribute(value=m.Name("strawberry"), attr=m.Name("union")) | m.Name("union") ) ) def leave_union_call( self, original_node: Call, updated_node: Call ) -> BaseExpression: if not self._is_using_named_import and isinstance(original_node.func, cst.Name): return original_node types = _find_named_argument(original_node.args, "types") union_name = _find_named_argument(original_node.args, "name") if types is None: types = _find_positional_argument(original_node.args, 1) # this is probably a strawberry.union(name="...") so we skip the conversion # as it is going to be used in the new way already 😊 if types is None: return original_node AddImportsVisitor.add_needed_import( self.context, "typing_extensions" if self.use_typing_extensions else "typing", "Annotated", ) RemoveImportsVisitor.remove_unused_import(self.context, "strawberry", "union") if union_name is None: union_name = _find_positional_argument(original_node.args, 0) assert union_name assert isinstance(types.value, (cst.Tuple, cst.List)) types = types.value.elements # type: ignore union_name = union_name.value # type: ignore description = _find_named_argument(original_node.args, "description") directives = _find_named_argument(original_node.args, "directives") if self.use_pipe_syntax: union_node = self._create_union_node_with_pipe_syntax(types) # type: ignore else: AddImportsVisitor.add_needed_import(self.context, "typing", "Union") union_node = cst.Subscript( value=cst.Name(value="Union"), slice=[ cst.SubscriptElement(slice=cst.Index(value=t.value)) for t in types # type: ignore ], ) union_call_args = [ cst.Arg( value=union_name, # type: ignore keyword=cst.Name(value="name"), equal=cst.AssignEqual( whitespace_before=cst.SimpleWhitespace(""), whitespace_after=cst.SimpleWhitespace(""), ), ) ] additional_args = {"description": description, "directives": directives} union_call_args.extend( cst.Arg( value=arg.value, keyword=cst.Name(name), equal=cst.AssignEqual( whitespace_before=cst.SimpleWhitespace(""), whitespace_after=cst.SimpleWhitespace(""), ), ) for name, arg in additional_args.items() if arg is not None ) union_call_node = cst.Call( func=cst.Attribute( value=cst.Name(value="strawberry"), attr=cst.Name(value="union"), ), args=union_call_args, ) return cst.Subscript( value=cst.Name(value="Annotated"), slice=[ cst.SubscriptElement( slice=cst.Index( value=union_node, ), ), cst.SubscriptElement( slice=cst.Index( value=union_call_node, ), ), ], ) @classmethod def _create_union_node_with_pipe_syntax( cls, types: Sequence[cst.BaseElement] ) -> cst.BaseExpression: type_names = [t.value for t in types] if not all(isinstance(t, cst.Name) for t in type_names): raise ValueError("Only names are supported for now") expression = " | ".join(name.value for name in type_names) # type: ignore return cst.parse_expression(expression) strawberry-graphql-0.287.0/strawberry/codemods/maybe_optional.py000066400000000000000000000104611511033167500251110ustar00rootroot00000000000000from __future__ import annotations import libcst as cst import libcst.matchers as m from libcst._nodes.expression import BaseExpression # noqa: TC002 from libcst.codemod import CodemodContext, VisitorBasedCodemodCommand from libcst.codemod.visitors import AddImportsVisitor class ConvertMaybeToOptional(VisitorBasedCodemodCommand): DESCRIPTION: str = ( "Converts strawberry.Maybe[T] to strawberry.Maybe[T | None] " "to match the new Maybe type definition" ) def __init__( self, context: CodemodContext, use_pipe_syntax: bool = True, # Default to pipe syntax for modern Python ) -> None: self.use_pipe_syntax = use_pipe_syntax super().__init__(context) @m.leave( m.Subscript( value=m.Attribute(value=m.Name("strawberry"), attr=m.Name("Maybe")) | m.Name("Maybe") ) ) def leave_maybe_subscript( self, original_node: cst.Subscript, updated_node: cst.Subscript ) -> BaseExpression: # Skip if it's not a strawberry.Maybe or imported Maybe if isinstance(original_node.value, cst.Name): # Check if this is an imported Maybe from strawberry # For now, we'll assume any standalone "Maybe" is from strawberry # In a more robust implementation, we'd track imports pass # Get the inner type if isinstance(original_node.slice, (list, tuple)): if len(original_node.slice) != 1: return original_node slice_element = original_node.slice[0] else: slice_element = original_node.slice if not isinstance(slice_element, cst.SubscriptElement): return original_node if not isinstance(slice_element.slice, cst.Index): return original_node inner_type = slice_element.slice.value # Check if the inner type already includes None if self._already_includes_none(inner_type): return original_node # Create the new union type with None new_type: BaseExpression if self.use_pipe_syntax: new_type = cst.BinaryOperation( left=inner_type, operator=cst.BitOr( whitespace_before=cst.SimpleWhitespace(" "), whitespace_after=cst.SimpleWhitespace(" "), ), right=cst.Name("None"), ) else: # Use Union[T, None] syntax AddImportsVisitor.add_needed_import(self.context, "typing", "Union") new_type = cst.Subscript( value=cst.Name("Union"), slice=[ cst.SubscriptElement(slice=cst.Index(value=inner_type)), cst.SubscriptElement(slice=cst.Index(value=cst.Name("None"))), ], ) # Return the updated Maybe[T | None] return updated_node.with_changes( slice=[cst.SubscriptElement(slice=cst.Index(value=new_type))] ) def _already_includes_none(self, node: BaseExpression) -> bool: """Check if the type already includes None (e.g., T | None or Union[T, None]).""" # Check for T | None pattern if isinstance(node, cst.BinaryOperation) and isinstance( node.operator, cst.BitOr ): if isinstance(node.right, cst.Name) and node.right.value == "None": return True # Recursively check left side for chained unions if self._already_includes_none(node.left): return True # Check for Union[..., None] pattern if ( isinstance(node, cst.Subscript) and isinstance(node.value, cst.Name) and node.value.value == "Union" ): # Handle both list and tuple slice formats slice_elements = ( node.slice if isinstance(node.slice, (list, tuple)) else [node.slice] ) for element in slice_elements: if ( isinstance(element, cst.SubscriptElement) and isinstance(element.slice, cst.Index) and isinstance(element.slice.value, cst.Name) and element.slice.value.value == "None" ): return True return False strawberry-graphql-0.287.0/strawberry/codemods/update_imports.py000066400000000000000000000110161511033167500251430ustar00rootroot00000000000000from __future__ import annotations import libcst as cst import libcst.matchers as m from libcst.codemod import CodemodContext, VisitorBasedCodemodCommand from libcst.codemod.visitors import AddImportsVisitor, RemoveImportsVisitor class UpdateImportsCodemod(VisitorBasedCodemodCommand): def __init__(self, context: CodemodContext) -> None: super().__init__(context) self.add_imports_visitor = AddImportsVisitor(context) self.remove_imports_visitor = RemoveImportsVisitor(context) def _update_imports( self, node: cst.ImportFrom, updated_node: cst.ImportFrom ) -> cst.ImportFrom: imports = [ "field", "union", "auto", "unset", "arguments", "lazy_type", "object_type", "private", "enum", ] for import_name in imports: if m.matches( node, m.ImportFrom( module=m.Attribute( value=m.Name("strawberry"), attr=m.Name(import_name) ) ), ): updated_node = updated_node.with_changes( module=cst.Attribute( value=cst.Attribute( value=cst.Name("strawberry"), attr=cst.Name("types") ), attr=cst.Name(import_name), ), ) return updated_node def _update_types_types_imports( self, node: cst.ImportFrom, updated_node: cst.ImportFrom ) -> cst.ImportFrom: if m.matches( node, m.ImportFrom( module=m.Attribute( value=m.Attribute(value=m.Name("strawberry"), attr=m.Name("types")), attr=m.Name("types"), ) ), ): updated_node = updated_node.with_changes( module=cst.Attribute( value=cst.Attribute( value=cst.Name("strawberry"), attr=cst.Name("types") ), attr=cst.Name("base"), ), ) return updated_node def _update_strawberry_type_imports( self, node: cst.ImportFrom, updated_node: cst.ImportFrom ) -> cst.ImportFrom: if m.matches( node, m.ImportFrom( module=m.Attribute(value=m.Name("strawberry"), attr=m.Name("type")) ), ): has_get_object_definition = ( any( m.matches(name, m.ImportAlias(name=m.Name("get_object_definition"))) for name in node.names ) if not isinstance(node.names, cst.ImportStar) else False ) has_has_object_definition = ( any( m.matches(name, m.ImportAlias(name=m.Name("has_object_definition"))) for name in node.names ) if not isinstance(node.names, cst.ImportStar) else False ) updated_node = updated_node.with_changes( module=cst.Attribute( value=cst.Attribute( value=cst.Name("strawberry"), attr=cst.Name("types") ), attr=cst.Name("base"), ), ) self.remove_imports_visitor.remove_unused_import( self.context, "strawberry.types.base", "get_object_definition" ) self.remove_imports_visitor.remove_unused_import( self.context, "strawberry.types.base", "has_object_definition" ) if has_get_object_definition: self.add_imports_visitor.add_needed_import( self.context, "strawberry.types", "get_object_definition" ) if has_has_object_definition: self.add_imports_visitor.add_needed_import( self.context, "strawberry.types", "has_object_definition" ) return updated_node def leave_ImportFrom( # noqa: N802 self, node: cst.ImportFrom, updated_node: cst.ImportFrom ) -> cst.ImportFrom: updated_node = self._update_imports(updated_node, updated_node) updated_node = self._update_types_types_imports(updated_node, updated_node) return self._update_strawberry_type_imports(updated_node, updated_node) strawberry-graphql-0.287.0/strawberry/dataloader.py000066400000000000000000000172421511033167500224160ustar00rootroot00000000000000from __future__ import annotations import dataclasses from abc import ABC, abstractmethod from asyncio import create_task, gather, get_event_loop from asyncio.futures import Future from dataclasses import dataclass from typing import ( TYPE_CHECKING, Any, Generic, TypeVar, overload, ) from .exceptions import WrongNumberOfResultsReturned if TYPE_CHECKING: from asyncio.events import AbstractEventLoop from collections.abc import ( Awaitable, Callable, Hashable, Iterable, Mapping, Sequence, ) T = TypeVar("T") K = TypeVar("K") @dataclass class LoaderTask(Generic[K, T]): key: K future: Future @dataclass class Batch(Generic[K, T]): tasks: list[LoaderTask] = dataclasses.field(default_factory=list) dispatched: bool = False def add_task(self, key: Any, future: Future) -> None: task = LoaderTask[K, T](key, future) self.tasks.append(task) def __len__(self) -> int: return len(self.tasks) class AbstractCache(ABC, Generic[K, T]): @abstractmethod def get(self, key: K) -> Future[T] | None: pass @abstractmethod def set(self, key: K, value: Future[T]) -> None: pass @abstractmethod def delete(self, key: K) -> None: pass @abstractmethod def clear(self) -> None: pass class DefaultCache(AbstractCache[K, T]): def __init__(self, cache_key_fn: Callable[[K], Hashable] | None = None) -> None: self.cache_key_fn: Callable[[K], Hashable] = ( cache_key_fn if cache_key_fn is not None else lambda x: x ) self.cache_map: dict[Hashable, Future[T]] = {} def get(self, key: K) -> Future[T] | None: return self.cache_map.get(self.cache_key_fn(key)) def set(self, key: K, value: Future[T]) -> None: self.cache_map[self.cache_key_fn(key)] = value def delete(self, key: K) -> None: del self.cache_map[self.cache_key_fn(key)] def clear(self) -> None: self.cache_map.clear() class DataLoader(Generic[K, T]): batch: Batch[K, T] | None = None cache: bool = False cache_map: AbstractCache[K, T] @overload def __init__( self, # any BaseException is rethrown in 'load', so should be excluded from the T type load_fn: Callable[[list[K]], Awaitable[Sequence[T | BaseException]]], max_batch_size: int | None = None, cache: bool = True, loop: AbstractEventLoop | None = None, cache_map: AbstractCache[K, T] | None = None, cache_key_fn: Callable[[K], Hashable] | None = None, ) -> None: ... # fallback if load_fn is untyped and there's no other info for inference @overload def __init__( self: DataLoader[K, Any], load_fn: Callable[[list[K]], Awaitable[list[Any]]], max_batch_size: int | None = None, cache: bool = True, loop: AbstractEventLoop | None = None, cache_map: AbstractCache[K, T] | None = None, cache_key_fn: Callable[[K], Hashable] | None = None, ) -> None: ... def __init__( self, load_fn: Callable[[list[K]], Awaitable[Sequence[T | BaseException]]], max_batch_size: int | None = None, cache: bool = True, loop: AbstractEventLoop | None = None, cache_map: AbstractCache[K, T] | None = None, cache_key_fn: Callable[[K], Hashable] | None = None, ): self.load_fn = load_fn self.max_batch_size = max_batch_size self._loop = loop self.cache = cache if self.cache: self.cache_map = ( DefaultCache(cache_key_fn) if cache_map is None else cache_map ) @property def loop(self) -> AbstractEventLoop: if self._loop is None: self._loop = get_event_loop() return self._loop def load(self, key: K) -> Awaitable[T]: if self.cache: future = self.cache_map.get(key) if future and not future.cancelled(): return future future = self.loop.create_future() if self.cache: self.cache_map.set(key, future) batch = get_current_batch(self) batch.add_task(key, future) return future def load_many(self, keys: Iterable[K]) -> Awaitable[list[T]]: return gather(*map(self.load, keys)) def clear(self, key: K) -> None: if self.cache: self.cache_map.delete(key) def clear_many(self, keys: Iterable[K]) -> None: if self.cache: for key in keys: self.cache_map.delete(key) def clear_all(self) -> None: if self.cache: self.cache_map.clear() def prime(self, key: K, value: T, force: bool = False) -> None: self.prime_many({key: value}, force) def prime_many(self, data: Mapping[K, T], force: bool = False) -> None: # Populate the cache with the specified values if self.cache: for key, value in data.items(): if not self.cache_map.get(key) or force: future: Future = Future(loop=self.loop) future.set_result(value) self.cache_map.set(key, future) # For keys that are pending on the current batch, but the # batch hasn't started fetching yet: Remove it from the # batch and set to the specified value if self.batch is not None and not self.batch.dispatched: batch_updated = False for task in self.batch.tasks: if task.key in data: batch_updated = True task.future.set_result(data[task.key]) if batch_updated: self.batch.tasks = [ task for task in self.batch.tasks if not task.future.done() ] def should_create_new_batch(loader: DataLoader, batch: Batch) -> bool: return bool( batch.dispatched or (loader.max_batch_size and len(batch) >= loader.max_batch_size) ) def get_current_batch(loader: DataLoader) -> Batch: if loader.batch and not should_create_new_batch(loader, loader.batch): return loader.batch loader.batch = Batch() dispatch(loader, loader.batch) return loader.batch def dispatch(loader: DataLoader, batch: Batch) -> None: loader.loop.call_soon(create_task, dispatch_batch(loader, batch)) async def dispatch_batch(loader: DataLoader, batch: Batch) -> None: batch.dispatched = True keys = [task.key for task in batch.tasks] if len(keys) == 0: # Ensure batch is not empty # Unlikely, but could happen if the tasks are # overriden with preset values return # TODO: check if load_fn return an awaitable and it is a list try: values = await loader.load_fn(keys) values = list(values) if len(values) != len(batch): raise WrongNumberOfResultsReturned( # noqa: TRY301 expected=len(batch), received=len(values) ) for task, value in zip(batch.tasks, values, strict=True): # Trying to set_result in a cancelled future would raise # asyncio.exceptions.InvalidStateError if task.future.cancelled(): continue if isinstance(value, BaseException): task.future.set_exception(value) else: task.future.set_result(value) except Exception as e: # noqa: BLE001 for task in batch.tasks: task.future.set_exception(e) __all__ = [ "AbstractCache", "Batch", "DataLoader", "DefaultCache", "LoaderTask", "dispatch", "dispatch_batch", "get_current_batch", "should_create_new_batch", ] strawberry-graphql-0.287.0/strawberry/directive.py000066400000000000000000000067761511033167500223060ustar00rootroot00000000000000from __future__ import annotations import dataclasses from functools import cached_property from typing import ( TYPE_CHECKING, Annotated, Any, Generic, TypeVar, ) from graphql import DirectiveLocation from strawberry.types.field import StrawberryField from strawberry.types.fields.resolver import ( INFO_PARAMSPEC, ReservedType, StrawberryResolver, ) from strawberry.types.unset import UNSET if TYPE_CHECKING: import inspect from collections.abc import Callable from strawberry.types.arguments import StrawberryArgument # TODO: should this be directive argument? def directive_field( name: str, default: object = UNSET, ) -> Any: """Function to add metadata to a directive argument, like the GraphQL name. Args: name: The GraphQL name of the directive argument default: The default value of the argument Returns: A StrawberryField object that can be used to customise a directive argument Example: ```python import strawberry from strawberry.schema_directive import Location @strawberry.schema_directive(locations=[Location.FIELD_DEFINITION]) class Sensitive: reason: str = strawberry.directive_field(name="as") ``` """ return StrawberryField( python_name=None, graphql_name=name, default=default, ) T = TypeVar("T") class StrawberryDirectiveValue: ... DirectiveValue = Annotated[T, StrawberryDirectiveValue()] DirectiveValue.__doc__ = ( """Represents the ``value`` argument for a GraphQL query directive.""" ) # Registers `DirectiveValue[...]` annotated arguments as reserved VALUE_PARAMSPEC = ReservedType(name="value", type=StrawberryDirectiveValue) class StrawberryDirectiveResolver(StrawberryResolver[T]): RESERVED_PARAMSPEC = ( INFO_PARAMSPEC, VALUE_PARAMSPEC, ) @cached_property def value_parameter(self) -> inspect.Parameter | None: return self.reserved_parameters.get(VALUE_PARAMSPEC) @dataclasses.dataclass class StrawberryDirective(Generic[T]): python_name: str graphql_name: str | None resolver: StrawberryDirectiveResolver[T] locations: list[DirectiveLocation] description: str | None = None @cached_property def arguments(self) -> list[StrawberryArgument]: return self.resolver.arguments def directive( *, locations: list[DirectiveLocation], description: str | None = None, name: str | None = None, ) -> Callable[[Callable[..., T]], StrawberryDirective[T]]: """Decorator to create a GraphQL operation directive. Args: locations: The locations where the directive can be used description: The GraphQL description of the directive name: The GraphQL name of the directive Returns: A StrawberryDirective object that can be used to customise a directive Example: ```python import strawberry from strawberry.directive import DirectiveLocation @strawberry.directive( locations=[DirectiveLocation.FIELD], description="Make string uppercase" ) def turn_uppercase(value: str): return value.upper() ``` """ def _wrap(f: Callable[..., T]) -> StrawberryDirective[T]: return StrawberryDirective( python_name=f.__name__, graphql_name=name, locations=locations, description=description, resolver=StrawberryDirectiveResolver(f), ) return _wrap __all__ = ["DirectiveLocation", "StrawberryDirective", "directive"] strawberry-graphql-0.287.0/strawberry/django/000077500000000000000000000000001511033167500212005ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/django/__init__.py000066400000000000000000000014531511033167500233140ustar00rootroot00000000000000from typing import Any try: # import modules and objects from external strawberry-graphql-django # package so that it can be used through strawberry.django namespace from strawberry_django import * # noqa: F403 except ModuleNotFoundError: import importlib def __getattr__(name: str) -> Any: # try to import submodule and raise exception only if it does not exist import_symbol = f"{__name__}.{name}" try: return importlib.import_module(import_symbol) except ModuleNotFoundError as e: raise AttributeError( f"Attempted import of {import_symbol} failed. Make sure to install the" "'strawberry-graphql-django' package to use the Strawberry Django " "extension API." ) from e strawberry-graphql-0.287.0/strawberry/django/apps.py000066400000000000000000000002071511033167500225140ustar00rootroot00000000000000from django.apps import AppConfig # pragma: no cover class StrawberryConfig(AppConfig): # pragma: no cover name = "strawberry" strawberry-graphql-0.287.0/strawberry/django/context.py000066400000000000000000000012171511033167500232370ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from django.http import HttpRequest, HttpResponse @dataclass class StrawberryDjangoContext: request: HttpRequest response: HttpResponse def __getitem__(self, key: str) -> Any: # __getitem__ override needed to avoid issues for who's # using info.context["request"] return super().__getattribute__(key) def get(self, key: str) -> Any: """Enable .get notation for accessing the request.""" return super().__getattribute__(key) __all__ = ["StrawberryDjangoContext"] strawberry-graphql-0.287.0/strawberry/django/test/000077500000000000000000000000001511033167500221575ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/django/test/__init__.py000066400000000000000000000001071511033167500242660ustar00rootroot00000000000000from .client import GraphQLTestClient __all__ = ["GraphQLTestClient"] strawberry-graphql-0.287.0/strawberry/django/test/client.py000066400000000000000000000011341511033167500240060ustar00rootroot00000000000000from typing import Any from strawberry.test import BaseGraphQLTestClient class GraphQLTestClient(BaseGraphQLTestClient): def request( self, body: dict[str, object], headers: dict[str, object] | None = None, files: dict[str, object] | None = None, ) -> Any: if files: return self._client.post( self.url, data=body, format="multipart", headers=headers ) return self._client.post( self.url, data=body, content_type="application/json", headers=headers ) __all__ = ["GraphQLTestClient"] strawberry-graphql-0.287.0/strawberry/django/views.py000066400000000000000000000173131511033167500227140ustar00rootroot00000000000000from __future__ import annotations import json import warnings from typing import ( TYPE_CHECKING, Any, TypeGuard, ) from asgiref.sync import markcoroutinefunction from django.core.serializers.json import DjangoJSONEncoder from django.http import ( HttpRequest, HttpResponse, HttpResponseNotAllowed, JsonResponse, StreamingHttpResponse, ) from django.http.response import HttpResponseBase from django.template.exceptions import TemplateDoesNotExist from django.template.loader import render_to_string from django.utils.decorators import classonlymethod from django.views.generic import View from lia import AsyncDjangoHTTPRequestAdapter, DjangoHTTPRequestAdapter, HTTPException from strawberry.http.async_base_view import AsyncBaseHTTPView from strawberry.http.sync_base_view import SyncBaseHTTPView from strawberry.http.typevars import ( Context, RootValue, ) from .context import StrawberryDjangoContext if TYPE_CHECKING: from collections.abc import AsyncIterator, Callable from django.template.response import TemplateResponse from strawberry.http import GraphQLHTTPResponse from strawberry.http.ides import GraphQL_IDE from strawberry.schema import BaseSchema # TODO: remove this and unify temporal responses class TemporalHttpResponse(JsonResponse): status_code: int | None = None # pyright: ignore def __init__(self) -> None: super().__init__({}) def __repr__(self) -> str: """Adopted from Django to handle `status_code=None`.""" if self.status_code is not None: return super().__repr__() return "<{cls} status_code={status_code}{content_type}>".format( # noqa: UP032 cls=self.__class__.__name__, status_code=self.status_code, content_type=self._content_type_for_repr, # pyright: ignore ) class BaseView: graphql_ide_html: str def __init__( self, schema: BaseSchema, graphiql: str | None = None, graphql_ide: GraphQL_IDE | None = "graphiql", allow_queries_via_get: bool = True, multipart_uploads_enabled: bool = False, **kwargs: Any, ) -> None: self.schema = schema self.allow_queries_via_get = allow_queries_via_get self.multipart_uploads_enabled = multipart_uploads_enabled if graphiql is not None: warnings.warn( "The `graphiql` argument is deprecated in favor of `graphql_ide`", DeprecationWarning, stacklevel=2, ) self.graphql_ide = "graphiql" if graphiql else None else: self.graphql_ide = graphql_ide super().__init__(**kwargs) def create_response( self, response_data: GraphQLHTTPResponse | list[GraphQLHTTPResponse], sub_response: HttpResponse, ) -> HttpResponseBase: data = self.encode_json(response_data) response = HttpResponse( data, content_type="application/json", ) for name, value in sub_response.items(): response[name] = value if sub_response.status_code: response.status_code = sub_response.status_code for name, value in sub_response.cookies.items(): response.cookies[name] = value return response async def create_streaming_response( self, request: HttpRequest, stream: Callable[[], AsyncIterator[Any]], sub_response: TemporalHttpResponse, headers: dict[str, str], ) -> HttpResponseBase: return StreamingHttpResponse( streaming_content=stream(), status=sub_response.status_code, headers={ **sub_response.headers, **headers, }, ) def encode_json(self, data: object) -> str | bytes: return json.dumps(data, cls=DjangoJSONEncoder) class GraphQLView( BaseView, SyncBaseHTTPView[ HttpRequest, HttpResponseBase, TemporalHttpResponse, Context, RootValue ], View, ): graphiql: bool | None = None graphql_ide: GraphQL_IDE | None = "graphiql" allow_queries_via_get = True schema: BaseSchema = None # type: ignore request_adapter_class = DjangoHTTPRequestAdapter def get_root_value(self, request: HttpRequest) -> RootValue | None: return None def get_context(self, request: HttpRequest, response: HttpResponse) -> Context: return StrawberryDjangoContext(request=request, response=response) # type: ignore def get_sub_response(self, request: HttpRequest) -> TemporalHttpResponse: return TemporalHttpResponse() def dispatch( self, request: HttpRequest, *args: Any, **kwargs: Any ) -> HttpResponseNotAllowed | TemplateResponse | HttpResponseBase: try: return self.run(request=request) except HTTPException as e: return HttpResponse( content=e.reason, status=e.status_code, content_type="text/plain", ) def render_graphql_ide(self, request: HttpRequest) -> HttpResponse: try: content = render_to_string("graphql/graphiql.html", request=request) except TemplateDoesNotExist: content = self.graphql_ide_html return HttpResponse(content) class AsyncGraphQLView( BaseView, AsyncBaseHTTPView[ HttpRequest, HttpResponseBase, TemporalHttpResponse, HttpRequest, TemporalHttpResponse, Context, RootValue, ], View, ): graphiql: bool | None = None graphql_ide: GraphQL_IDE | None = "graphiql" allow_queries_via_get = True schema: BaseSchema = None # type: ignore request_adapter_class = AsyncDjangoHTTPRequestAdapter @classonlymethod # pyright: ignore[reportIncompatibleMethodOverride] def as_view(cls, **initkwargs: Any) -> Callable[..., HttpResponse]: # noqa: N805 # This code tells django that this view is async, see docs here: # https://docs.djangoproject.com/en/3.1/topics/async/#async-views view = super().as_view(**initkwargs) markcoroutinefunction(view) return view async def get_root_value(self, request: HttpRequest) -> RootValue | None: return None async def get_context( self, request: HttpRequest, response: HttpResponse ) -> Context: return StrawberryDjangoContext(request=request, response=response) # type: ignore async def get_sub_response(self, request: HttpRequest) -> TemporalHttpResponse: return TemporalHttpResponse() async def dispatch( # pyright: ignore self, request: HttpRequest, *args: Any, **kwargs: Any ) -> HttpResponseNotAllowed | TemplateResponse | HttpResponseBase: try: return await self.run(request=request) except HTTPException as e: return HttpResponse( content=e.reason, status=e.status_code, content_type="text/plain", ) async def render_graphql_ide(self, request: HttpRequest) -> HttpResponse: try: content = render_to_string("graphql/graphiql.html", request=request) except TemplateDoesNotExist: content = self.graphql_ide_html return HttpResponse(content=content) def is_websocket_request(self, request: HttpRequest) -> TypeGuard[HttpRequest]: return False async def pick_websocket_subprotocol(self, request: HttpRequest) -> str | None: raise NotImplementedError async def create_websocket_response( self, request: HttpRequest, subprotocol: str | None ) -> TemporalHttpResponse: raise NotImplementedError __all__ = ["AsyncGraphQLView", "GraphQLView"] strawberry-graphql-0.287.0/strawberry/exceptions/000077500000000000000000000000001511033167500221175ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/exceptions/__init__.py000066400000000000000000000147041511033167500242360ustar00rootroot00000000000000from __future__ import annotations from functools import cached_property from typing import TYPE_CHECKING from graphql import GraphQLError from .conflicting_arguments import ConflictingArgumentsError from .duplicated_type_name import DuplicatedTypeName from .exception import StrawberryException, UnableToFindExceptionSource from .handler import setup_exception_handler from .invalid_argument_type import InvalidArgumentTypeError from .invalid_superclass_interface import InvalidSuperclassInterfaceError from .invalid_union_type import InvalidTypeForUnionMergeError, InvalidUnionTypeError from .missing_arguments_annotations import MissingArgumentsAnnotationsError from .missing_dependencies import MissingOptionalDependenciesError from .missing_field_annotation import MissingFieldAnnotationError from .missing_return_annotation import MissingReturnAnnotationError from .object_is_not_a_class import ObjectIsNotClassError from .object_is_not_an_enum import ObjectIsNotAnEnumError from .private_strawberry_field import PrivateStrawberryFieldError from .scalar_already_registered import ScalarAlreadyRegisteredError from .unresolved_field_type import UnresolvedFieldTypeError if TYPE_CHECKING: from graphql import GraphQLInputObjectType, GraphQLObjectType from strawberry.types.base import StrawberryType from .exception_source import ExceptionSource setup_exception_handler() # TODO: this doesn't seem to be tested class WrongReturnTypeForUnion(Exception): """The Union type cannot be resolved because it's not a field.""" def __init__(self, field_name: str, result_type: str) -> None: message = ( f'The type "{result_type}" cannot be resolved for the field "{field_name}" ' ", are you using a strawberry.field?" ) super().__init__(message) class UnallowedReturnTypeForUnion(Exception): """The return type is not in the list of Union types.""" def __init__( self, field_name: str, result_type: str, allowed_types: set[GraphQLObjectType] ) -> None: formatted_allowed_types = sorted(type_.name for type_ in allowed_types) message = ( f'The type "{result_type}" of the field "{field_name}" ' f'is not in the list of the types of the union: "{formatted_allowed_types}"' ) super().__init__(message) # TODO: this doesn't seem to be tested class InvalidTypeInputForUnion(Exception): def __init__(self, annotation: GraphQLInputObjectType) -> None: message = f"Union for {annotation} is not supported because it is an Input type" super().__init__(message) # TODO: this doesn't seem to be tested class MissingTypesForGenericError(Exception): """Raised when a generic types was used without passing any type.""" def __init__(self, annotation: StrawberryType | type) -> None: message = f'The type "{annotation!r}" is generic, but no type has been passed' super().__init__(message) class UnsupportedTypeError(StrawberryException): def __init__(self, annotation: StrawberryType | type) -> None: message = f"{annotation} conversion is not supported" super().__init__(message) @cached_property def exception_source(self) -> ExceptionSource | None: return None class MultipleStrawberryArgumentsError(Exception): def __init__(self, argument_name: str) -> None: message = ( f"Annotation for argument `{argument_name}` cannot have multiple " f"`strawberry.argument`s" ) super().__init__(message) class WrongNumberOfResultsReturned(Exception): def __init__(self, expected: int, received: int) -> None: message = ( "Received wrong number of results in dataloader, " f"expected: {expected}, received: {received}" ) super().__init__(message) class FieldWithResolverAndDefaultValueError(Exception): def __init__(self, field_name: str, type_name: str) -> None: message = ( f'Field "{field_name}" on type "{type_name}" cannot define a default ' "value and a resolver." ) super().__init__(message) class FieldWithResolverAndDefaultFactoryError(Exception): def __init__(self, field_name: str, type_name: str) -> None: message = ( f'Field "{field_name}" on type "{type_name}" cannot define a ' "default_factory and a resolver." ) super().__init__(message) class MissingQueryError(Exception): def __init__(self) -> None: message = 'Request data is missing a "query" value' super().__init__(message) class InvalidDefaultFactoryError(Exception): def __init__(self) -> None: message = "`default_factory` must be a callable that requires no arguments" super().__init__(message) class InvalidCustomContext(Exception): """Raised when a custom context object is of the wrong python type.""" def __init__(self) -> None: message = ( "The custom context must be either a class " "that inherits from BaseContext or a dictionary" ) super().__init__(message) class StrawberryGraphQLError(GraphQLError): """Use it when you want to override the graphql.GraphQLError in custom extensions.""" class ConnectionRejectionError(Exception): """Use it when you want to reject a WebSocket connection.""" def __init__(self, payload: dict[str, object] | None = None) -> None: if payload is None: payload = {} self.payload = payload __all__ = [ "ConflictingArgumentsError", "DuplicatedTypeName", "FieldWithResolverAndDefaultFactoryError", "FieldWithResolverAndDefaultValueError", "InvalidArgumentTypeError", "InvalidCustomContext", "InvalidDefaultFactoryError", "InvalidSuperclassInterfaceError", "InvalidTypeForUnionMergeError", "InvalidUnionTypeError", "MissingArgumentsAnnotationsError", "MissingFieldAnnotationError", "MissingOptionalDependenciesError", "MissingQueryError", "MissingReturnAnnotationError", "MissingTypesForGenericError", "MultipleStrawberryArgumentsError", "ObjectIsNotAnEnumError", "ObjectIsNotClassError", "PrivateStrawberryFieldError", "ScalarAlreadyRegisteredError", "StrawberryException", "StrawberryGraphQLError", "UnableToFindExceptionSource", "UnallowedReturnTypeForUnion", "UnresolvedFieldTypeError", "UnsupportedTypeError", "WrongNumberOfResultsReturned", "WrongReturnTypeForUnion", ] strawberry-graphql-0.287.0/strawberry/exceptions/conflicting_arguments.py000066400000000000000000000030401511033167500270520ustar00rootroot00000000000000from __future__ import annotations from functools import cached_property from typing import TYPE_CHECKING from .exception import StrawberryException from .utils.source_finder import SourceFinder if TYPE_CHECKING: from strawberry.types.fields.resolver import StrawberryResolver from .exception_source import ExceptionSource class ConflictingArgumentsError(StrawberryException): def __init__( self, resolver: StrawberryResolver, arguments: list[str], ) -> None: self.function = resolver.wrapped_func self.argument_names = arguments self.message = ( f"Arguments {self.argument_names_str} define conflicting resources. " "Only one of these arguments may be defined per resolver." ) self.rich_message = self.message self.suggestion = ( f"Only one of {self.argument_names_str} may be defined per resolver." ) self.annotation_message = self.suggestion @cached_property def argument_names_str(self) -> str: return ( ", ".join(f'"{name}"' for name in self.argument_names[:-1]) + " and " + f'"{self.argument_names[-1]}"' ) @cached_property def exception_source(self) -> ExceptionSource | None: if self.function is None: return None # pragma: no cover source_finder = SourceFinder() return source_finder.find_argument_from_object( self.function, # type: ignore self.argument_names[1], ) strawberry-graphql-0.287.0/strawberry/exceptions/duplicated_type_name.py000066400000000000000000000042111511033167500266460ustar00rootroot00000000000000from __future__ import annotations from functools import cached_property from typing import TYPE_CHECKING from .exception import StrawberryException from .utils.source_finder import SourceFinder if TYPE_CHECKING: from rich.console import RenderableType from .exception_source import ExceptionSource class DuplicatedTypeName(StrawberryException): """Raised when the same type with different definition is reused inside a schema.""" def __init__( self, first_cls: type | None, second_cls: type | None, duplicated_type_name: str, ) -> None: self.first_cls = first_cls self.second_cls = second_cls self.message = ( f"Type {duplicated_type_name} is defined multiple times in the schema" ) self.rich_message = ( f"Type `[underline]{duplicated_type_name}[/]` " "is defined multiple times in the schema" ) self.suggestion = ( "To fix this error you should either rename the type or " "remove the duplicated definition." ) super().__init__(self.message) @property def __rich_body__(self) -> RenderableType: if self.first_cls is None or self.second_cls is None: return "" from rich.console import Group source_finder = SourceFinder() first_class_source = self.exception_source assert first_class_source second_class_source = source_finder.find_class_from_object(self.second_cls) if second_class_source is None: return self._get_error_inline( first_class_source, "first class defined here" ) return Group( self._get_error_inline(first_class_source, "first class defined here"), "", self._get_error_inline(second_class_source, "second class defined here"), ) @cached_property def exception_source(self) -> ExceptionSource | None: if self.first_cls is None: return None # pragma: no cover source_finder = SourceFinder() return source_finder.find_class_from_object(self.first_cls) strawberry-graphql-0.287.0/strawberry/exceptions/exception.py000066400000000000000000000065451511033167500245010ustar00rootroot00000000000000from __future__ import annotations from abc import ABC, abstractmethod from functools import cached_property from typing import TYPE_CHECKING from strawberry.utils.str_converters import to_kebab_case if TYPE_CHECKING: from rich.console import RenderableType from .exception_source import ExceptionSource class UnableToFindExceptionSource(Exception): """Internal exception raised when we can't find the exception source.""" class StrawberryException(Exception, ABC): message: str rich_message: str suggestion: str annotation_message: str def __init__(self, message: str) -> None: self.message = message def __str__(self) -> str: return self.message @property def documentation_path(self) -> str: return to_kebab_case(self.__class__.__name__.replace("Error", "")) @property def documentation_url(self) -> str: prefix = "https://errors.strawberry.rocks/" return prefix + self.documentation_path @cached_property @abstractmethod def exception_source(self) -> ExceptionSource | None: return None @property def __rich_header__(self) -> RenderableType: return f"[bold red]error: {self.rich_message}" @property def __rich_body__(self) -> RenderableType: assert self.exception_source return self._get_error_inline(self.exception_source, self.annotation_message) @property def __rich_footer__(self) -> RenderableType: return ( f"{self.suggestion}\n\n" "Read more about this error on [bold underline]" f"[link={self.documentation_url}]{self.documentation_url}" ).strip() def __rich__(self) -> RenderableType | None: from rich.box import SIMPLE from rich.console import Group from rich.panel import Panel if self.exception_source is None: raise UnableToFindExceptionSource from self content = ( self.__rich_header__, "", self.__rich_body__, "", "", self.__rich_footer__, ) return Panel.fit( Group(*content), box=SIMPLE, ) def _get_error_inline( self, exception_source: ExceptionSource, message: str ) -> RenderableType: source_file = exception_source.path relative_path = exception_source.path_relative_to_cwd error_line = exception_source.error_line from rich.console import Group from .syntax import Syntax path = f"[white] @ [link=file://{source_file}]{relative_path}:{error_line}" prefix = " " * exception_source.error_column caret = "^" * ( exception_source.error_column_end - exception_source.error_column ) message = f"{prefix}[bold]{caret}[/] {message}" error_line = exception_source.error_line line_annotations = {error_line: message} return Group( path, "", Syntax( code=exception_source.code, highlight_lines={error_line}, line_offset=exception_source.start_line - 1, line_annotations=line_annotations, line_range=( exception_source.start_line - 1, exception_source.end_line, ), ), ) strawberry-graphql-0.287.0/strawberry/exceptions/exception_source.py000066400000000000000000000006241511033167500260510ustar00rootroot00000000000000from dataclasses import dataclass from pathlib import Path @dataclass class ExceptionSource: path: Path code: str start_line: int end_line: int error_line: int error_column: int error_column_end: int @property def path_relative_to_cwd(self) -> Path: if self.path.is_absolute(): return self.path.relative_to(Path.cwd()) return self.path strawberry-graphql-0.287.0/strawberry/exceptions/handler.py000066400000000000000000000051741511033167500241150ustar00rootroot00000000000000import os import sys import threading from collections.abc import Callable from types import TracebackType from typing import Any, cast from .exception import StrawberryException, UnableToFindExceptionSource original_threading_exception_hook = threading.excepthook ExceptionHandler = Callable[ [type[BaseException], BaseException, TracebackType | None], None ] def should_use_rich_exceptions() -> bool: errors_disabled = os.environ.get("STRAWBERRY_DISABLE_RICH_ERRORS", "") return errors_disabled.lower() not in ["true", "1", "yes"] def _get_handler(exception_type: type[BaseException]) -> ExceptionHandler: if issubclass(exception_type, StrawberryException): try: import rich except ImportError: pass else: def _handler( exception_type: type[BaseException], exception: BaseException, traceback: TracebackType | None, ) -> None: try: rich.print(exception) # we check if weren't able to find the exception source # in that case we fallback to the original exception handler except UnableToFindExceptionSource: sys.__excepthook__(exception_type, exception, traceback) return _handler return sys.__excepthook__ def strawberry_exception_handler( exception_type: type[BaseException], exception: BaseException, traceback: TracebackType | None, ) -> None: _get_handler(exception_type)(exception_type, exception, traceback) def strawberry_threading_exception_handler( args: tuple[ type[BaseException], BaseException | None, TracebackType | None, threading.Thread | None, ], ) -> None: (exception_type, exception, traceback, _) = args if exception is None: # this cast is only here because some weird issue with mypy # and the inability to disable this error based on the python version # (we'd need to do type ignore for python 3.8 and above, but mypy # doesn't seem to be able to handle that and will complain in python 3.7) cast("Any", original_threading_exception_hook)(args) return _get_handler(exception_type)(exception_type, exception, traceback) def reset_exception_handler() -> None: sys.excepthook = sys.__excepthook__ threading.excepthook = original_threading_exception_hook def setup_exception_handler() -> None: if should_use_rich_exceptions(): sys.excepthook = strawberry_exception_handler threading.excepthook = strawberry_threading_exception_handler strawberry-graphql-0.287.0/strawberry/exceptions/invalid_argument_type.py000066400000000000000000000042301511033167500270610ustar00rootroot00000000000000from __future__ import annotations from functools import cached_property from typing import TYPE_CHECKING from strawberry.types.base import get_object_definition from .exception import StrawberryException from .utils.source_finder import SourceFinder if TYPE_CHECKING: from strawberry.types.arguments import StrawberryArgument from strawberry.types.fields.resolver import StrawberryResolver from .exception_source import ExceptionSource class InvalidArgumentTypeError(StrawberryException): def __init__( self, resolver: StrawberryResolver, argument: StrawberryArgument, ) -> None: from strawberry.types.union import StrawberryUnion self.function = resolver.wrapped_func self.argument_name = argument.python_name # argument_type: Literal["union", "interface"], argument_type = "unknown" if isinstance(argument.type, StrawberryUnion): argument_type = "union" else: type_definition = get_object_definition(argument.type) if type_definition and type_definition.is_interface: argument_type = "interface" self.message = ( f'Argument "{self.argument_name}" on field ' f'"{resolver.name}" cannot be of type ' f'"{argument_type}"' ) self.rich_message = self.message if argument_type == "union": self.suggestion = "Unions are not supported as arguments in GraphQL." elif argument_type == "interface": self.suggestion = "Interfaces are not supported as arguments in GraphQL." else: self.suggestion = f"{self.argument_name} is not supported as an argument." self.annotation_message = ( f'Argument "{self.argument_name}" cannot be of type "{argument_type}"' ) @cached_property def exception_source(self) -> ExceptionSource | None: if self.function is None: return None # pragma: no cover source_finder = SourceFinder() return source_finder.find_argument_from_object( self.function, # type: ignore self.argument_name, ) strawberry-graphql-0.287.0/strawberry/exceptions/invalid_superclass_interface.py000066400000000000000000000024001511033167500303770ustar00rootroot00000000000000from __future__ import annotations from functools import cached_property from typing import TYPE_CHECKING from .exception import StrawberryException from .utils.source_finder import SourceFinder if TYPE_CHECKING: from collections.abc import Iterable from strawberry.types.base import StrawberryObjectDefinition from .exception_source import ExceptionSource class InvalidSuperclassInterfaceError(StrawberryException): def __init__( self, cls: type[type], input_name: str, interfaces: Iterable[StrawberryObjectDefinition], ) -> None: self.cls = cls pretty_interface_names = ", ".join(interface.name for interface in interfaces) self.message = self.rich_message = ( f"Input class {input_name!r} cannot inherit " f"from interface(s): {pretty_interface_names}" ) self.annotation_message = "input type class defined here" self.suggestion = "To fix this error, make sure your input type class does not inherit from any interfaces." super().__init__(self.message) @cached_property def exception_source(self) -> ExceptionSource | None: source_finder = SourceFinder() return source_finder.find_class_from_object(self.cls) strawberry-graphql-0.287.0/strawberry/exceptions/invalid_union_type.py000066400000000000000000000070071511033167500263740ustar00rootroot00000000000000from __future__ import annotations from functools import cached_property from inspect import getframeinfo, stack from pathlib import Path from typing import TYPE_CHECKING from strawberry.exceptions.utils.source_finder import SourceFinder from .exception import StrawberryException if TYPE_CHECKING: from strawberry.types.union import StrawberryUnion from .exception_source import ExceptionSource class InvalidUnionTypeError(StrawberryException): """The union is constructed with an invalid type.""" invalid_type: object def __init__( self, union_name: str, invalid_type: object, union_definition: StrawberryUnion | None = None, ) -> None: from strawberry.types.base import StrawberryList from strawberry.types.scalar import ScalarWrapper self.union_name = union_name self.invalid_type = invalid_type self.union_definition = union_definition # assuming that the exception happens two stack frames above the current one. # one is our code checking for invalid types, the other is the caller self.frame = getframeinfo(stack()[2][0]) if isinstance(invalid_type, ScalarWrapper): type_name = invalid_type.wrap.__name__ elif isinstance(invalid_type, StrawberryList): type_name = "list[...]" else: try: type_name = invalid_type.__name__ # type: ignore except AttributeError: # might be StrawberryList instance type_name = invalid_type.__class__.__name__ self.message = f"Type `{type_name}` cannot be used in a GraphQL Union" self.rich_message = ( f"Type `[underline]{type_name}[/]` cannot be used in a GraphQL Union" ) self.suggestion = ( "To fix this error you should replace the type a strawberry.type" ) self.annotation_message = "invalid type here" @cached_property def exception_source(self) -> ExceptionSource | None: source_finder = SourceFinder() if self.union_definition: source = source_finder.find_annotated_union( self.union_definition, self.invalid_type ) if source: return source if not self.frame: return None path = Path(self.frame.filename) return source_finder.find_union_call(path, self.union_name, self.invalid_type) class InvalidTypeForUnionMergeError(StrawberryException): """A specialized version of InvalidUnionTypeError for when trying to merge unions using the pipe operator.""" invalid_type: type def __init__(self, union: StrawberryUnion, other: object) -> None: self.union = union self.other = other # assuming that the exception happens two stack frames above the current one. # one is our code checking for invalid types, the other is the caller self.frame = getframeinfo(stack()[2][0]) other_name = getattr(other, "__name__", str(other)) self.message = f"`{other_name}` cannot be used when merging GraphQL Unions" self.rich_message = ( f"`[underline]{other_name}[/]` cannot be used when merging GraphQL Unions" ) self.suggestion = "" self.annotation_message = "invalid type here" @cached_property def exception_source(self) -> ExceptionSource | None: source_finder = SourceFinder() return source_finder.find_union_merge(self.union, self.other, frame=self.frame) strawberry-graphql-0.287.0/strawberry/exceptions/missing_arguments_annotations.py000066400000000000000000000037121511033167500306470ustar00rootroot00000000000000from __future__ import annotations from functools import cached_property from typing import TYPE_CHECKING from .exception import StrawberryException from .utils.source_finder import SourceFinder if TYPE_CHECKING: from strawberry.types.fields.resolver import StrawberryResolver from .exception_source import ExceptionSource class MissingArgumentsAnnotationsError(StrawberryException): """The field is missing the annotation for one or more arguments.""" def __init__( self, resolver: StrawberryResolver, arguments: list[str], ) -> None: self.missing_arguments = arguments self.function = resolver.wrapped_func self.argument_name = arguments[0] self.message = ( f"Missing annotation for {self.missing_arguments_str} " f'in field "{resolver.name}", did you forget to add it?' ) self.rich_message = ( f"Missing annotation for {self.missing_arguments_str} in " f"`[underline]{resolver.name}[/]`" ) self.suggestion = ( "To fix this error you can add an annotation to the argument " f"like so [italic]`{self.missing_arguments[0]}: str`" ) first = "first " if len(self.missing_arguments) > 1 else "" self.annotation_message = f"{first}argument missing annotation" @property def missing_arguments_str(self) -> str: arguments = self.missing_arguments if len(arguments) == 1: return f'argument "{arguments[0]}"' head = ", ".join(arguments[:-1]) return f'arguments "{head}" and "{arguments[-1]}"' @cached_property def exception_source(self) -> ExceptionSource | None: if self.function is None: return None # pragma: no cover source_finder = SourceFinder() return source_finder.find_argument_from_object( self.function, # type: ignore self.argument_name, ) strawberry-graphql-0.287.0/strawberry/exceptions/missing_dependencies.py000066400000000000000000000014351511033167500266530ustar00rootroot00000000000000from __future__ import annotations class MissingOptionalDependenciesError(Exception): """Some optional dependencies that are required for a particular task are missing.""" def __init__( self, *, packages: list[str] | None = None, extras: list[str] | None = None, ) -> None: """Initialize the error. Args: packages: List of packages that are required but missing. extras: List of extras that are required but missing. """ packages = packages or [] if extras: packages.append(f"'strawberry-graphql[{','.join(extras)}]'") hint = f" (hint: pip install {' '.join(packages)})" if packages else "" self.message = f"Some optional dependencies are missing{hint}" strawberry-graphql-0.287.0/strawberry/exceptions/missing_field_annotation.py000066400000000000000000000024471511033167500275460ustar00rootroot00000000000000from __future__ import annotations from functools import cached_property from typing import TYPE_CHECKING from .exception import StrawberryException from .utils.source_finder import SourceFinder if TYPE_CHECKING: from .exception_source import ExceptionSource class MissingFieldAnnotationError(StrawberryException): def __init__(self, field_name: str, cls: type) -> None: self.cls = cls self.field_name = field_name self.message = ( f'Unable to determine the type of field "{field_name}". Either ' f"annotate it directly, or provide a typed resolver using " f"@strawberry.field." ) self.rich_message = ( f"Missing annotation for field `[underline]{self.field_name}[/]`" ) self.suggestion = ( "To fix this error you can add an annotation, " f"like so [italic]`{self.field_name}: str`" ) self.annotation_message = "field missing annotation" super().__init__(self.message) @cached_property def exception_source(self) -> ExceptionSource | None: if self.cls is None: return None # pragma: no cover source_finder = SourceFinder() return source_finder.find_class_attribute_from_object(self.cls, self.field_name) strawberry-graphql-0.287.0/strawberry/exceptions/missing_return_annotation.py000066400000000000000000000025551511033167500300020ustar00rootroot00000000000000from __future__ import annotations from functools import cached_property from typing import TYPE_CHECKING from .exception import StrawberryException from .utils.source_finder import SourceFinder if TYPE_CHECKING: from strawberry.types.fields.resolver import StrawberryResolver from .exception_source import ExceptionSource class MissingReturnAnnotationError(StrawberryException): """The field is missing the return annotation.""" def __init__( self, field_name: str, resolver: StrawberryResolver, ) -> None: self.function = resolver.wrapped_func self.message = ( f'Return annotation missing for field "{field_name}", ' "did you forget to add it?" ) self.rich_message = ( f"[bold red]Missing annotation for field `[underline]{resolver.name}[/]`" ) self.suggestion = ( "To fix this error you can add an annotation, " f"like so [italic]`def {resolver.name}(...) -> str:`" ) self.annotation_message = "resolver missing annotation" @cached_property def exception_source(self) -> ExceptionSource | None: if self.function is None: return None # pragma: no cover source_finder = SourceFinder() return source_finder.find_function_from_object(self.function) # type: ignore strawberry-graphql-0.287.0/strawberry/exceptions/object_is_not_a_class.py000066400000000000000000000037131511033167500270030ustar00rootroot00000000000000from __future__ import annotations from enum import Enum from functools import cached_property from typing import TYPE_CHECKING from .exception import StrawberryException from .utils.source_finder import SourceFinder if TYPE_CHECKING: from .exception_source import ExceptionSource class ObjectIsNotClassError(StrawberryException): class MethodType(Enum): INPUT = "input" INTERFACE = "interface" TYPE = "type" def __init__(self, obj: object, method_type: MethodType) -> None: self.obj = obj self.function = obj # TODO: assert obj is a function for now and skip the error if it is # something else obj_name = obj.__name__ # type: ignore self.message = ( f"strawberry.{method_type.value} can only be used with class types. " f"Provided object {obj_name} is not a type." ) self.rich_message = ( f"strawberry.{method_type.value} can only be used with class types. " f"Provided object `[underline]{obj_name}[/]` is not a type." ) self.annotation_message = "function defined here" self.suggestion = ( "To fix this error, make sure your use " f"strawberry.{method_type.value} on a class." ) super().__init__(self.message) @classmethod def input(cls, obj: object) -> ObjectIsNotClassError: return cls(obj, cls.MethodType.INPUT) @classmethod def interface(cls, obj: object) -> ObjectIsNotClassError: return cls(obj, cls.MethodType.INTERFACE) @classmethod def type(cls, obj: object) -> ObjectIsNotClassError: return cls(obj, cls.MethodType.TYPE) @cached_property def exception_source(self) -> ExceptionSource | None: if self.function is None: return None # pragma: no cover source_finder = SourceFinder() return source_finder.find_function_from_object(self.function) # type: ignore strawberry-graphql-0.287.0/strawberry/exceptions/object_is_not_an_enum.py000066400000000000000000000023421511033167500270150ustar00rootroot00000000000000from __future__ import annotations from functools import cached_property from typing import TYPE_CHECKING from .exception import StrawberryException from .utils.source_finder import SourceFinder if TYPE_CHECKING: from enum import Enum from .exception_source import ExceptionSource class ObjectIsNotAnEnumError(StrawberryException): def __init__(self, cls: type[Enum]) -> None: self.cls = cls self.message = ( "strawberry.enum can only be used with subclasses of Enum. " f"Provided object {cls.__name__} is not an enum." ) self.rich_message = ( "strawberry.enum can only be used with subclasses of Enum. " f"Provided object `[underline]{cls.__name__}[/]` is not an enum." ) self.annotation_message = "class defined here" self.suggestion = ( "To fix this error, make sure your class is a subclass of enum.Enum." ) super().__init__(self.message) @cached_property def exception_source(self) -> ExceptionSource | None: if self.cls is None: return None # pragma: no cover source_finder = SourceFinder() return source_finder.find_class_from_object(self.cls) strawberry-graphql-0.287.0/strawberry/exceptions/permission_fail_silently_requires_optional.py000066400000000000000000000033011511033167500334200ustar00rootroot00000000000000from __future__ import annotations from functools import cached_property from typing import TYPE_CHECKING from .exception import StrawberryException from .utils.source_finder import SourceFinder if TYPE_CHECKING: from strawberry.field import StrawberryField from .exception_source import ExceptionSource class PermissionFailSilentlyRequiresOptionalError(StrawberryException): def __init__(self, field: StrawberryField) -> None: self.field = field self.message = ( "Cannot use fail_silently=True with a non-optional or non-list field" ) self.rich_message = ( "fail_silently permissions can only be used with fields of type " f"optional or list. Provided field `[underline]{field.name}[/]` " f"is of type `[underline]{field.type.__name__}[/]`" ) self.annotation_message = "field defined here" self.suggestion = ( "To fix this error, make sure you apply use `fail_silently`" " on a field that is either a list or nullable." ) super().__init__(self.message) @cached_property def exception_source(self) -> ExceptionSource | None: origin = self.field.origin source_finder = SourceFinder() source = None if origin is not None: source = source_finder.find_class_attribute_from_object( origin, self.field.python_name, ) # in case it is a function if source is None and self.field.base_resolver is not None: source = source_finder.find_function_from_object( self.field.base_resolver.wrapped_func ) return source strawberry-graphql-0.287.0/strawberry/exceptions/private_strawberry_field.py000066400000000000000000000024431511033167500275750ustar00rootroot00000000000000from __future__ import annotations from functools import cached_property from typing import TYPE_CHECKING from .exception import StrawberryException from .utils.source_finder import SourceFinder if TYPE_CHECKING: from .exception_source import ExceptionSource class PrivateStrawberryFieldError(StrawberryException): def __init__(self, field_name: str, cls: type) -> None: self.cls = cls self.field_name = field_name self.message = ( f"Field {field_name} on type {cls.__name__} cannot be both " "private and a strawberry.field" ) self.rich_message = ( f"`[underline]{self.field_name}[/]` field cannot be both " "private and a strawberry.field " ) self.annotation_message = "private field defined here" self.suggestion = ( "To fix this error you should either make the field non private, " "or remove the strawberry.field annotation." ) super().__init__(self.message) @cached_property def exception_source(self) -> ExceptionSource | None: if self.cls is None: return None # pragma: no cover source_finder = SourceFinder() return source_finder.find_class_attribute_from_object(self.cls, self.field_name) strawberry-graphql-0.287.0/strawberry/exceptions/scalar_already_registered.py000066400000000000000000000034531511033167500276610ustar00rootroot00000000000000from __future__ import annotations from functools import cached_property from pathlib import Path from typing import TYPE_CHECKING from strawberry.exceptions.utils.source_finder import SourceFinder from .exception import StrawberryException if TYPE_CHECKING: from strawberry.types.scalar import ScalarDefinition from .exception_source import ExceptionSource class ScalarAlreadyRegisteredError(StrawberryException): def __init__( self, scalar_definition: ScalarDefinition, other_scalar_definition: ScalarDefinition, ) -> None: self.scalar_definition = scalar_definition scalar_name = scalar_definition.name self.message = f"Scalar `{scalar_name}` has already been registered" self.rich_message = ( f"Scalar `[underline]{scalar_name}[/]` has already been registered" ) self.annotation_message = "scalar defined here" self.suggestion = ( "To fix this error you should either rename the scalar, " "or reuse the existing one" ) if other_scalar_definition._source_file: other_path = Path(other_scalar_definition._source_file) other_line = other_scalar_definition._source_line self.suggestion += ( f", defined in [bold white][link=file://{other_path}]" f"{other_path.relative_to(Path.cwd())}:{other_line}[/]" ) super().__init__(self.message) @cached_property def exception_source(self) -> ExceptionSource | None: if not all( (self.scalar_definition._source_file, self.scalar_definition._source_line) ): return None # pragma: no cover source_finder = SourceFinder() return source_finder.find_scalar_call(self.scalar_definition) strawberry-graphql-0.287.0/strawberry/exceptions/syntax.py000066400000000000000000000032741511033167500240250ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from pygments.lexers import PythonLexer from rich.segment import Segment from rich.syntax import Syntax as RichSyntax if TYPE_CHECKING: from rich.console import Console, ConsoleOptions, RenderResult class Syntax(RichSyntax): def __init__( self, code: str, line_range: tuple[int, int], highlight_lines: set[int] | None = None, line_offset: int = 0, line_annotations: dict[int, str] | None = None, ) -> None: self.line_offset = line_offset self.line_annotations = line_annotations or {} super().__init__( code=code, lexer=PythonLexer(), line_numbers=True, word_wrap=False, theme="ansi_light", highlight_lines=highlight_lines, line_range=line_range, ) def __rich_console__( self, console: Console, options: ConsoleOptions ) -> RenderResult: assert self.line_range segments = self._get_syntax(console, options) annotations = self.line_annotations.copy() current_line = self.line_range[0] or 0 for segment in segments: if segment.text == "\n": # 3 = | + space + space prefix = " " * (self._numbers_column_width + 3) annotation = annotations.pop(current_line, None) current_line += 1 if annotation: yield "" yield prefix + annotation continue yield segment if segment.text.strip() == str(current_line): yield Segment("| ") strawberry-graphql-0.287.0/strawberry/exceptions/unresolved_field_type.py000066400000000000000000000035101511033167500270620ustar00rootroot00000000000000from __future__ import annotations from functools import cached_property from typing import TYPE_CHECKING from strawberry.exceptions.utils.source_finder import SourceFinder from .exception import StrawberryException if TYPE_CHECKING: from strawberry.types.field import StrawberryField from strawberry.types.object_type import StrawberryObjectDefinition from .exception_source import ExceptionSource class UnresolvedFieldTypeError(StrawberryException): def __init__( self, type_definition: StrawberryObjectDefinition, field: StrawberryField, ) -> None: self.type_definition = type_definition self.field = field self.message = ( f"Could not resolve the type of '{self.field.name}'. " "Check that the class is accessible from the global module scope." ) self.rich_message = ( f"Could not resolve the type of [underline]'{self.field.name}'[/]. " "Check that the class is accessible from the global module scope." ) self.annotation_message = "field defined here" self.suggestion = ( "To fix this error you should either import the type or use LazyType." ) super().__init__(self.message) @cached_property def exception_source(self) -> ExceptionSource | None: source_finder = SourceFinder() # field could be attached to the class or not source = source_finder.find_class_attribute_from_object( self.type_definition.origin, self.field.name ) if source is not None: return source if self.field.base_resolver: return source_finder.find_function_from_object( self.field.base_resolver.wrapped_func # type: ignore ) return None # pragma: no cover strawberry-graphql-0.287.0/strawberry/exceptions/utils/000077500000000000000000000000001511033167500232575ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/exceptions/utils/__init__.py000066400000000000000000000000001511033167500253560ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/exceptions/utils/source_finder.py000066400000000000000000000537271511033167500264760ustar00rootroot00000000000000from __future__ import annotations import importlib import importlib.util import sys from dataclasses import dataclass from functools import cached_property from pathlib import Path from typing import TYPE_CHECKING, Any, cast from strawberry.exceptions.exception_source import ExceptionSource if TYPE_CHECKING: from collections.abc import Callable, Sequence from inspect import Traceback from libcst import BinaryOperation, Call, CSTNode, FunctionDef from strawberry.types.scalar import ScalarDefinition from strawberry.types.union import StrawberryUnion @dataclass class SourcePath: path: Path code: str class LibCSTSourceFinder: def __init__(self) -> None: self.cst = importlib.import_module("libcst") def find_source(self, module: str) -> SourcePath | None: # TODO: support for pyodide source_module = sys.modules.get(module) path = None if source_module is None: spec = importlib.util.find_spec(module) if spec is not None and spec.origin is not None: path = Path(spec.origin) elif source_module.__file__ is not None: path = Path(source_module.__file__) if path is None: return None if not path.exists() or path.suffix != ".py": return None # pragma: no cover source = path.read_text(encoding="utf-8") return SourcePath(path=path, code=source) def _find(self, source: str, matcher: Any) -> Sequence[CSTNode]: from libcst.metadata import ( MetadataWrapper, ParentNodeProvider, PositionProvider, ) module = self.cst.parse_module(source) self._metadata_wrapper = MetadataWrapper(module) self._position_metadata = self._metadata_wrapper.resolve(PositionProvider) self._parent_metadata = self._metadata_wrapper.resolve(ParentNodeProvider) import libcst.matchers as m return m.findall(self._metadata_wrapper, matcher) def _find_definition_by_qualname( self, qualname: str, nodes: Sequence[CSTNode] ) -> CSTNode | None: from libcst import ClassDef, FunctionDef for definition in nodes: parent: CSTNode | None = definition stack = [] while parent: if isinstance(parent, ClassDef): stack.append(parent.name.value) if isinstance(parent, FunctionDef): stack.extend(("", parent.name.value)) parent = self._parent_metadata.get(parent) if stack[0] == "": stack.pop(0) found_class_name = ".".join(reversed(stack)) if found_class_name == qualname: return definition return None def _find_function_definition( self, source: SourcePath, function: Callable[..., Any] ) -> FunctionDef | None: import libcst.matchers as m matcher = m.FunctionDef(name=m.Name(value=function.__name__)) function_defs = self._find(source.code, matcher) return cast( "FunctionDef", self._find_definition_by_qualname(function.__qualname__, function_defs), ) def _find_class_definition( self, source: SourcePath, cls: type[Any] ) -> CSTNode | None: import libcst.matchers as m matcher = m.ClassDef(name=m.Name(value=cls.__name__)) class_defs = self._find(source.code, matcher) return self._find_definition_by_qualname(cls.__qualname__, class_defs) def find_class(self, cls: type[Any]) -> ExceptionSource | None: source = self.find_source(cls.__module__) if source is None: return None # pragma: no cover class_def = self._find_class_definition(source, cls) if class_def is None: return None # pragma: no cover position = self._position_metadata[class_def] column_start = position.start.column + len("class ") return ExceptionSource( path=source.path, code=source.code, start_line=position.start.line, error_line=position.start.line, end_line=position.end.line, error_column=column_start, error_column_end=column_start + len(cls.__name__), ) def find_class_attribute( self, cls: type[Any], attribute_name: str ) -> ExceptionSource | None: source = self.find_source(cls.__module__) if source is None: return None # pragma: no cover class_def = self._find_class_definition(source, cls) if class_def is None: return None # pragma: no cover import libcst.matchers as m from libcst import AnnAssign attribute_definitions = m.findall( class_def, m.AssignTarget(target=m.Name(value=attribute_name)) | m.AnnAssign(target=m.Name(value=attribute_name)) | m.FunctionDef(name=m.Name(value=attribute_name)), ) if not attribute_definitions: return None attribute_definition = attribute_definitions[0] if isinstance(attribute_definition, AnnAssign): attribute_definition = attribute_definition.target class_position = self._position_metadata[class_def] attribute_position = self._position_metadata[attribute_definition] return ExceptionSource( path=source.path, code=source.code, start_line=class_position.start.line, error_line=attribute_position.start.line, end_line=class_position.end.line, error_column=attribute_position.start.column, error_column_end=attribute_position.end.column, ) def find_function(self, function: Callable[..., Any]) -> ExceptionSource | None: source = self.find_source(function.__module__) if source is None: return None # pragma: no cover function_def = self._find_function_definition(source, function) if function_def is None: return None # pragma: no cover position = self._position_metadata[function_def] prefix = f"def{function_def.whitespace_after_def.value}" if function_def.asynchronous: prefix = f"async{function_def.asynchronous.whitespace_after.value}{prefix}" function_prefix = len(prefix) error_column = position.start.column + function_prefix error_column_end = error_column + len(function.__name__) return ExceptionSource( path=source.path, code=source.code, start_line=position.start.line, error_line=position.start.line, end_line=position.end.line, error_column=error_column, error_column_end=error_column_end, ) def find_argument( self, function: Callable[..., Any], argument_name: str ) -> ExceptionSource | None: source = self.find_source(function.__module__) if source is None: return None # pragma: no cover function_def = self._find_function_definition(source, function) if function_def is None: return None # pragma: no cover import libcst.matchers as m argument_defs = m.findall( function_def, m.Param(name=m.Name(value=argument_name)), ) if not argument_defs: return None # pragma: no cover argument_def = argument_defs[0] function_position = self._position_metadata[function_def] position = self._position_metadata[argument_def] return ExceptionSource( path=source.path, code=source.code, start_line=function_position.start.line, end_line=function_position.end.line, error_line=position.start.line, error_column=position.start.column, error_column_end=position.end.column, ) def find_union_call( self, path: Path, union_name: str, invalid_type: object ) -> ExceptionSource | None: import libcst.matchers as m source = path.read_text() invalid_type_name = getattr(invalid_type, "__name__", None) types_arg_matcher = ( [ m.Tuple( elements=[ m.ZeroOrMore(), m.Element(value=m.Name(value=invalid_type_name)), m.ZeroOrMore(), ], ) | m.List( elements=[ m.ZeroOrMore(), m.Element(value=m.Name(value=invalid_type_name)), m.ZeroOrMore(), ], ) ] if invalid_type_name is not None else [] ) matcher = m.Call( func=m.Attribute( value=m.Name(value="strawberry"), attr=m.Name(value="union"), ) | m.Name(value="union"), args=[ m.Arg(value=m.SimpleString(value=f"'{union_name}'")) | m.Arg(value=m.SimpleString(value=f'"{union_name}"')), m.Arg(*types_arg_matcher), # type: ignore ], ) union_calls = self._find(source, matcher) if not union_calls: return None # pragma: no cover union_call = cast("Call", union_calls[0]) if invalid_type_name: invalid_type_nodes = m.findall( union_call.args[1], m.Element(value=m.Name(value=invalid_type_name)), ) if not invalid_type_nodes: return None # pragma: no cover invalid_type_node = invalid_type_nodes[0] else: invalid_type_node = union_call position = self._position_metadata[union_call] invalid_type_node_position = self._position_metadata[invalid_type_node] return ExceptionSource( path=path, code=source, start_line=position.start.line, error_line=invalid_type_node_position.start.line, end_line=position.end.line, error_column=invalid_type_node_position.start.column, error_column_end=invalid_type_node_position.end.column, ) def find_union_merge( self, union: StrawberryUnion, other: object, frame: Traceback ) -> ExceptionSource | None: import libcst.matchers as m path = Path(frame.filename) source = path.read_text() other_name = getattr(other, "__name__", None) if other_name is None: return None # pragma: no cover matcher = m.BinaryOperation(operator=m.BitOr(), right=m.Name(value=other_name)) merge_calls = self._find(source, matcher) if not merge_calls: return None # pragma: no cover merge_call_node = cast("BinaryOperation", merge_calls[0]) invalid_type_node = merge_call_node.right position = self._position_metadata[merge_call_node] invalid_type_node_position = self._position_metadata[invalid_type_node] return ExceptionSource( path=path, code=source, start_line=position.start.line, error_line=invalid_type_node_position.start.line, end_line=position.end.line, error_column=invalid_type_node_position.start.column, error_column_end=invalid_type_node_position.end.column, ) def find_annotated_union( self, union_definition: StrawberryUnion, invalid_type: object ) -> ExceptionSource | None: if union_definition._source_file is None: return None # find things like Annotated[Union[...], strawberry.union(name="aaa")] import libcst.matchers as m path = Path(union_definition._source_file) source = path.read_text() matcher = m.Subscript( value=m.Name(value="Annotated"), slice=( m.SubscriptElement( slice=m.Index( value=m.Subscript( value=m.Name(value="Union"), ) ) ), m.SubscriptElement( slice=m.Index( value=m.Call( func=m.Attribute( value=m.Name(value="strawberry"), attr=m.Name(value="union"), ), args=[ m.Arg( value=m.SimpleString( value=f"'{union_definition.graphql_name}'" ) | m.SimpleString( value=f'"{union_definition.graphql_name}"' ) ) ], ) ) ), ), ) annotated_calls = self._find(source, matcher) invalid_type_name = getattr(invalid_type, "__name__", None) if hasattr(invalid_type, "_scalar_definition"): invalid_type_name = invalid_type._scalar_definition.name if annotated_calls: annotated_call_node = annotated_calls[0] if invalid_type_name: invalid_type_nodes = m.findall( annotated_call_node, m.SubscriptElement(slice=m.Index(m.Name(invalid_type_name))), ) if not invalid_type_nodes: return None # pragma: no cover invalid_type_node = invalid_type_nodes[0] else: invalid_type_node = annotated_call_node else: matcher = m.Subscript( value=m.Name(value="Annotated"), slice=( m.SubscriptElement(slice=m.Index(value=m.BinaryOperation())), m.SubscriptElement( slice=m.Index( value=m.Call( func=m.Attribute( value=m.Name(value="strawberry"), attr=m.Name(value="union"), ), args=[ m.Arg( value=m.SimpleString( value=f"'{union_definition.graphql_name}'" ) | m.SimpleString( value=f'"{union_definition.graphql_name}"' ) ) ], ) ) ), ), ) annotated_calls = self._find(source, matcher) if not annotated_calls: return None annotated_call_node = annotated_calls[0] if invalid_type_name: invalid_type_nodes = m.findall( annotated_call_node, m.BinaryOperation(left=m.Name(invalid_type_name)), ) if not invalid_type_nodes: return None # pragma: no cover invalid_type_node = invalid_type_nodes[0].left # type: ignore else: invalid_type_node = annotated_call_node position = self._position_metadata[annotated_call_node] invalid_type_node_position = self._position_metadata[invalid_type_node] return ExceptionSource( path=path, code=source, start_line=position.start.line, end_line=position.end.line, error_line=invalid_type_node_position.start.line, error_column=invalid_type_node_position.start.column, error_column_end=invalid_type_node_position.end.column, ) def find_scalar_call( self, scalar_definition: ScalarDefinition ) -> ExceptionSource | None: if scalar_definition._source_file is None: return None # pragma: no cover import libcst.matchers as m path = Path(scalar_definition._source_file) source = path.read_text() # Try to find direct strawberry.scalar() calls with name parameter direct_matcher = m.Call( func=m.Attribute(value=m.Name(value="strawberry"), attr=m.Name("scalar")) | m.Name("scalar"), args=[ m.ZeroOrMore(), m.Arg( keyword=m.Name(value="name"), value=m.SimpleString(value=f"'{scalar_definition.name}'") | m.SimpleString(value=f'"{scalar_definition.name}"'), ), m.ZeroOrMore(), ], ) direct_calls = self._find(source, direct_matcher) if direct_calls: return self._create_scalar_exception_source( path, source, direct_calls[0], scalar_definition, is_newtype=False ) # Try to find strawberry.scalar() calls with NewType pattern newtype_matcher = m.Call( func=m.Attribute(value=m.Name(value="strawberry"), attr=m.Name("scalar")) | m.Name("scalar"), args=[ m.Arg( value=m.Call( func=m.Name(value="NewType"), args=[ m.Arg( value=m.SimpleString( value=f"'{scalar_definition.name}'" ) | m.SimpleString(value=f'"{scalar_definition.name}"'), ), m.ZeroOrMore(), ], ) ), m.ZeroOrMore(), ], ) newtype_calls = self._find(source, newtype_matcher) if newtype_calls: return self._create_scalar_exception_source( path, source, newtype_calls[0], scalar_definition, is_newtype=True ) return None # pragma: no cover def _create_scalar_exception_source( self, path: Path, source: str, call_node: Any, scalar_definition: ScalarDefinition, is_newtype: bool, ) -> ExceptionSource | None: """Helper method to create ExceptionSource for scalar calls.""" import libcst.matchers as m if is_newtype: # For NewType pattern, find the string argument within the NewType call newtype_nodes = m.findall(call_node, m.Call(func=m.Name(value="NewType"))) if not newtype_nodes: return None # pragma: no cover target_node = newtype_nodes[0] string_nodes = m.findall( target_node, m.SimpleString(value=f"'{scalar_definition.name}'") | m.SimpleString(value=f'"{scalar_definition.name}"'), ) if not string_nodes: return None # pragma: no cover target_node = string_nodes[0] else: # For direct calls, find the name argument target_nodes = m.findall(call_node, m.Arg(keyword=m.Name(value="name"))) if not target_nodes: return None # pragma: no cover target_node = target_nodes[0] position = self._position_metadata[call_node] target_position = self._position_metadata[target_node] return ExceptionSource( path=path, code=source, start_line=position.start.line, end_line=position.end.line, error_line=target_position.start.line, error_column=target_position.start.column, error_column_end=target_position.end.column, ) class SourceFinder: @cached_property def cst(self) -> LibCSTSourceFinder | None: try: return LibCSTSourceFinder() except ImportError: return None # pragma: no cover def find_class_from_object(self, cls: type[Any]) -> ExceptionSource | None: return self.cst.find_class(cls) if self.cst else None def find_class_attribute_from_object( self, cls: type[Any], attribute_name: str ) -> ExceptionSource | None: return self.cst.find_class_attribute(cls, attribute_name) if self.cst else None def find_function_from_object( self, function: Callable[..., Any] ) -> ExceptionSource | None: return self.cst.find_function(function) if self.cst else None def find_argument_from_object( self, function: Callable[..., Any], argument_name: str ) -> ExceptionSource | None: return self.cst.find_argument(function, argument_name) if self.cst else None def find_union_call( self, path: Path, union_name: str, invalid_type: object ) -> ExceptionSource | None: return ( self.cst.find_union_call(path, union_name, invalid_type) if self.cst else None ) def find_union_merge( self, union: StrawberryUnion, other: object, frame: Traceback ) -> ExceptionSource | None: return self.cst.find_union_merge(union, other, frame) if self.cst else None def find_scalar_call( self, scalar_definition: ScalarDefinition ) -> ExceptionSource | None: return self.cst.find_scalar_call(scalar_definition) if self.cst else None def find_annotated_union( self, union_definition: StrawberryUnion, invalid_type: object ) -> ExceptionSource | None: return ( self.cst.find_annotated_union(union_definition, invalid_type) if self.cst else None ) __all__ = ["SourceFinder"] strawberry-graphql-0.287.0/strawberry/experimental/000077500000000000000000000000001511033167500224335ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/experimental/__init__.py000066400000000000000000000001461511033167500245450ustar00rootroot00000000000000try: from . import pydantic except ModuleNotFoundError: pass else: __all__ = ["pydantic"] strawberry-graphql-0.287.0/strawberry/experimental/pydantic/000077500000000000000000000000001511033167500242465ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/experimental/pydantic/__init__.py000066400000000000000000000003771511033167500263660ustar00rootroot00000000000000from .error_type import error_type from .exceptions import UnregisteredTypeException from .object_type import input, interface, type # noqa: A004 __all__ = [ "UnregisteredTypeException", "error_type", "input", "interface", "type", ] strawberry-graphql-0.287.0/strawberry/experimental/pydantic/_compat.py000066400000000000000000000230231511033167500262420ustar00rootroot00000000000000import dataclasses from collections.abc import Callable from dataclasses import dataclass from decimal import Decimal from functools import cached_property from typing import TYPE_CHECKING, Any, Optional from uuid import UUID import pydantic from pydantic import BaseModel from pydantic.version import VERSION as PYDANTIC_VERSION from strawberry.experimental.pydantic.exceptions import UnsupportedTypeError if TYPE_CHECKING: from pydantic.fields import ComputedFieldInfo, FieldInfo IS_PYDANTIC_V2: bool = PYDANTIC_VERSION.startswith("2.") IS_PYDANTIC_V1: bool = not IS_PYDANTIC_V2 @dataclass class CompatModelField: name: str type_: Any outer_type_: Any default: Any default_factory: Callable[[], Any] | None required: bool alias: str | None allow_none: bool has_alias: bool description: str | None _missing_type: Any is_v1: bool @property def has_default_factory(self) -> bool: return self.default_factory is not self._missing_type @property def has_default(self) -> bool: return self.default is not self._missing_type ATTR_TO_TYPE_MAP = { "NoneStr": Optional[str], # noqa: UP045 "NoneBytes": Optional[bytes], # noqa: UP045 "StrBytes": None, "NoneStrBytes": None, "StrictStr": str, "ConstrainedBytes": bytes, "conbytes": bytes, "ConstrainedStr": str, "constr": str, "EmailStr": str, "PyObject": None, "ConstrainedInt": int, "conint": int, "PositiveInt": int, "NegativeInt": int, "ConstrainedFloat": float, "confloat": float, "PositiveFloat": float, "NegativeFloat": float, "ConstrainedDecimal": Decimal, "condecimal": Decimal, "UUID1": UUID, "UUID3": UUID, "UUID4": UUID, "UUID5": UUID, "FilePath": None, "DirectoryPath": None, "Json": None, "JsonWrapper": None, "SecretStr": str, "SecretBytes": bytes, "StrictBool": bool, "StrictInt": int, "StrictFloat": float, "PaymentCardNumber": None, "ByteSize": None, "AnyUrl": str, "AnyHttpUrl": str, "HttpUrl": str, "PostgresDsn": str, "RedisDsn": str, } ATTR_TO_TYPE_MAP_Pydantic_V2 = { "EmailStr": str, "SecretStr": str, "SecretBytes": bytes, "AnyUrl": str, "AnyHttpUrl": str, "HttpUrl": str, "PostgresDsn": str, "RedisDsn": str, } ATTR_TO_TYPE_MAP_Pydantic_Core_V2 = { "MultiHostUrl": str, } def get_fields_map_for_v2() -> dict[Any, Any]: import pydantic_core fields_map = { getattr(pydantic, field_name): type for field_name, type in ATTR_TO_TYPE_MAP_Pydantic_V2.items() if hasattr(pydantic, field_name) } fields_map.update( { getattr(pydantic_core, field_name): type for field_name, type in ATTR_TO_TYPE_MAP_Pydantic_Core_V2.items() if hasattr(pydantic_core, field_name) } ) return fields_map class PydanticV2Compat: @property def PYDANTIC_MISSING_TYPE(self) -> Any: # noqa: N802 from pydantic_core import PydanticUndefined return PydanticUndefined def get_model_computed_fields( self, model: type[BaseModel] ) -> dict[str, CompatModelField]: computed_field_info: dict[str, ComputedFieldInfo] = model.model_computed_fields new_fields = {} # Convert it into CompatModelField for name, field in computed_field_info.items(): new_fields[name] = CompatModelField( name=name, type_=field.return_type, outer_type_=field.return_type, default=None, default_factory=None, required=False, alias=field.alias, # v2 doesn't have allow_none allow_none=False, has_alias=field is not None, description=field.description, _missing_type=self.PYDANTIC_MISSING_TYPE, is_v1=False, ) return new_fields def get_model_fields( self, model: type[BaseModel], include_computed: bool = False ) -> dict[str, CompatModelField]: field_info: dict[str, FieldInfo] = model.model_fields new_fields = {} # Convert it into CompatModelField for name, field in field_info.items(): new_fields[name] = CompatModelField( name=name, type_=field.annotation, outer_type_=field.annotation, default=field.default, default_factory=field.default_factory, # type: ignore required=field.is_required(), alias=field.alias, # v2 doesn't have allow_none allow_none=False, has_alias=field is not None, description=field.description, _missing_type=self.PYDANTIC_MISSING_TYPE, is_v1=False, ) if include_computed: new_fields |= self.get_model_computed_fields(model) return new_fields @cached_property def fields_map(self) -> dict[Any, Any]: return get_fields_map_for_v2() def get_basic_type(self, type_: Any) -> type[Any]: if type_ in self.fields_map: type_ = self.fields_map[type_] if type_ is None: raise UnsupportedTypeError if is_new_type(type_): return new_type_supertype(type_) return type_ def model_dump(self, model_instance: BaseModel) -> dict[Any, Any]: return model_instance.model_dump() class PydanticV1Compat: @property def PYDANTIC_MISSING_TYPE(self) -> Any: # noqa: N802 return dataclasses.MISSING def get_model_fields( self, model: type[BaseModel], include_computed: bool = False ) -> dict[str, CompatModelField]: """`include_computed` is a noop for PydanticV1Compat.""" new_fields = {} # Convert it into CompatModelField for name, field in model.__fields__.items(): # type: ignore[attr-defined] new_fields[name] = CompatModelField( name=name, type_=field.type_, outer_type_=field.outer_type_, default=field.default, default_factory=field.default_factory, required=field.required, alias=field.alias, allow_none=field.allow_none, has_alias=field.has_alias, description=field.field_info.description, _missing_type=self.PYDANTIC_MISSING_TYPE, is_v1=True, ) return new_fields @cached_property def fields_map(self) -> dict[Any, Any]: if IS_PYDANTIC_V2: return { getattr(pydantic.v1, field_name): type for field_name, type in ATTR_TO_TYPE_MAP.items() if hasattr(pydantic.v1, field_name) } return { getattr(pydantic, field_name): type for field_name, type in ATTR_TO_TYPE_MAP.items() if hasattr(pydantic, field_name) } def get_basic_type(self, type_: Any) -> type[Any]: if IS_PYDANTIC_V1: ConstrainedInt = pydantic.ConstrainedInt ConstrainedFloat = pydantic.ConstrainedFloat ConstrainedStr = pydantic.ConstrainedStr ConstrainedList = pydantic.ConstrainedList else: ConstrainedInt = pydantic.v1.ConstrainedInt ConstrainedFloat = pydantic.v1.ConstrainedFloat ConstrainedStr = pydantic.v1.ConstrainedStr ConstrainedList = pydantic.v1.ConstrainedList if lenient_issubclass(type_, ConstrainedInt): # type: ignore return int if lenient_issubclass(type_, ConstrainedFloat): # type: ignore return float if lenient_issubclass(type_, ConstrainedStr): # type: ignore return str if lenient_issubclass(type_, ConstrainedList): # type: ignore return list[self.get_basic_type(type_.item_type)] # type: ignore if type_ in self.fields_map: type_ = self.fields_map[type_] if type_ is None: raise UnsupportedTypeError if is_new_type(type_): return new_type_supertype(type_) return type_ def model_dump(self, model_instance: BaseModel) -> dict[Any, Any]: return model_instance.dict() class PydanticCompat: def __init__(self, is_v2: bool) -> None: if is_v2: self._compat = PydanticV2Compat() else: self._compat = PydanticV1Compat() # type: ignore[assignment] @classmethod def from_model(cls, model: type[BaseModel]) -> "PydanticCompat": if hasattr(model, "model_fields"): return cls(is_v2=True) return cls(is_v2=False) def __getattr__(self, name: str) -> Any: return getattr(self._compat, name) if IS_PYDANTIC_V2: from typing import get_args, get_origin from pydantic.v1.typing import is_new_type from pydantic.v1.utils import lenient_issubclass, smart_deepcopy def new_type_supertype(type_: Any) -> Any: return type_.__supertype__ else: from pydantic.typing import ( # type: ignore[no-redef] get_args, get_origin, is_new_type, new_type_supertype, ) from pydantic.utils import ( # type: ignore[no-redef] lenient_issubclass, smart_deepcopy, ) __all__ = [ "PydanticCompat", "get_args", "get_origin", "is_new_type", "lenient_issubclass", "new_type_supertype", "smart_deepcopy", ] strawberry-graphql-0.287.0/strawberry/experimental/pydantic/conversion.py000066400000000000000000000102311511033167500270020ustar00rootroot00000000000000from __future__ import annotations import copy import dataclasses from typing import TYPE_CHECKING, Any, cast from strawberry.types.base import ( StrawberryList, StrawberryOptional, has_object_definition, ) from strawberry.types.enum import StrawberryEnumDefinition from strawberry.types.union import StrawberryUnion if TYPE_CHECKING: from strawberry.types.base import StrawberryType from strawberry.types.field import StrawberryField def _convert_from_pydantic_to_strawberry_type( type_: StrawberryType | type, data_from_model=None, # noqa: ANN001 extra=None, # noqa: ANN001 ) -> Any: data = data_from_model if data_from_model is not None else extra if isinstance(type_, StrawberryOptional): if data is None: return data return _convert_from_pydantic_to_strawberry_type( type_.of_type, data_from_model=data, extra=extra ) if isinstance(type_, StrawberryUnion): for option_type in type_.types: if hasattr(option_type, "_pydantic_type"): source_type = option_type._pydantic_type else: source_type = cast("type", option_type) if isinstance(data, source_type): return _convert_from_pydantic_to_strawberry_type( option_type, data_from_model=data, extra=extra ) if isinstance(type_, StrawberryEnumDefinition): return data if isinstance(type_, StrawberryList): items = [] for index, item in enumerate(data): items.append( _convert_from_pydantic_to_strawberry_type( type_.of_type, data_from_model=item, extra=extra[index] if extra else None, ) ) return items if has_object_definition(type_): # in the case of an interface, the concrete type may be more specific # than the type in the field definition # don't check _strawberry_input_type because inputs can't be interfaces if hasattr(type(data), "_strawberry_type"): type_ = type(data)._strawberry_type if hasattr(type_, "from_pydantic"): return type_.from_pydantic(data_from_model, extra) return convert_pydantic_model_to_strawberry_class( type_, model_instance=data_from_model, extra=extra ) return data def convert_pydantic_model_to_strawberry_class( cls, # noqa: ANN001 *, model_instance=None, # noqa: ANN001 extra=None, # noqa: ANN001 ) -> Any: extra = extra or {} kwargs = {} for field_ in cls.__strawberry_definition__.fields: field = cast("StrawberryField", field_) python_name = field.python_name # only convert and add fields to kwargs if they are present in the `__init__` # method of the class if field.init: data_from_extra = extra.get(python_name, None) data_from_model = ( getattr(model_instance, python_name, None) if model_instance else None ) kwargs[python_name] = _convert_from_pydantic_to_strawberry_type( field.type, data_from_model, extra=data_from_extra ) return cls(**kwargs) def convert_strawberry_class_to_pydantic_model(obj: type) -> Any: if hasattr(obj, "to_pydantic"): return obj.to_pydantic() if dataclasses.is_dataclass(obj): result = [] for f in dataclasses.fields(obj): value = convert_strawberry_class_to_pydantic_model(getattr(obj, f.name)) result.append((f.name, value)) return dict(result) if isinstance(obj, (list, tuple)): # Assume we can create an object of this type by passing in a # generator (which is not true for namedtuples, not supported). return type(obj)(convert_strawberry_class_to_pydantic_model(v) for v in obj) if isinstance(obj, dict): return type(obj)( ( convert_strawberry_class_to_pydantic_model(k), convert_strawberry_class_to_pydantic_model(v), ) for k, v in obj.items() ) return copy.deepcopy(obj) strawberry-graphql-0.287.0/strawberry/experimental/pydantic/conversion_types.py000066400000000000000000000016201511033167500302300ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any, TypeVar from typing_extensions import Protocol from pydantic import BaseModel if TYPE_CHECKING: from strawberry.types.base import StrawberryObjectDefinition PydanticModel = TypeVar("PydanticModel", bound=BaseModel) class StrawberryTypeFromPydantic(Protocol[PydanticModel]): """This class does not exist in runtime. It only makes the methods below visible for IDEs. """ def __init__(self, **kwargs: Any) -> None: ... @staticmethod def from_pydantic( instance: PydanticModel, extra: dict[str, Any] | None = None ) -> StrawberryTypeFromPydantic[PydanticModel]: ... def to_pydantic(self, **kwargs: Any) -> PydanticModel: ... @property def __strawberry_definition__(self) -> StrawberryObjectDefinition: ... @property def _pydantic_type(self) -> type[PydanticModel]: ... strawberry-graphql-0.287.0/strawberry/experimental/pydantic/error_type.py000066400000000000000000000107161511033167500270170ustar00rootroot00000000000000from __future__ import annotations import dataclasses import warnings from typing import ( TYPE_CHECKING, Any, Optional, cast, ) from pydantic import BaseModel from strawberry.experimental.pydantic._compat import ( CompatModelField, PydanticCompat, lenient_issubclass, ) from strawberry.experimental.pydantic.utils import ( get_private_fields, get_strawberry_type_from_model, normalize_type, ) from strawberry.types.auto import StrawberryAuto from strawberry.types.object_type import _process_type, _wrap_dataclass from strawberry.types.type_resolver import _get_fields from strawberry.utils.typing import get_list_annotation, is_list from .exceptions import MissingFieldsListError if TYPE_CHECKING: from collections.abc import Callable, Sequence from strawberry.types.base import WithStrawberryObjectDefinition def get_type_for_field(field: CompatModelField) -> type[None | list] | Any: type_ = field.outer_type_ type_ = normalize_type(type_) return field_type_to_type(type_) def field_type_to_type(type_: type) -> Any | list[Any] | None: error_class: Any = str strawberry_type: Any = error_class if is_list(type_): child_type = get_list_annotation(type_) if is_list(child_type): strawberry_type = field_type_to_type(child_type) elif lenient_issubclass(child_type, BaseModel): strawberry_type = get_strawberry_type_from_model(child_type) else: strawberry_type = list[error_class] strawberry_type = Optional[strawberry_type] # noqa: UP045 elif lenient_issubclass(type_, BaseModel): strawberry_type = get_strawberry_type_from_model(type_) return Optional[strawberry_type] # noqa: UP045 return Optional[list[strawberry_type]] # noqa: UP045 def error_type( model: type[BaseModel], *, fields: list[str] | None = None, name: str | None = None, description: str | None = None, directives: Sequence[object] | None = (), all_fields: bool = False, ) -> Callable[..., type]: def wrap(cls: type) -> type: compat = PydanticCompat.from_model(model) model_fields = compat.get_model_fields(model) fields_set = set(fields) if fields else set() if fields: warnings.warn( "`fields` is deprecated, use `auto` type annotations instead", DeprecationWarning, stacklevel=2, ) existing_fields = getattr(cls, "__annotations__", {}) auto_fields_set = { name for name, type_ in existing_fields.items() if isinstance(type_, StrawberryAuto) } fields_set |= auto_fields_set if all_fields: if fields_set: warnings.warn( "Using all_fields overrides any explicitly defined fields " "in the model, using both is likely a bug", stacklevel=2, ) fields_set = set(model_fields.keys()) if not fields_set: raise MissingFieldsListError(cls) all_model_fields: list[tuple[str, Any, dataclasses.Field]] = [ ( name, get_type_for_field(field), dataclasses.field(default=None), # type: ignore[arg-type] ) for name, field in model_fields.items() if name in fields_set ] wrapped: type[WithStrawberryObjectDefinition] = _wrap_dataclass(cls) extra_fields = cast("list[dataclasses.Field]", _get_fields(wrapped, {})) private_fields = get_private_fields(wrapped) all_model_fields.extend( ( field.name, field.type, field, ) for field in extra_fields + private_fields if ( field.name not in auto_fields_set and not isinstance(field.type, StrawberryAuto) ) ) cls = dataclasses.make_dataclass( cls.__name__, all_model_fields, bases=cls.__bases__, ) _process_type( cls, name=name, is_input=False, is_interface=False, description=description, directives=directives, ) model._strawberry_type = cls # type: ignore[attr-defined] cls._pydantic_type = model # type: ignore[attr-defined] return cls return wrap strawberry-graphql-0.287.0/strawberry/experimental/pydantic/exceptions.py000066400000000000000000000027161511033167500270070ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from pydantic import BaseModel from pydantic.typing import NoArgAnyCallable class MissingFieldsListError(Exception): def __init__(self, type: type[BaseModel]) -> None: message = ( f"List of fields to copy from {type} is empty. Add fields with the " f"`auto` type annotation" ) super().__init__(message) class UnsupportedTypeError(Exception): pass class UnregisteredTypeException(Exception): def __init__(self, type: type[BaseModel]) -> None: message = ( f"Cannot find a Strawberry Type for {type} did you forget to register it?" ) super().__init__(message) class BothDefaultAndDefaultFactoryDefinedError(Exception): def __init__(self, default: Any, default_factory: NoArgAnyCallable) -> None: message = ( f"Not allowed to specify both default and default_factory. " f"default:{default} default_factory:{default_factory}" ) super().__init__(message) class AutoFieldsNotInBaseModelError(Exception): def __init__( self, fields: list[str], cls_name: str, model: type[BaseModel], ) -> None: message = ( f"{cls_name} defines {fields} with strawberry.auto. " f"Field(s) not present in {model.__name__} BaseModel." ) super().__init__(message) strawberry-graphql-0.287.0/strawberry/experimental/pydantic/fields.py000066400000000000000000000041311511033167500260650ustar00rootroot00000000000000import builtins from types import UnionType from typing import ( Annotated, Any, Union, ) from typing import GenericAlias as TypingGenericAlias # type: ignore from pydantic import BaseModel from strawberry.experimental.pydantic._compat import ( PydanticCompat, get_args, get_origin, lenient_issubclass, ) from strawberry.experimental.pydantic.exceptions import ( UnregisteredTypeException, ) from strawberry.types.base import StrawberryObjectDefinition def replace_pydantic_types(type_: Any, is_input: bool) -> Any: if lenient_issubclass(type_, BaseModel): attr = "_strawberry_input_type" if is_input else "_strawberry_type" if hasattr(type_, attr): return getattr(type_, attr) raise UnregisteredTypeException(type_) return type_ def replace_types_recursively( type_: Any, is_input: bool, compat: PydanticCompat ) -> Any: """Runs the conversions recursively into the arguments of generic types if any.""" basic_type = compat.get_basic_type(type_) replaced_type = replace_pydantic_types(basic_type, is_input) origin = get_origin(type_) if not origin or not hasattr(type_, "__args__"): return replaced_type converted = tuple( replace_types_recursively(t, is_input=is_input, compat=compat) for t in get_args(replaced_type) ) if isinstance(replaced_type, TypingGenericAlias): return TypingGenericAlias(origin, converted) if isinstance(replaced_type, UnionType): return Union[converted] # noqa: UP007 # TODO: investigate if we could move the check for annotated to the top if origin is Annotated and converted: converted = (converted[0],) replaced_type = replaced_type.copy_with(converted) if isinstance(replaced_type, StrawberryObjectDefinition): # TODO: Not sure if this is necessary. No coverage in tests # TODO: Unnecessary with StrawberryObject replaced_type = builtins.type( replaced_type.name, (), {"__strawberry_definition__": replaced_type}, ) return replaced_type strawberry-graphql-0.287.0/strawberry/experimental/pydantic/object_type.py000066400000000000000000000305321511033167500271320ustar00rootroot00000000000000from __future__ import annotations import dataclasses import sys import warnings from typing import ( TYPE_CHECKING, Any, Optional, cast, ) from strawberry.annotation import StrawberryAnnotation from strawberry.experimental.pydantic._compat import ( CompatModelField, PydanticCompat, ) from strawberry.experimental.pydantic.conversion import ( convert_pydantic_model_to_strawberry_class, convert_strawberry_class_to_pydantic_model, ) from strawberry.experimental.pydantic.exceptions import MissingFieldsListError from strawberry.experimental.pydantic.fields import replace_types_recursively from strawberry.experimental.pydantic.utils import ( DataclassCreationFields, ensure_all_auto_fields_in_pydantic, get_default_factory_for_field, get_private_fields, ) from strawberry.types.auto import StrawberryAuto from strawberry.types.cast import get_strawberry_type_cast from strawberry.types.field import StrawberryField from strawberry.types.object_type import _process_type, _wrap_dataclass from strawberry.types.type_resolver import _get_fields if TYPE_CHECKING: import builtins from collections.abc import Callable, Sequence from graphql import GraphQLResolveInfo def get_type_for_field(field: CompatModelField, is_input: bool, compat: PydanticCompat): # noqa: ANN201 outer_type = field.outer_type_ replaced_type = replace_types_recursively(outer_type, is_input, compat=compat) if field.is_v1: # only pydantic v1 has this Optional logic should_add_optional: bool = field.allow_none if should_add_optional: return Optional[replaced_type] # noqa: UP045 return replaced_type def _build_dataclass_creation_fields( field: CompatModelField, is_input: bool, existing_fields: dict[str, StrawberryField], auto_fields_set: set[str], use_pydantic_alias: bool, compat: PydanticCompat, ) -> DataclassCreationFields: field_type = ( get_type_for_field(field, is_input, compat=compat) if field.name in auto_fields_set else existing_fields[field.name].type ) if ( field.name in existing_fields and existing_fields[field.name].base_resolver is not None ): # if the user has defined a resolver for this field, always use it strawberry_field = existing_fields[field.name] else: # otherwise we build an appropriate strawberry field that resolves it existing_field = existing_fields.get(field.name) graphql_name = None if existing_field and existing_field.graphql_name: graphql_name = existing_field.graphql_name elif field.has_alias and use_pydantic_alias: graphql_name = field.alias strawberry_field = StrawberryField( python_name=field.name, graphql_name=graphql_name, # always unset because we use default_factory instead default=dataclasses.MISSING, default_factory=get_default_factory_for_field(field, compat=compat), type_annotation=StrawberryAnnotation.from_annotation(field_type), description=field.description, deprecation_reason=( existing_field.deprecation_reason if existing_field else None ), permission_classes=( existing_field.permission_classes if existing_field else [] ), directives=existing_field.directives if existing_field else (), metadata=existing_field.metadata if existing_field else {}, ) return DataclassCreationFields( name=field.name, field_type=field_type, # type: ignore field=strawberry_field, ) if TYPE_CHECKING: from strawberry.experimental.pydantic.conversion_types import ( PydanticModel, StrawberryTypeFromPydantic, ) def type( model: builtins.type[PydanticModel], *, fields: list[str] | None = None, name: str | None = None, is_input: bool = False, is_interface: bool = False, description: str | None = None, directives: Sequence[object] | None = (), all_fields: bool = False, include_computed: bool = False, use_pydantic_alias: bool = True, ) -> Callable[..., builtins.type[StrawberryTypeFromPydantic[PydanticModel]]]: def wrap(cls: Any) -> builtins.type[StrawberryTypeFromPydantic[PydanticModel]]: compat = PydanticCompat.from_model(model) model_fields = compat.get_model_fields(model, include_computed=include_computed) original_fields_set = set(fields) if fields else set() if fields: warnings.warn( "`fields` is deprecated, use `auto` type annotations instead", DeprecationWarning, stacklevel=2, ) existing_fields = getattr(cls, "__annotations__", {}) # these are the fields that matched a field name in the pydantic model # and should copy their alias from the pydantic model fields_set = original_fields_set.union( {name for name, _ in existing_fields.items() if name in model_fields} ) # these are the fields that were marked with strawberry.auto and # should copy their type from the pydantic model auto_fields_set = original_fields_set.union( { name for name, type_ in existing_fields.items() if isinstance(type_, StrawberryAuto) } ) if all_fields: if fields_set: warnings.warn( "Using all_fields overrides any explicitly defined fields " "in the model, using both is likely a bug", stacklevel=2, ) fields_set = set(model_fields.keys()) auto_fields_set = set(model_fields.keys()) if not fields_set: raise MissingFieldsListError(cls) ensure_all_auto_fields_in_pydantic( model=model, auto_fields=auto_fields_set, cls_name=cls.__name__, include_computed=include_computed, ) wrapped = _wrap_dataclass(cls) extra_strawberry_fields = _get_fields(wrapped, {}) extra_fields = cast("list[dataclasses.Field]", extra_strawberry_fields) private_fields = get_private_fields(wrapped) extra_fields_dict = {field.name: field for field in extra_strawberry_fields} all_model_fields: list[DataclassCreationFields] = [ _build_dataclass_creation_fields( field, is_input, extra_fields_dict, auto_fields_set, use_pydantic_alias, compat=compat, ) for field_name, field in model_fields.items() if field_name in fields_set ] all_model_fields = [ DataclassCreationFields( name=field.name, field_type=field.type, # type: ignore field=field, ) for field in extra_fields + private_fields if field.name not in fields_set ] + all_model_fields # Implicitly define `is_type_of` to support interfaces/unions that use # pydantic objects (not the corresponding strawberry type) @classmethod # type: ignore def is_type_of(cls: builtins.type, obj: Any, _info: GraphQLResolveInfo) -> bool: if (type_cast := get_strawberry_type_cast(obj)) is not None: return type_cast is cls return isinstance(obj, (cls, model)) namespace = {"is_type_of": is_type_of} # We need to tell the difference between a from_pydantic method that is # inherited from a base class and one that is defined by the user in the # decorated class. We want to override the method only if it is # inherited. To tell the difference, we compare the class name to the # fully qualified name of the method, which will end in .from_pydantic has_custom_from_pydantic = hasattr( cls, "from_pydantic" ) and cls.from_pydantic.__qualname__.endswith(f"{cls.__name__}.from_pydantic") has_custom_to_pydantic = hasattr( cls, "to_pydantic" ) and cls.to_pydantic.__qualname__.endswith(f"{cls.__name__}.to_pydantic") if has_custom_from_pydantic: namespace["from_pydantic"] = cls.from_pydantic if has_custom_to_pydantic: namespace["to_pydantic"] = cls.to_pydantic if hasattr(cls, "resolve_reference"): namespace["resolve_reference"] = cls.resolve_reference kwargs: dict[str, object] = {} # Python 3.10.1 introduces the kw_only param to `make_dataclass`. # If we're on an older version then generate our own custom init function # Note: Python 3.10.0 added the `kw_only` param to dataclasses, it was # just missed from the `make_dataclass` function: # https://github.com/python/cpython/issues/89961 if sys.version_info >= (3, 10, 1): kwargs["kw_only"] = dataclasses.MISSING cls = dataclasses.make_dataclass( cls.__name__, [field.to_tuple() for field in all_model_fields], bases=cls.__bases__, namespace=namespace, **kwargs, # type: ignore ) _process_type( cls, name=name, is_input=is_input, is_interface=is_interface, description=description, directives=directives, ) if is_input: model._strawberry_input_type = cls # type: ignore else: model._strawberry_type = cls # type: ignore cls._pydantic_type = model def from_pydantic_default( instance: PydanticModel, extra: dict[str, Any] | None = None ) -> StrawberryTypeFromPydantic[PydanticModel]: ret = convert_pydantic_model_to_strawberry_class( cls=cls, model_instance=instance, extra=extra ) ret._original_model = instance return ret def to_pydantic_default(self: Any, **kwargs: Any) -> PydanticModel: instance_kwargs = { f.name: convert_strawberry_class_to_pydantic_model( getattr(self, f.name) ) for f in dataclasses.fields(self) } instance_kwargs.update(kwargs) return model(**instance_kwargs) if not has_custom_from_pydantic: cls.from_pydantic = staticmethod(from_pydantic_default) if not has_custom_to_pydantic: cls.to_pydantic = to_pydantic_default return cls return wrap def input( model: builtins.type[PydanticModel], *, fields: list[str] | None = None, name: str | None = None, is_interface: bool = False, description: str | None = None, directives: Sequence[object] | None = (), all_fields: bool = False, use_pydantic_alias: bool = True, ) -> Callable[..., builtins.type[StrawberryTypeFromPydantic[PydanticModel]]]: """Convenience decorator for creating an input type from a Pydantic model. Equal to `partial(type, is_input=True)` See https://github.com/strawberry-graphql/strawberry/issues/1830. """ return type( model=model, fields=fields, name=name, is_input=True, is_interface=is_interface, description=description, directives=directives, all_fields=all_fields, use_pydantic_alias=use_pydantic_alias, ) def interface( model: builtins.type[PydanticModel], *, fields: list[str] | None = None, name: str | None = None, is_input: bool = False, description: str | None = None, directives: Sequence[object] | None = (), all_fields: bool = False, use_pydantic_alias: bool = True, ) -> Callable[..., builtins.type[StrawberryTypeFromPydantic[PydanticModel]]]: """Convenience decorator for creating an interface type from a Pydantic model. Equal to `partial(type, is_interface=True)` See https://github.com/strawberry-graphql/strawberry/issues/1830. """ return type( model=model, fields=fields, name=name, is_input=is_input, is_interface=True, description=description, directives=directives, all_fields=all_fields, use_pydantic_alias=use_pydantic_alias, ) strawberry-graphql-0.287.0/strawberry/experimental/pydantic/utils.py000066400000000000000000000076621511033167500257730ustar00rootroot00000000000000from __future__ import annotations import dataclasses from typing import ( TYPE_CHECKING, Any, NamedTuple, cast, ) from pydantic import BaseModel from strawberry.experimental.pydantic._compat import ( CompatModelField, PydanticCompat, smart_deepcopy, ) from strawberry.experimental.pydantic.exceptions import ( AutoFieldsNotInBaseModelError, BothDefaultAndDefaultFactoryDefinedError, UnregisteredTypeException, ) from strawberry.types.private import is_private from strawberry.types.unset import UNSET from strawberry.utils.typing import ( get_list_annotation, get_optional_annotation, is_list, is_optional, ) if TYPE_CHECKING: from pydantic.typing import NoArgAnyCallable def normalize_type(type_: type) -> Any: if is_list(type_): return list[normalize_type(get_list_annotation(type_))] # type: ignore if is_optional(type_): return get_optional_annotation(type_) return type_ def get_strawberry_type_from_model(type_: Any) -> Any: if hasattr(type_, "_strawberry_type"): return type_._strawberry_type raise UnregisteredTypeException(type_) def get_private_fields(cls: type) -> list[dataclasses.Field]: return [field for field in dataclasses.fields(cls) if is_private(field.type)] class DataclassCreationFields(NamedTuple): """Fields required for the fields parameter of make_dataclass.""" name: str field_type: type field: dataclasses.Field def to_tuple(self) -> tuple[str, type, dataclasses.Field]: # fields parameter wants (name, type, Field) return self.name, self.field_type, self.field def get_default_factory_for_field( field: CompatModelField, compat: PydanticCompat, ) -> NoArgAnyCallable | dataclasses._MISSING_TYPE: """Gets the default factory for a pydantic field. Handles mutable defaults when making the dataclass by using pydantic's smart_deepcopy Returns optionally a NoArgAnyCallable representing a default_factory parameter """ # replace dataclasses.MISSING with our own UNSET to make comparisons easier default_factory = field.default_factory if field.has_default_factory else UNSET default = field.default if field.has_default else UNSET has_factory = default_factory is not None and default_factory is not UNSET has_default = default is not None and default is not UNSET # defining both default and default_factory is not supported if has_factory and has_default: default_factory = cast("NoArgAnyCallable", default_factory) raise BothDefaultAndDefaultFactoryDefinedError( default=default, default_factory=default_factory ) # if we have a default_factory, we should return it if has_factory: return cast("NoArgAnyCallable", default_factory) # if we have a default, we should return it if has_default: # if the default value is a pydantic base model # we should return the serialized version of that default for # printing the value. if isinstance(default, BaseModel): return lambda: compat.model_dump(default) return lambda: smart_deepcopy(default) # if we don't have default or default_factory, but the field is not required, # we should return a factory that returns None if not field.required: return lambda: None return dataclasses.MISSING def ensure_all_auto_fields_in_pydantic( model: type[BaseModel], auto_fields: set[str], cls_name: str, include_computed: bool = False, ) -> None: compat = PydanticCompat.from_model(model) # Raise error if user defined a strawberry.auto field not present in the model non_existing_fields = list( auto_fields - compat.get_model_fields(model, include_computed=include_computed).keys() ) if non_existing_fields: raise AutoFieldsNotInBaseModelError( fields=non_existing_fields, cls_name=cls_name, model=model ) strawberry-graphql-0.287.0/strawberry/ext/000077500000000000000000000000001511033167500205365ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/ext/LICENSE000066400000000000000000000023411511033167500215430ustar00rootroot00000000000000Strawberry has a copy of the Mypy dataclasses plugin. Mypy (and mypyc) are licensed under the terms of the MIT license, reproduced below. = = = = = The MIT License Copyright (c) 2015-2019 Jukka Lehtosalo and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. strawberry-graphql-0.287.0/strawberry/ext/__init__.py000066400000000000000000000000001511033167500226350ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/ext/dataclasses/000077500000000000000000000000001511033167500230255ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/ext/dataclasses/LICENSE000066400000000000000000000331451511033167500240400ustar00rootroot00000000000000A. HISTORY OF THE SOFTWARE ========================== Python was created in the early 1990s by Guido van Rossum at Stichting Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands as a successor of a language called ABC. Guido remains Python's principal author, although it includes many contributions from others. In 1995, Guido continued his work on Python at the Corporation for National Research Initiatives (CNRI, see http://www.cnri.reston.va.us) in Reston, Virginia where he released several versions of the software. In May 2000, Guido and the Python core development team moved to BeOpen.com to form the BeOpen PythonLabs team. In October of the same year, the PythonLabs team moved to Digital Creations, which became Zope Corporation. In 2001, the Python Software Foundation (PSF, see https://www.python.org/psf/) was formed, a non-profit organization created specifically to own Python-related Intellectual Property. Zope Corporation was a sponsoring member of the PSF. All Python releases are Open Source (see http://www.opensource.org for the Open Source Definition). Historically, most, but not all, Python releases have also been GPL-compatible; the table below summarizes the various releases. Release Derived Year Owner GPL- from compatible? (1) 0.9.0 thru 1.2 1991-1995 CWI yes 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes 1.6 1.5.2 2000 CNRI no 2.0 1.6 2000 BeOpen.com no 1.6.1 1.6 2001 CNRI yes (2) 2.1 2.0+1.6.1 2001 PSF no 2.0.1 2.0+1.6.1 2001 PSF yes 2.1.1 2.1+2.0.1 2001 PSF yes 2.1.2 2.1.1 2002 PSF yes 2.1.3 2.1.2 2002 PSF yes 2.2 and above 2.1.1 2001-now PSF yes Footnotes: (1) GPL-compatible doesn't mean that we're distributing Python under the GPL. All Python licenses, unlike the GPL, let you distribute a modified version without making your changes open source. The GPL-compatible licenses make it possible to combine Python with other software that is released under the GPL; the others don't. (2) According to Richard Stallman, 1.6.1 is not GPL-compatible, because its license has a choice of law clause. According to CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1 is "not incompatible" with the GPL. Thanks to the many outside volunteers who have worked under Guido's direction to make these releases possible. B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON =============================================================== Python software and documentation are licensed under the Python Software Foundation License Version 2. Starting with Python 3.8.6, examples, recipes, and other code in the documentation are dual licensed under the PSF License Version 2 and the Zero-Clause BSD license. Some software incorporated into Python is under different licenses. The licenses are listed with code falling under that license. PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 -------------------------------------------- 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and the Individual or Organization ("Licensee") accessing and otherwise using this software ("Python") in source or binary form and its associated documentation. 2. Subject to the terms and conditions of this License Agreement, PSF hereby grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative works, distribute, and otherwise use Python alone or in any derivative version, provided, however, that PSF's License Agreement and PSF's notice of copyright, i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021 Python Software Foundation; All Rights Reserved" are retained in Python alone or in any derivative version prepared by Licensee. 3. In the event Licensee prepares a derivative work that is based on or incorporates Python or any part thereof, and wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in any such work a brief summary of the changes made to Python. 4. PSF is making Python available to Licensee on an "AS IS" basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions. 7. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint venture between PSF and Licensee. This License Agreement does not grant permission to use PSF trademarks or trade name in a trademark sense to endorse or promote products or services of Licensee, or any third party. 8. By copying, installing or otherwise using Python, Licensee agrees to be bound by the terms and conditions of this License Agreement. BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 ------------------------------------------- BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the Individual or Organization ("Licensee") accessing and otherwise using this software in source or binary form and its associated documentation ("the Software"). 2. Subject to the terms and conditions of this BeOpen Python License Agreement, BeOpen hereby grants Licensee a non-exclusive, royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative works, distribute, and otherwise use the Software alone or in any derivative version, provided, however, that the BeOpen Python License is retained in the Software, alone or in any derivative version prepared by Licensee. 3. BeOpen is making the Software available to Licensee on an "AS IS" basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. 4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. 5. This License Agreement will automatically terminate upon a material breach of its terms and conditions. 6. This License Agreement shall be governed by and interpreted in all respects by the law of the State of California, excluding conflict of law provisions. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint venture between BeOpen and Licensee. This License Agreement does not grant permission to use BeOpen trademarks or trade names in a trademark sense to endorse or promote products or services of Licensee, or any third party. As an exception, the "BeOpen Python" logos available at http://www.pythonlabs.com/logos.html may be used according to the permissions granted on that web page. 7. By copying, installing or otherwise using the software, Licensee agrees to be bound by the terms and conditions of this License Agreement. CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 --------------------------------------- 1. This LICENSE AGREEMENT is between the Corporation for National Research Initiatives, having an office at 1895 Preston White Drive, Reston, VA 20191 ("CNRI"), and the Individual or Organization ("Licensee") accessing and otherwise using Python 1.6.1 software in source or binary form and its associated documentation. 2. Subject to the terms and conditions of this License Agreement, CNRI hereby grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative works, distribute, and otherwise use Python 1.6.1 alone or in any derivative version, provided, however, that CNRI's License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) 1995-2001 Corporation for National Research Initiatives; All Rights Reserved" are retained in Python 1.6.1 alone or in any derivative version prepared by Licensee. Alternately, in lieu of CNRI's License Agreement, Licensee may substitute the following text (omitting the quotes): "Python 1.6.1 is made available subject to the terms and conditions in CNRI's License Agreement. This Agreement together with Python 1.6.1 may be located on the Internet using the following unique, persistent identifier (known as a handle): 1895.22/1013. This Agreement may also be obtained from a proxy server on the Internet using the following URL: http://hdl.handle.net/1895.22/1013". 3. In the event Licensee prepares a derivative work that is based on or incorporates Python 1.6.1 or any part thereof, and wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in any such work a brief summary of the changes made to Python 1.6.1. 4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. 5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON 1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions. 7. This License Agreement shall be governed by the federal intellectual property law of the United States, including without limitation the federal copyright law, and, to the extent such U.S. federal law does not apply, by the law of the Commonwealth of Virginia, excluding Virginia's conflict of law provisions. Notwithstanding the foregoing, with regard to derivative works based on Python 1.6.1 that incorporate non-separable material that was previously distributed under the GNU General Public License (GPL), the law of the Commonwealth of Virginia shall govern this License Agreement only as to issues arising under or with respect to Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint venture between CNRI and Licensee. This License Agreement does not grant permission to use CNRI trademarks or trade name in a trademark sense to endorse or promote products or services of Licensee, or any third party. 8. By clicking on the "ACCEPT" button where indicated, or by copying, installing or otherwise using Python 1.6.1, Licensee agrees to be bound by the terms and conditions of this License Agreement. ACCEPT CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 -------------------------------------------------- Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, The Netherlands. All rights reserved. Permission to use, copy, modify, and distribute this software and its documentation for any purpose and without fee is hereby granted, provided that the above copyright notice appear in all copies and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of Stichting Mathematisch Centrum or CWI not be used in advertising or publicity pertaining to distribution of the software without specific, written prior permission. STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON DOCUMENTATION ---------------------------------------------------------------------- Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. strawberry-graphql-0.287.0/strawberry/ext/dataclasses/README.md000066400000000000000000000025551511033167500243130ustar00rootroot00000000000000# Additional information This folder contains file(s) or code that is originally taken from other projects and got further adaptations by the maintainers of Strawberry. ## `dataclasses.py` The file [dataclasses.py](https://github.com/strawberry-graphql/strawberry/tree/main/strawberry/ext/dataclasses/dataclasses.py) which is based on https://github.com/python/cpython/blob/v3.9.6/Lib/dataclasses.py#L489-L536 but has got some small adjustments in the adopted function `dataclass_init_fn()` so the functionality is fitting the desired requirements within Strawberry. From the docstring of `dataclass_init_fn()`: ``` """ Create an __init__ function for a dataclass. We create a custom __init__ function for the dataclasses that back Strawberry object types to only accept keyword arguments. This allows us to avoid the problem where a type cannot define a field with a default value before a field that doesn't have a default value. An example of the problem: https://stackoverflow.com/questions/51575931/class-inheritance-in-python-3-7-dataclasses Code is adapted from: https://github.com/python/cpython/blob/v3.9.6/Lib/dataclasses.py#L489-L536 Note: in Python 3.10 and above we use the `kw_only` argument to achieve the same result. """ ``` The file was added so kwargs could be enforced on Python classes within Strawberry. See also the file LICENSE for copyright information. strawberry-graphql-0.287.0/strawberry/ext/dataclasses/__init__.py000066400000000000000000000000001511033167500251240ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/ext/dataclasses/dataclasses.py000066400000000000000000000043571511033167500256770ustar00rootroot00000000000000# # This code is licensed under the Python Software Foundation License Version 2 # from dataclasses import ( # type: ignore _FIELD_INITVAR, _HAS_DEFAULT_FACTORY, _POST_INIT_NAME, MISSING, _create_fn, _field_init, _init_param, ) from typing import Any def dataclass_init_fn( fields: list[Any], frozen: bool, has_post_init: bool, self_name: str, globals_: dict[str, Any], ) -> Any: """Create an __init__ function for a dataclass. We create a custom __init__ function for the dataclasses that back Strawberry object types to only accept keyword arguments. This allows us to avoid the problem where a type cannot define a field with a default value before a field that doesn't have a default value. An example of the problem: https://stackoverflow.com/questions/51575931/class-inheritance-in-python-3-7-dataclasses Code is adapted from: https://github.com/python/cpython/blob/v3.9.6/Lib/dataclasses.py#L489-L536 Note: in Python 3.10 and above we use the `kw_only` argument to achieve the same result. """ # fields contains both real fields and InitVar pseudo-fields. locals_ = {f"_type_{f.name}": f.type for f in fields} locals_.update( { "MISSING": MISSING, "_HAS_DEFAULT_FACTORY": _HAS_DEFAULT_FACTORY, } ) body_lines = [] for f in fields: line = _field_init(f, frozen, locals_, self_name) # line is None means that this field doesn't require # initialization (it's a pseudo-field). Just skip it. if line: body_lines.append(line) # Does this class have a post-init function? if has_post_init: params_str = ",".join(f.name for f in fields if f._field_type is _FIELD_INITVAR) body_lines.append(f"{self_name}.{_POST_INIT_NAME}({params_str})") # If no body lines, use 'pass'. if not body_lines: body_lines = ["pass"] _init_params = [_init_param(f) for f in fields if f.init] if len(_init_params) > 0: _init_params = ["*", *_init_params] return _create_fn( "__init__", [self_name, *_init_params], body_lines, locals=locals_, globals=globals_, return_type=None, ) strawberry-graphql-0.287.0/strawberry/ext/mypy_plugin.py000066400000000000000000000477261511033167500235040ustar00rootroot00000000000000from __future__ import annotations import re import warnings from decimal import Decimal from typing import ( TYPE_CHECKING, Any, cast, ) from mypy.nodes import ( ARG_OPT, ARG_STAR2, GDEF, MDEF, Argument, AssignmentStmt, Block, CallExpr, CastExpr, FuncDef, IndexExpr, MemberExpr, NameExpr, PassStmt, SymbolTableNode, TupleExpr, TypeAlias, Var, ) from mypy.plugin import ( Plugin, SemanticAnalyzerPluginInterface, ) from mypy.plugins.common import _get_argument, add_method from mypy.semanal_shared import set_callable_name from mypy.types import ( AnyType, CallableType, Instance, NoneType, TypeOfAny, TypeVarType, UnionType, ) from mypy.typevars import fill_typevars from mypy.util import get_unique_redefinition_name # Backwards compatible with the removal of `TypeVarDef` in mypy 0.920. try: from mypy.types import TypeVarDef # type: ignore except ImportError: TypeVarDef = TypeVarType PYDANTIC_VERSION: tuple[int, ...] | None = None # To be compatible with user who don't use pydantic try: import pydantic from pydantic.mypy import METADATA_KEY as PYDANTIC_METADATA_KEY from pydantic.mypy import PydanticModelField PYDANTIC_VERSION = tuple(map(int, pydantic.__version__.split("."))) # noqa: RUF048 from strawberry.experimental.pydantic._compat import IS_PYDANTIC_V1 except ImportError: PYDANTIC_METADATA_KEY = "" IS_PYDANTIC_V1 = False if TYPE_CHECKING: from collections.abc import Callable from mypy.nodes import ClassDef, Expression from mypy.plugins import ( # type: ignore AnalyzeTypeContext, CheckerPluginInterface, ClassDefContext, DynamicClassDefContext, ) from mypy.types import Type VERSION_RE = re.compile(r"(^0|^(?:[1-9][0-9]*))\.(0|(?:[1-9][0-9]*))") FALLBACK_VERSION = Decimal("0.800") class MypyVersion: """Stores the mypy version to be used by the plugin.""" VERSION: Decimal class InvalidNodeTypeException(Exception): def __init__(self, node: Any) -> None: self.message = f"Invalid node type: {node!s}" super().__init__() def __str__(self) -> str: return self.message def lazy_type_analyze_callback(ctx: AnalyzeTypeContext) -> Type: if len(ctx.type.args) == 0: # TODO: maybe this should throw an error return AnyType(TypeOfAny.special_form) type_name = ctx.type.args[0] return ctx.api.analyze_type(type_name) def _get_named_type(name: str, api: SemanticAnalyzerPluginInterface) -> Any: if "." in name: return api.named_type_or_none(name) return api.named_type(name) def _get_type_for_expr(expr: Expression, api: SemanticAnalyzerPluginInterface) -> Type: if isinstance(expr, NameExpr): # guarding against invalid nodes, still have to figure out why this happens # but sometimes mypy crashes because the internal node of the named type # is actually a Var node, which is unexpected, so we do a naive guard here # and raise an exception for it. if expr.fullname: sym = api.lookup_fully_qualified_or_none(expr.fullname) if sym and isinstance(sym.node, Var): raise InvalidNodeTypeException(sym.node) return _get_named_type(expr.fullname or expr.name, api) if isinstance(expr, IndexExpr): type_ = _get_type_for_expr(expr.base, api) type_.args = (_get_type_for_expr(expr.index, api),) # type: ignore return type_ if isinstance(expr, MemberExpr): if expr.fullname: return _get_named_type(expr.fullname, api) raise InvalidNodeTypeException(expr) if isinstance(expr, CallExpr): if expr.analyzed: return _get_type_for_expr(expr.analyzed, api) raise InvalidNodeTypeException(expr) if isinstance(expr, CastExpr): return expr.type raise ValueError(f"Unsupported expression {type(expr)}") def create_type_hook(ctx: DynamicClassDefContext) -> None: # returning classes/type aliases is not supported yet by mypy # see https://github.com/python/mypy/issues/5865 type_alias = TypeAlias( AnyType(TypeOfAny.from_error), fullname=ctx.api.qualified_name(ctx.name), line=ctx.call.line, column=ctx.call.column, ) ctx.api.add_symbol_table_node( ctx.name, SymbolTableNode(GDEF, type_alias, plugin_generated=True), ) def union_hook(ctx: DynamicClassDefContext) -> None: try: # Check if types is passed as a keyword argument types = ctx.call.args[ctx.call.arg_names.index("types")] except ValueError: # Fall back to assuming position arguments types = ctx.call.args[1] if isinstance(types, TupleExpr): try: type_ = UnionType( tuple(_get_type_for_expr(x, ctx.api) for x in types.items) ) except InvalidNodeTypeException: type_alias = TypeAlias( AnyType(TypeOfAny.from_error), fullname=ctx.api.qualified_name(ctx.name), line=ctx.call.line, column=ctx.call.column, ) ctx.api.add_symbol_table_node( ctx.name, SymbolTableNode(GDEF, type_alias, plugin_generated=False), ) return type_alias = TypeAlias( type_, fullname=ctx.api.qualified_name(ctx.name), line=ctx.call.line, column=ctx.call.column, ) ctx.api.add_symbol_table_node( ctx.name, SymbolTableNode(GDEF, type_alias, plugin_generated=False) ) def enum_hook(ctx: DynamicClassDefContext) -> None: first_argument = ctx.call.args[0] if isinstance(first_argument, NameExpr): if not first_argument.node: ctx.api.defer() return if isinstance(first_argument.node, Var): var_type = first_argument.node.type or AnyType( TypeOfAny.implementation_artifact ) type_alias = TypeAlias( var_type, fullname=ctx.api.qualified_name(ctx.name), line=ctx.call.line, column=ctx.call.column, ) ctx.api.add_symbol_table_node( ctx.name, SymbolTableNode(GDEF, type_alias, plugin_generated=False) ) return enum_type: Type | None try: enum_type = _get_type_for_expr(first_argument, ctx.api) except InvalidNodeTypeException: enum_type = None if not enum_type: enum_type = AnyType(TypeOfAny.from_error) type_alias = TypeAlias( enum_type, fullname=ctx.api.qualified_name(ctx.name), line=ctx.call.line, column=ctx.call.column, ) ctx.api.add_symbol_table_node( ctx.name, SymbolTableNode(GDEF, type_alias, plugin_generated=False) ) def scalar_hook(ctx: DynamicClassDefContext) -> None: first_argument = ctx.call.args[0] if isinstance(first_argument, NameExpr): if not first_argument.node: ctx.api.defer() return if isinstance(first_argument.node, Var): var_type = first_argument.node.type or AnyType( TypeOfAny.implementation_artifact ) type_alias = TypeAlias( var_type, fullname=ctx.api.qualified_name(ctx.name), line=ctx.call.line, column=ctx.call.column, ) ctx.api.add_symbol_table_node( ctx.name, SymbolTableNode(GDEF, type_alias, plugin_generated=False) ) return scalar_type: Type | None # TODO: add proper support for NewType try: scalar_type = _get_type_for_expr(first_argument, ctx.api) except InvalidNodeTypeException: scalar_type = None if not scalar_type: scalar_type = AnyType(TypeOfAny.from_error) type_alias = TypeAlias( scalar_type, fullname=ctx.api.qualified_name(ctx.name), line=ctx.call.line, column=ctx.call.column, ) ctx.api.add_symbol_table_node( ctx.name, SymbolTableNode(GDEF, type_alias, plugin_generated=False) ) def add_static_method_to_class( api: SemanticAnalyzerPluginInterface | CheckerPluginInterface, cls: ClassDef, name: str, args: list[Argument], return_type: Type, tvar_def: TypeVarType | None = None, ) -> None: """Adds a static method. Edited `add_method_to_class` to incorporate static method logic https://github.com/python/mypy/blob/9c05d3d19/mypy/plugins/common.py. """ info = cls.info # First remove any previously generated methods with the same name # to avoid clashes and problems in the semantic analyzer. if name in info.names: sym = info.names[name] if sym.plugin_generated and isinstance(sym.node, FuncDef): cls.defs.body.remove(sym.node) # For compat with mypy < 0.93 if Decimal("0.93") > MypyVersion.VERSION: function_type = api.named_type("__builtins__.function") elif isinstance(api, SemanticAnalyzerPluginInterface): function_type = api.named_type("builtins.function") else: function_type = api.named_generic_type("builtins.function", []) arg_types, arg_names, arg_kinds = [], [], [] for arg in args: assert arg.type_annotation, "All arguments must be fully typed." arg_types.append(arg.type_annotation) arg_names.append(arg.variable.name) arg_kinds.append(arg.kind) signature = CallableType( arg_types, arg_kinds, arg_names, return_type, function_type ) if tvar_def: signature.variables = [tvar_def] # type: ignore[assignment] func = FuncDef(name, args, Block([PassStmt()])) func.is_static = True func.info = info func.type = set_callable_name(signature, func) func._fullname = f"{info.fullname}.{name}" func.line = info.line # NOTE: we would like the plugin generated node to dominate, but we still # need to keep any existing definitions so they get semantically analyzed. if name in info.names: # Get a nice unique name instead. r_name = get_unique_redefinition_name(name, info.names) info.names[r_name] = info.names[name] info.names[name] = SymbolTableNode(MDEF, func, plugin_generated=True) info.defn.defs.body.append(func) def strawberry_pydantic_class_callback(ctx: ClassDefContext) -> None: # in future we want to have a proper pydantic plugin, but for now # let's fallback to **kwargs for __init__, some resources are here: # https://github.com/samuelcolvin/pydantic/blob/master/pydantic/mypy.py # >>> model_index = ctx.cls.decorators[0].arg_names.index("model") # >>> model_name = ctx.cls.decorators[0].args[model_index].name # >>> model_type = ctx.api.named_type("UserModel") # >>> model_type = ctx.api.lookup(model_name, Context()) model_expression = _get_argument(call=ctx.reason, name="model") if model_expression is None: ctx.api.fail("model argument in decorator failed to be parsed", ctx.reason) else: # Add __init__ init_args = [ Argument(Var("kwargs"), AnyType(TypeOfAny.explicit), None, ARG_STAR2) ] add_method(ctx, "__init__", init_args, NoneType()) model_type = cast("Instance", _get_type_for_expr(model_expression, ctx.api)) # these are the fields that the user added to the strawberry type new_strawberry_fields: set[str] = set() # TODO: think about inheritance for strawberry? for stmt in ctx.cls.defs.body: if isinstance(stmt, AssignmentStmt): lhs = cast("NameExpr", stmt.lvalues[0]) new_strawberry_fields.add(lhs.name) pydantic_fields: set[PydanticModelField] = set() try: fields = model_type.type.metadata[PYDANTIC_METADATA_KEY]["fields"] for data in fields.items(): if IS_PYDANTIC_V1: field = PydanticModelField.deserialize(ctx.cls.info, data[1]) # type:ignore[call-arg] else: field = PydanticModelField.deserialize( info=ctx.cls.info, data=data[1], api=ctx.api ) pydantic_fields.add(field) except KeyError: # this will happen if the user didn't add the pydantic plugin # AND is using the pydantic conversion decorator ctx.api.fail( "Pydantic plugin not installed," " please add pydantic.mypy your mypy.ini plugins", ctx.reason, ) potentially_missing_fields: set[PydanticModelField] = { f for f in pydantic_fields if f.name not in new_strawberry_fields } """ Need to check if all_fields=True from the pydantic decorator There is no way to real check that Literal[True] was used We just check if the strawberry type is missing all the fields This means that the user is using all_fields=True """ is_all_fields: bool = len(potentially_missing_fields) == len(pydantic_fields) missing_pydantic_fields: set[PydanticModelField] = ( potentially_missing_fields if not is_all_fields else set() ) # Add the default to_pydantic if undefined by the user if "to_pydantic" not in ctx.cls.info.names: if IS_PYDANTIC_V1: add_method( ctx, "to_pydantic", args=[ f.to_argument( # TODO: use_alias should depend on config? info=model_type.type, # type:ignore[call-arg] typed=True, force_optional=False, use_alias=True, ) for f in missing_pydantic_fields ], return_type=model_type, ) else: extra = {} if PYDANTIC_VERSION: if PYDANTIC_VERSION >= (2, 7, 0): extra["api"] = ctx.api if PYDANTIC_VERSION >= (2, 8, 0): # Based on pydantic's default value # https://github.com/pydantic/pydantic/pull/9606/files#diff-469037bbe55bbf9aa359480a16040d368c676adad736e133fb07e5e20d6ac523R1066 extra["force_typevars_invariant"] = False if PYDANTIC_VERSION >= (2, 9, 0): extra["model_strict"] = model_type.type.metadata[ PYDANTIC_METADATA_KEY ]["config"].get("strict", False) extra["is_root_model_root"] = any( "pydantic.root_model.RootModel" in base.fullname for base in model_type.type.mro[:-1] ) add_method( ctx, "to_pydantic", args=[ f.to_argument( # TODO: use_alias should depend on config? current_info=model_type.type, typed=True, force_optional=False, use_alias=True, **extra, ) for f in missing_pydantic_fields ], return_type=model_type, ) # Add from_pydantic model_argument = Argument( variable=Var(name="instance", type=model_type), type_annotation=model_type, initializer=None, kind=ARG_OPT, ) extra_type = ctx.api.named_type( "builtins.dict", [ctx.api.named_type("builtins.str"), AnyType(TypeOfAny.explicit)], ) extra_argument = Argument( variable=Var(name="extra", type=UnionType([NoneType(), extra_type])), type_annotation=UnionType([NoneType(), extra_type]), initializer=None, kind=ARG_OPT, ) add_static_method_to_class( ctx.api, ctx.cls, name="from_pydantic", args=[model_argument, extra_argument], return_type=fill_typevars(ctx.cls.info), ) class StrawberryPlugin(Plugin): def get_dynamic_class_hook( self, fullname: str ) -> Callable[[DynamicClassDefContext], None] | None: # TODO: investigate why we need this instead of `strawberry.union.union` on CI # we have the same issue in the other hooks if self._is_strawberry_union(fullname): return union_hook if self._is_strawberry_enum(fullname): return enum_hook if self._is_strawberry_scalar(fullname): return scalar_hook if self._is_strawberry_create_type(fullname): return create_type_hook return None def get_type_analyze_hook(self, fullname: str) -> Callable[..., Type] | None: if self._is_strawberry_lazy_type(fullname): return lazy_type_analyze_callback return None def get_class_decorator_hook( self, fullname: str ) -> Callable[[ClassDefContext], None] | None: if self._is_strawberry_pydantic_decorator(fullname): return strawberry_pydantic_class_callback return None def _is_strawberry_union(self, fullname: str) -> bool: return fullname == "strawberry.types.union.union" or fullname.endswith( "strawberry.union" ) def _is_strawberry_enum(self, fullname: str) -> bool: return fullname == "strawberry.types.enum.enum" or fullname.endswith( "strawberry.enum" ) def _is_strawberry_scalar(self, fullname: str) -> bool: return fullname == "strawberry.types.scalar.scalar" or fullname.endswith( "strawberry.scalar" ) def _is_strawberry_lazy_type(self, fullname: str) -> bool: return fullname == "strawberry.types.lazy_type.LazyType" def _is_strawberry_create_type(self, fullname: str) -> bool: # using endswith(.create_type) is not ideal as there might be # other function called like that, but it's the best we can do # when follow-imports is set to "skip". Hopefully in the future # we can remove our custom hook for create type return ( fullname == "strawberry.tools.create_type.create_type" or fullname.endswith(".create_type") ) def _is_strawberry_pydantic_decorator(self, fullname: str) -> bool: if any( strawberry_decorator in fullname for strawberry_decorator in ( "strawberry.experimental.pydantic.object_type.type", "strawberry.experimental.pydantic.object_type.input", "strawberry.experimental.pydantic.object_type.interface", "strawberry.experimental.pydantic.error_type", ) ): return True # in some cases `fullpath` is not what we would expect, this usually # happens when `follow_imports` are disabled in mypy when you get a path # that looks likes `some_module.types.strawberry.type` return any( fullname.endswith(decorator) for decorator in ( "strawberry.experimental.pydantic.type", "strawberry.experimental.pydantic.input", "strawberry.experimental.pydantic.error_type", ) ) def plugin(version: str) -> type[StrawberryPlugin]: match = VERSION_RE.match(version) if match: MypyVersion.VERSION = Decimal(".".join(match.groups())) else: MypyVersion.VERSION = FALLBACK_VERSION warnings.warn( f"Mypy version {version} could not be parsed. Reverting to v0.800", stacklevel=1, ) return StrawberryPlugin strawberry-graphql-0.287.0/strawberry/extensions/000077500000000000000000000000001511033167500221355ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/extensions/__init__.py000066400000000000000000000024341511033167500242510ustar00rootroot00000000000000import warnings from .add_validation_rules import AddValidationRules from .base_extension import LifecycleStep, SchemaExtension from .disable_introspection import DisableIntrospection from .disable_validation import DisableValidation from .field_extension import FieldExtension from .mask_errors import MaskErrors from .max_aliases import MaxAliasesLimiter from .max_tokens import MaxTokensLimiter from .parser_cache import ParserCache from .query_depth_limiter import IgnoreContext, QueryDepthLimiter from .validation_cache import ValidationCache def __getattr__(name: str) -> type[SchemaExtension]: if name == "Extension": warnings.warn( ( "importing `Extension` from `strawberry.extensions` " "is deprecated, import `SchemaExtension` instead." ), DeprecationWarning, stacklevel=2, ) return SchemaExtension raise AttributeError(f"module {__name__} has no attribute {name}") __all__ = [ "AddValidationRules", "DisableIntrospection", "DisableValidation", "FieldExtension", "IgnoreContext", "LifecycleStep", "MaskErrors", "MaxAliasesLimiter", "MaxTokensLimiter", "ParserCache", "QueryDepthLimiter", "SchemaExtension", "ValidationCache", ] strawberry-graphql-0.287.0/strawberry/extensions/add_validation_rules.py000066400000000000000000000025161511033167500266670ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from strawberry.extensions.base_extension import SchemaExtension if TYPE_CHECKING: from collections.abc import Iterator from graphql import ASTValidationRule class AddValidationRules(SchemaExtension): """Add graphql-core validation rules. Example: ```python import strawberry from strawberry.extensions import AddValidationRules from graphql import ValidationRule, GraphQLError class MyCustomRule(ValidationRule): def enter_field(self, node, *args) -> None: if node.name.value == "secret_field": self.report_error(GraphQLError("Can't query field 'secret_field'")) schema = strawberry.Schema( Query, extensions=[ AddValidationRules( [ MyCustomRule, ] ), ], ) ``` """ validation_rules: list[type[ASTValidationRule]] def __init__(self, validation_rules: list[type[ASTValidationRule]]) -> None: self.validation_rules = validation_rules def on_operation(self) -> Iterator[None]: self.execution_context.validation_rules = ( self.execution_context.validation_rules + tuple(self.validation_rules) ) yield __all__ = ["AddValidationRules"] strawberry-graphql-0.287.0/strawberry/extensions/base_extension.py000066400000000000000000000045121511033167500255170ustar00rootroot00000000000000from __future__ import annotations from collections.abc import Callable from enum import Enum from typing import TYPE_CHECKING, Any from strawberry.utils.await_maybe import AsyncIteratorOrIterator, AwaitableOrValue if TYPE_CHECKING: from graphql import GraphQLResolveInfo from strawberry.types import ExecutionContext class LifecycleStep(Enum): OPERATION = "operation" VALIDATION = "validation" PARSE = "parse" RESOLVE = "resolve" class SchemaExtension: execution_context: ExecutionContext # to support extensions that still use the old signature # we have an optional argument here for ease of initialization. def __init__( self, *, execution_context: ExecutionContext | None = None ) -> None: ... def on_operation( # type: ignore self, ) -> AsyncIteratorOrIterator[None]: # pragma: no cover """Called before and after a GraphQL operation (query / mutation) starts.""" yield None def on_validate( # type: ignore self, ) -> AsyncIteratorOrIterator[None]: # pragma: no cover """Called before and after the validation step.""" yield None def on_parse( # type: ignore self, ) -> AsyncIteratorOrIterator[None]: # pragma: no cover """Called before and after the parsing step.""" yield None def on_execute( # type: ignore self, ) -> AsyncIteratorOrIterator[None]: # pragma: no cover """Called before and after the execution step.""" yield None def resolve( self, _next: Callable, root: Any, info: GraphQLResolveInfo, *args: str, **kwargs: Any, ) -> AwaitableOrValue[object]: return _next(root, info, *args, **kwargs) def get_results(self) -> AwaitableOrValue[dict[str, Any]]: return {} @classmethod def _implements_resolve(cls) -> bool: """Whether the extension implements the resolve method.""" return cls.resolve is not SchemaExtension.resolve Hook = Callable[[SchemaExtension], AsyncIteratorOrIterator[None]] HOOK_METHODS: set[str] = { SchemaExtension.on_operation.__name__, SchemaExtension.on_validate.__name__, SchemaExtension.on_parse.__name__, SchemaExtension.on_execute.__name__, } __all__ = ["HOOK_METHODS", "Hook", "LifecycleStep", "SchemaExtension"] strawberry-graphql-0.287.0/strawberry/extensions/context.py000066400000000000000000000156651511033167500242100ustar00rootroot00000000000000from __future__ import annotations import contextlib import inspect import types import warnings from asyncio import iscoroutinefunction from typing import ( TYPE_CHECKING, Any, NamedTuple, ) from strawberry.extensions import SchemaExtension from strawberry.utils.await_maybe import AwaitableOrValue, await_maybe if TYPE_CHECKING: from collections.abc import AsyncIterator, Callable, Iterator from types import TracebackType from strawberry.extensions.base_extension import Hook class WrappedHook(NamedTuple): extension: SchemaExtension hook: Callable[ ..., contextlib.AbstractAsyncContextManager[None] | contextlib.AbstractContextManager[None], ] is_async: bool class ExtensionContextManagerBase: __slots__ = ( "async_exit_stack", "default_hook", "deprecation_message", "exit_stack", "hooks", ) def __init_subclass__(cls) -> None: cls.DEPRECATION_MESSAGE = ( f"Event driven styled extensions for " f"{cls.LEGACY_ENTER} or {cls.LEGACY_EXIT}" f" are deprecated, use {cls.HOOK_NAME} instead" ) HOOK_NAME: str DEPRECATION_MESSAGE: str LEGACY_ENTER: str LEGACY_EXIT: str def __init__(self, extensions: list[SchemaExtension]) -> None: self.hooks: list[WrappedHook] = [] self.default_hook: Hook = getattr(SchemaExtension, self.HOOK_NAME) for extension in extensions: hook = self.get_hook(extension) if hook: self.hooks.append(hook) def get_hook(self, extension: SchemaExtension) -> WrappedHook | None: on_start = getattr(extension, self.LEGACY_ENTER, None) on_end = getattr(extension, self.LEGACY_EXIT, None) is_legacy = on_start is not None or on_end is not None hook_fn: Hook | None = getattr(type(extension), self.HOOK_NAME) hook_fn = hook_fn if hook_fn is not self.default_hook else None if is_legacy and hook_fn is not None: raise ValueError( f"{extension} defines both legacy and new style extension hooks for " "{self.HOOK_NAME}" ) if is_legacy: warnings.warn(self.DEPRECATION_MESSAGE, DeprecationWarning, stacklevel=3) return self.from_legacy(extension, on_start, on_end) if hook_fn: if inspect.isgeneratorfunction(hook_fn): context_manager = contextlib.contextmanager( types.MethodType(hook_fn, extension) ) return WrappedHook( extension=extension, hook=context_manager, is_async=False ) if inspect.isasyncgenfunction(hook_fn): context_manager_async = contextlib.asynccontextmanager( types.MethodType(hook_fn, extension) ) return WrappedHook( extension=extension, hook=context_manager_async, is_async=True ) if callable(hook_fn): return self.from_callable(extension, hook_fn) raise ValueError( f"Hook {self.HOOK_NAME} on {extension} " f"must be callable, received {hook_fn!r}" ) return None # Current extension does not define a hook for this lifecycle stage @staticmethod def from_legacy( extension: SchemaExtension, on_start: Callable[[], None] | None = None, on_end: Callable[[], None] | None = None, ) -> WrappedHook: if iscoroutinefunction(on_start) or iscoroutinefunction(on_end): @contextlib.asynccontextmanager async def iterator() -> AsyncIterator: if on_start: await await_maybe(on_start()) yield if on_end: await await_maybe(on_end()) return WrappedHook(extension=extension, hook=iterator, is_async=True) @contextlib.contextmanager def iterator_async() -> Iterator[None]: if on_start: on_start() yield if on_end: on_end() return WrappedHook(extension=extension, hook=iterator_async, is_async=False) @staticmethod def from_callable( extension: SchemaExtension, func: Callable[[SchemaExtension], AwaitableOrValue[Any]], ) -> WrappedHook: if iscoroutinefunction(func): @contextlib.asynccontextmanager async def iterator() -> AsyncIterator[None]: await func(extension) yield return WrappedHook(extension=extension, hook=iterator, is_async=True) @contextlib.contextmanager # type: ignore[no-redef] def iterator() -> Iterator[None]: func(extension) yield return WrappedHook(extension=extension, hook=iterator, is_async=False) def __enter__(self) -> None: self.exit_stack = contextlib.ExitStack() self.exit_stack.__enter__() for hook in self.hooks: if hook.is_async: raise RuntimeError( f"SchemaExtension hook {hook.extension}.{self.HOOK_NAME} " "failed to complete synchronously." ) self.exit_stack.enter_context(hook.hook()) # type: ignore def __exit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: self.exit_stack.__exit__(exc_type, exc_val, exc_tb) async def __aenter__(self) -> None: self.async_exit_stack = contextlib.AsyncExitStack() await self.async_exit_stack.__aenter__() for hook in self.hooks: if hook.is_async: await self.async_exit_stack.enter_async_context(hook.hook()) # type: ignore else: self.async_exit_stack.enter_context(hook.hook()) # type: ignore async def __aexit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: await self.async_exit_stack.__aexit__(exc_type, exc_val, exc_tb) class OperationContextManager(ExtensionContextManagerBase): HOOK_NAME = SchemaExtension.on_operation.__name__ LEGACY_ENTER = "on_request_start" LEGACY_EXIT = "on_request_end" class ValidationContextManager(ExtensionContextManagerBase): HOOK_NAME = SchemaExtension.on_validate.__name__ LEGACY_ENTER = "on_validation_start" LEGACY_EXIT = "on_validation_end" class ParsingContextManager(ExtensionContextManagerBase): HOOK_NAME = SchemaExtension.on_parse.__name__ LEGACY_ENTER = "on_parsing_start" LEGACY_EXIT = "on_parsing_end" class ExecutingContextManager(ExtensionContextManagerBase): HOOK_NAME = SchemaExtension.on_execute.__name__ LEGACY_ENTER = "on_executing_start" LEGACY_EXIT = "on_executing_end" strawberry-graphql-0.287.0/strawberry/extensions/directives.py000066400000000000000000000060621511033167500246540ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any from strawberry.extensions import SchemaExtension from strawberry.types.nodes import convert_arguments from strawberry.utils.await_maybe import await_maybe if TYPE_CHECKING: from collections.abc import Callable from graphql import DirectiveNode, GraphQLResolveInfo from strawberry.directive import StrawberryDirective from strawberry.schema.schema import Schema from strawberry.types.field import StrawberryField from strawberry.utils.await_maybe import AwaitableOrValue SPECIFIED_DIRECTIVES = {"include", "skip"} class DirectivesExtension(SchemaExtension): async def resolve( self, _next: Callable, root: Any, info: GraphQLResolveInfo, *args: str, **kwargs: Any, ) -> AwaitableOrValue[Any]: value = await await_maybe(_next(root, info, *args, **kwargs)) nodes = list(info.field_nodes) for directive in nodes[0].directives: if directive.name.value in SPECIFIED_DIRECTIVES: continue strawberry_directive, arguments = process_directive(directive, value, info) value = await await_maybe(strawberry_directive.resolver(**arguments)) return value class DirectivesExtensionSync(SchemaExtension): def resolve( self, _next: Callable, root: Any, info: GraphQLResolveInfo, *args: str, **kwargs: Any, ) -> AwaitableOrValue[Any]: value = _next(root, info, *args, **kwargs) nodes = list(info.field_nodes) for directive in nodes[0].directives: if directive.name.value in SPECIFIED_DIRECTIVES: continue strawberry_directive, arguments = process_directive(directive, value, info) value = strawberry_directive.resolver(**arguments) return value def process_directive( directive: DirectiveNode, value: Any, info: GraphQLResolveInfo, ) -> tuple[StrawberryDirective, dict[str, Any]]: """Get a `StrawberryDirective` from ``directive` and prepare its arguments.""" directive_name = directive.name.value schema: Schema = info.schema._strawberry_schema # type: ignore strawberry_directive = schema.get_directive_by_name(directive_name) assert strawberry_directive is not None, f"Directive {directive_name} not found" arguments = convert_arguments(info=info, nodes=directive.arguments) resolver = strawberry_directive.resolver info_parameter = resolver.info_parameter value_parameter = resolver.value_parameter if info_parameter: field: StrawberryField = schema.get_field_for_type( # type: ignore field_name=info.field_name, type_name=info.parent_type.name, ) arguments[info_parameter.name] = schema.config.info_class( _raw_info=info, _field=field ) if value_parameter: arguments[value_parameter.name] = value return strawberry_directive, arguments __all__ = ["DirectivesExtension", "DirectivesExtensionSync"] strawberry-graphql-0.287.0/strawberry/extensions/disable_introspection.py000066400000000000000000000013151511033167500270720ustar00rootroot00000000000000from graphql.validation import NoSchemaIntrospectionCustomRule from strawberry.extensions import AddValidationRules class DisableIntrospection(AddValidationRules): """Disable introspection queries. Example: ```python import strawberry from strawberry.extensions import DisableIntrospection @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "Hello, world!" schema = strawberry.Schema( Query, extensions=[ DisableIntrospection(), ], ) ``` """ def __init__(self) -> None: super().__init__([NoSchemaIntrospectionCustomRule]) __all__ = ["DisableIntrospection"] strawberry-graphql-0.287.0/strawberry/extensions/disable_validation.py000066400000000000000000000013561511033167500263310ustar00rootroot00000000000000from collections.abc import Iterator from strawberry.extensions.base_extension import SchemaExtension class DisableValidation(SchemaExtension): """Disable query validation. Example: ```python import strawberry from strawberry.extensions import DisableValidation schema = strawberry.Schema( Query, extensions=[ DisableValidation, ], ) ``` """ def __init__(self) -> None: # There aren't any arguments to this extension yet but we might add # some in the future pass def on_operation(self) -> Iterator[None]: self.execution_context.validation_rules = () # remove all validation_rules yield __all__ = ["DisableValidation"] strawberry-graphql-0.287.0/strawberry/extensions/field_extension.py000066400000000000000000000126411511033167500256720ustar00rootroot00000000000000from __future__ import annotations import itertools from collections.abc import Awaitable, Callable from functools import cached_property from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from typing import TypeAlias from strawberry.types import Info from strawberry.types.field import StrawberryField SyncExtensionResolver: TypeAlias = Callable[..., Any] AsyncExtensionResolver: TypeAlias = Callable[..., Awaitable[Any]] class FieldExtension: def apply(self, field: StrawberryField) -> None: # pragma: no cover pass def resolve( self, next_: SyncExtensionResolver, source: Any, info: Info, **kwargs: Any ) -> Any: # pragma: no cover raise NotImplementedError( "Sync Resolve is not supported for this Field Extension" ) async def resolve_async( self, next_: AsyncExtensionResolver, source: Any, info: Info, **kwargs: Any ) -> Any: # pragma: no cover raise NotImplementedError( "Async Resolve is not supported for this Field Extension" ) @cached_property def supports_sync(self) -> bool: return type(self).resolve is not FieldExtension.resolve @cached_property def supports_async(self) -> bool: return type(self).resolve_async is not FieldExtension.resolve_async class SyncToAsyncExtension(FieldExtension): """Helper class for mixing async extensions with sync resolvers. Applied automatically. """ async def resolve_async( self, next_: AsyncExtensionResolver, source: Any, info: Info, **kwargs: Any ) -> Any: return next_(source, info, **kwargs) def _get_sync_resolvers( extensions: list[FieldExtension], ) -> list[SyncExtensionResolver]: return [extension.resolve for extension in extensions] def _get_async_resolvers( extensions: list[FieldExtension], ) -> list[AsyncExtensionResolver]: return [extension.resolve_async for extension in extensions] def build_field_extension_resolvers( field: StrawberryField, ) -> list[SyncExtensionResolver | AsyncExtensionResolver]: """Builds a list of resolvers for a field with extensions. Verifies that all of the field extensions for a given field support sync or async depending on the field resolver. Inserts a SyncToAsyncExtension to be able to use Async extensions on sync resolvers Throws a TypeError otherwise. Returns True if resolving should be async, False on sync resolving based on the resolver and extensions """ if not field.extensions: return [] # pragma: no cover non_async_extensions = [ extension for extension in field.extensions if not extension.supports_async ] non_async_extension_names = ",".join( [extension.__class__.__name__ for extension in non_async_extensions] ) if field.is_async: if len(non_async_extensions) > 0: raise TypeError( f"Cannot add sync-only extension(s) {non_async_extension_names} " f"to the async resolver of Field {field.name}. " f"Please add a resolve_async method to the extension(s)." ) return _get_async_resolvers(field.extensions) # Try to wrap all sync resolvers in async so that we can use async extensions # on sync fields. This is not possible the other way around since # the result of an async resolver would have to be awaited before calling # the sync extension, making it impossible for the extension to modify # any arguments. non_sync_extensions = [ extension for extension in field.extensions if not extension.supports_sync ] if len(non_sync_extensions) == 0: # Resolve everything sync return _get_sync_resolvers(field.extensions) # We have async-only extensions and need to wrap the resolver # That means we can't have sync-only extensions after the first async one # Check if we have a chain of sync-compatible # extensions before the async extensions # -> S-S-S-S-A-A-A-A found_sync_extensions = 0 # All sync only extensions must be found before the first async-only one found_sync_only_extensions = 0 for extension in field.extensions: # ...A, abort if extension in non_sync_extensions: break # ...S if extension in non_async_extensions: found_sync_only_extensions += 1 found_sync_extensions += 1 # Length of the chain equals length of non async extensions # All sync extensions run first if len(non_async_extensions) == found_sync_only_extensions: # Prepend sync to async extension to field extensions return list( itertools.chain( _get_sync_resolvers(field.extensions[:found_sync_extensions]), [SyncToAsyncExtension().resolve_async], _get_async_resolvers(field.extensions[found_sync_extensions:]), ) ) # Some sync extensions follow the first async-only extension. Error case async_extension_names = ",".join( [extension.__class__.__name__ for extension in non_sync_extensions] ) raise TypeError( f"Cannot mix async-only extension(s) {async_extension_names} " f"with sync-only extension(s) {non_async_extension_names} " f"on Field {field.name}. " f"If possible try to change the execution order so that all sync-only " f"extensions are executed first." ) __all__ = ["FieldExtension"] strawberry-graphql-0.287.0/strawberry/extensions/mask_errors.py000066400000000000000000000035441511033167500250440ustar00rootroot00000000000000from collections.abc import Callable, Iterator from typing import Any from graphql.error import GraphQLError from graphql.execution.execute import ExecutionResult as GraphQLExecutionResult from strawberry.extensions.base_extension import SchemaExtension from strawberry.types.execution import ExecutionResult as StrawberryExecutionResult def default_should_mask_error(_: GraphQLError) -> bool: # Mask all errors return True class MaskErrors(SchemaExtension): should_mask_error: Callable[[GraphQLError], bool] error_message: str def __init__( self, should_mask_error: Callable[[GraphQLError], bool] = default_should_mask_error, error_message: str = "Unexpected error.", ) -> None: self.should_mask_error = should_mask_error self.error_message = error_message def anonymise_error(self, error: GraphQLError) -> GraphQLError: return GraphQLError( message=self.error_message, nodes=error.nodes, source=error.source, positions=error.positions, path=error.path, original_error=None, ) # TODO: proper typing def _process_result(self, result: Any) -> None: if not result.errors: return processed_errors: list[GraphQLError] = [] for error in result.errors: if self.should_mask_error(error): processed_errors.append(self.anonymise_error(error)) else: processed_errors.append(error) result.errors = processed_errors def on_operation(self) -> Iterator[None]: yield result = self.execution_context.result if isinstance(result, (GraphQLExecutionResult, StrawberryExecutionResult)): self._process_result(result) elif result: self._process_result(result.initial_result) strawberry-graphql-0.287.0/strawberry/extensions/max_aliases.py000066400000000000000000000047241511033167500250040ustar00rootroot00000000000000from graphql import ( ExecutableDefinitionNode, FieldNode, GraphQLError, InlineFragmentNode, ValidationContext, ValidationRule, ) from strawberry.extensions.add_validation_rules import AddValidationRules class MaxAliasesLimiter(AddValidationRules): """Add a validator to limit the number of aliases used. Example: ```python import strawberry from strawberry.extensions import MaxAliasesLimiter schema = strawberry.Schema(Query, extensions=[MaxAliasesLimiter(max_alias_count=15)]) ``` """ def __init__(self, max_alias_count: int) -> None: """Initialize the MaxAliasesLimiter. Args: max_alias_count: The maximum number of aliases allowed in a GraphQL document. """ validator = create_validator(max_alias_count) super().__init__([validator]) def create_validator(max_alias_count: int) -> type[ValidationRule]: """Create a validator that checks the number of aliases in a document. Args: max_alias_count: The maximum number of aliases allowed in a GraphQL document. """ class MaxAliasesValidator(ValidationRule): def __init__(self, validation_context: ValidationContext) -> None: document = validation_context.document def_that_can_contain_alias = ( def_ for def_ in document.definitions if isinstance(def_, (ExecutableDefinitionNode)) ) total_aliases = sum( count_fields_with_alias(def_node) for def_node in def_that_can_contain_alias ) if total_aliases > max_alias_count: msg = f"{total_aliases} aliases found. Allowed: {max_alias_count}" validation_context.report_error(GraphQLError(msg)) super().__init__(validation_context) return MaxAliasesValidator def count_fields_with_alias( selection_set_owner: ExecutableDefinitionNode | FieldNode | InlineFragmentNode, ) -> int: if selection_set_owner.selection_set is None: return 0 result = 0 for selection in selection_set_owner.selection_set.selections: if isinstance(selection, FieldNode) and selection.alias: result += 1 if ( isinstance(selection, (FieldNode, InlineFragmentNode)) and selection.selection_set ): result += count_fields_with_alias(selection) return result __all__ = ["MaxAliasesLimiter"] strawberry-graphql-0.287.0/strawberry/extensions/max_tokens.py000066400000000000000000000020251511033167500246560ustar00rootroot00000000000000from collections.abc import Iterator from strawberry.extensions.base_extension import SchemaExtension class MaxTokensLimiter(SchemaExtension): """Add a validator to limit the number of tokens in a GraphQL document. Example: ```python import strawberry from strawberry.extensions import MaxTokensLimiter schema = strawberry.Schema(Query, extensions=[MaxTokensLimiter(max_token_count=1000)]) ``` The following things are counted as tokens: * various brackets: "{", "}", "(", ")" * colon : * words Not counted: * quotes """ def __init__( self, max_token_count: int, ) -> None: """Initialize the MaxTokensLimiter. Args: max_token_count: The maximum number of tokens allowed in a GraphQL document. """ self.max_token_count = max_token_count def on_operation(self) -> Iterator[None]: self.execution_context.parse_options["max_tokens"] = self.max_token_count yield __all__ = ["MaxTokensLimiter"] strawberry-graphql-0.287.0/strawberry/extensions/parser_cache.py000066400000000000000000000023541511033167500251320ustar00rootroot00000000000000from collections.abc import Iterator from functools import lru_cache from graphql.language.parser import parse from strawberry.extensions.base_extension import SchemaExtension class ParserCache(SchemaExtension): """Add LRU caching the parsing step during execution to improve performance. Example: ```python import strawberry from strawberry.extensions import ParserCache schema = strawberry.Schema( Query, extensions=[ ParserCache(maxsize=100), ], ) ``` """ def __init__(self, maxsize: int | None = None) -> None: """Initialize the ParserCache. Args: maxsize: Set the maxsize of the cache. If `maxsize` is set to `None` then the cache will grow without bound. More info: https://docs.python.org/3/library/functools.html#functools.lru_cache """ self.cached_parse_document = lru_cache(maxsize=maxsize)(parse) def on_parse(self) -> Iterator[None]: execution_context = self.execution_context execution_context.graphql_document = self.cached_parse_document( execution_context.query, **execution_context.parse_options ) yield __all__ = ["ParserCache"] strawberry-graphql-0.287.0/strawberry/extensions/pyinstrument.py000066400000000000000000000013611511033167500252710ustar00rootroot00000000000000from __future__ import annotations from pathlib import Path from typing import TYPE_CHECKING from pyinstrument import Profiler from strawberry.extensions.base_extension import SchemaExtension if TYPE_CHECKING: from collections.abc import Iterator class PyInstrument(SchemaExtension): """Extension to profile the execution time of resolvers using PyInstrument.""" def __init__( self, report_path: Path = Path("pyinstrument.html"), ) -> None: self._report_path = report_path def on_operation(self) -> Iterator[None]: profiler = Profiler() profiler.start() yield profiler.stop() self._report_path.write_text(profiler.output_html()) __all__ = ["PyInstrument"] strawberry-graphql-0.287.0/strawberry/extensions/query_depth_limiter.py000066400000000000000000000230521511033167500265670ustar00rootroot00000000000000# This is a Python port of https://github.com/stems/graphql-depth-limit # which is licensed under the terms of the MIT license, reproduced below. # # ----------- # # MIT License # # Copyright (c) 2017 Stem # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import annotations import re from collections.abc import Callable from dataclasses import dataclass from typing import ( TYPE_CHECKING, TypeAlias, ) from graphql import GraphQLError from graphql.language import ( BooleanValueNode, DefinitionNode, FieldNode, FloatValueNode, FragmentDefinitionNode, FragmentSpreadNode, InlineFragmentNode, IntValueNode, ListValueNode, Node, ObjectValueNode, OperationDefinitionNode, StringValueNode, ValueNode, ) from graphql.validation import ValidationContext, ValidationRule from strawberry.extensions import AddValidationRules from strawberry.extensions.utils import is_introspection_key if TYPE_CHECKING: from collections.abc import Iterable IgnoreType: TypeAlias = Callable[[str], bool] | re.Pattern | str FieldArgumentType: TypeAlias = ( bool | int | float | str | list["FieldArgumentType"] | dict[str, "FieldArgumentType"] ) FieldArgumentsType: TypeAlias = dict[str, FieldArgumentType] @dataclass class IgnoreContext: field_name: str field_args: FieldArgumentsType node: Node context: ValidationContext ShouldIgnoreType = Callable[[IgnoreContext], bool] class QueryDepthLimiter(AddValidationRules): """Add a validator to limit the query depth of GraphQL operations. Example: ```python import strawberry from strawberry.extensions import QueryDepthLimiter schema = strawberry.Schema( Query, extensions=[QueryDepthLimiter(max_depth=4)], ) ``` """ def __init__( self, max_depth: int, callback: Callable[[dict[str, int]], None] | None = None, should_ignore: ShouldIgnoreType | None = None, ) -> None: """Initialize the QueryDepthLimiter. Args: max_depth: The maximum allowed depth for any operation in a GraphQL document. callback: Called each time validation runs. Receives an Object which is a map of the depths for each operation. should_ignore: Stops recursive depth checking based on a field name and arguments. A function that returns a boolean and conforms to the ShouldIgnoreType function signature. """ if should_ignore is not None and not callable(should_ignore): raise TypeError( "The `should_ignore` argument to " "`QueryDepthLimiter` must be a callable." ) validator = create_validator(max_depth, should_ignore, callback) super().__init__([validator]) def create_validator( max_depth: int, should_ignore: ShouldIgnoreType | None, callback: Callable[[dict[str, int]], None] | None = None, ) -> type[ValidationRule]: class DepthLimitValidator(ValidationRule): def __init__(self, validation_context: ValidationContext) -> None: document = validation_context.document definitions = document.definitions fragments = get_fragments(definitions) queries = get_queries_and_mutations(definitions) query_depths = {} for query in queries: query_depths[query] = determine_depth( node=queries[query], fragments=fragments, depth_so_far=0, max_depth=max_depth, context=validation_context, operation_name=query, should_ignore=should_ignore, ) if callable(callback): callback(query_depths) super().__init__(validation_context) return DepthLimitValidator def get_fragments( definitions: Iterable[DefinitionNode], ) -> dict[str, FragmentDefinitionNode]: fragments = {} for definition in definitions: if isinstance(definition, FragmentDefinitionNode): fragments[definition.name.value] = definition return fragments # This will actually get both queries and mutations. # We can basically treat those the same def get_queries_and_mutations( definitions: Iterable[DefinitionNode], ) -> dict[str, OperationDefinitionNode]: operations = {} for definition in definitions: if isinstance(definition, OperationDefinitionNode): operation = definition.name.value if definition.name else "anonymous" operations[operation] = definition return operations def get_field_name( node: FieldNode, ) -> str: return node.alias.value if node.alias else node.name.value def resolve_field_value( value: ValueNode, ) -> FieldArgumentType: if isinstance(value, StringValueNode): return value.value if isinstance(value, IntValueNode): return int(value.value) if isinstance(value, FloatValueNode): return float(value.value) if isinstance(value, BooleanValueNode): return value.value if isinstance(value, ListValueNode): return [resolve_field_value(v) for v in value.values] if isinstance(value, ObjectValueNode): return {v.name.value: resolve_field_value(v.value) for v in value.fields} return {} def get_field_arguments( node: FieldNode, ) -> FieldArgumentsType: args_dict: FieldArgumentsType = {} for arg in node.arguments: args_dict[arg.name.value] = resolve_field_value(arg.value) return args_dict def determine_depth( node: Node, fragments: dict[str, FragmentDefinitionNode], depth_so_far: int, max_depth: int, context: ValidationContext, operation_name: str, should_ignore: ShouldIgnoreType | None, ) -> int: if depth_so_far > max_depth: context.report_error( GraphQLError( f"'{operation_name}' exceeds maximum operation depth of {max_depth}", [node], ) ) return depth_so_far if isinstance(node, FieldNode): # by default, ignore the introspection fields which begin # with double underscores should_ignore_field = is_introspection_key(node.name.value) or ( should_ignore( IgnoreContext( get_field_name(node), get_field_arguments(node), node, context, ) ) if should_ignore is not None else False ) if should_ignore_field or not node.selection_set: return 0 return 1 + max( determine_depth( node=selection, fragments=fragments, depth_so_far=depth_so_far + 1, max_depth=max_depth, context=context, operation_name=operation_name, should_ignore=should_ignore, ) for selection in node.selection_set.selections ) if isinstance(node, FragmentSpreadNode): return determine_depth( node=fragments[node.name.value], fragments=fragments, depth_so_far=depth_so_far, max_depth=max_depth, context=context, operation_name=operation_name, should_ignore=should_ignore, ) if isinstance( node, (InlineFragmentNode, FragmentDefinitionNode, OperationDefinitionNode) ): return max( determine_depth( node=selection, fragments=fragments, depth_so_far=depth_so_far, max_depth=max_depth, context=context, operation_name=operation_name, should_ignore=should_ignore, ) for selection in node.selection_set.selections ) raise TypeError(f"Depth crawler cannot handle: {node.kind}") # pragma: no cover def is_ignored(node: FieldNode, ignore: list[IgnoreType] | None = None) -> bool: if ignore is None: return False for rule in ignore: field_name = node.name.value if isinstance(rule, str): if field_name == rule: return True elif isinstance(rule, re.Pattern): if rule.match(field_name): return True elif callable(rule): if rule(field_name): return True else: raise TypeError(f"Invalid ignore option: {rule}") return False __all__ = ["QueryDepthLimiter"] strawberry-graphql-0.287.0/strawberry/extensions/runner.py000066400000000000000000000035041511033167500240220ustar00rootroot00000000000000from __future__ import annotations import inspect from typing import TYPE_CHECKING, Any from strawberry.extensions.context import ( ExecutingContextManager, OperationContextManager, ParsingContextManager, ValidationContextManager, ) from strawberry.utils.await_maybe import await_maybe if TYPE_CHECKING: from strawberry.types import ExecutionContext from . import SchemaExtension class SchemaExtensionsRunner: extensions: list[SchemaExtension] def __init__( self, execution_context: ExecutionContext, extensions: list[SchemaExtension] | None = None, ) -> None: self.execution_context = execution_context self.extensions = extensions or [] def operation(self) -> OperationContextManager: return OperationContextManager(self.extensions) def validation(self) -> ValidationContextManager: return ValidationContextManager(self.extensions) def parsing(self) -> ParsingContextManager: return ParsingContextManager(self.extensions) def executing(self) -> ExecutingContextManager: return ExecutingContextManager(self.extensions) def get_extensions_results_sync(self) -> dict[str, Any]: data: dict[str, Any] = {} for extension in self.extensions: if inspect.iscoroutinefunction(extension.get_results): msg = "Cannot use async extension hook during sync execution" raise RuntimeError(msg) data.update(extension.get_results()) # type: ignore return data async def get_extensions_results(self, ctx: ExecutionContext) -> dict[str, Any]: data: dict[str, Any] = {} for extension in self.extensions: data.update(await await_maybe(extension.get_results())) data.update(ctx.extensions_results) return data strawberry-graphql-0.287.0/strawberry/extensions/tracing/000077500000000000000000000000001511033167500235645ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/extensions/tracing/__init__.py000066400000000000000000000021301511033167500256710ustar00rootroot00000000000000import importlib from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from .apollo import ApolloTracingExtension, ApolloTracingExtensionSync from .datadog import DatadogTracingExtension, DatadogTracingExtensionSync from .opentelemetry import ( OpenTelemetryExtension, OpenTelemetryExtensionSync, ) __all__ = [ "ApolloTracingExtension", "ApolloTracingExtensionSync", "DatadogTracingExtension", "DatadogTracingExtensionSync", "OpenTelemetryExtension", "OpenTelemetryExtensionSync", ] def __getattr__(name: str) -> Any: if name in {"DatadogTracingExtension", "DatadogTracingExtensionSync"}: return getattr(importlib.import_module(".datadog", __name__), name) if name in {"ApolloTracingExtension", "ApolloTracingExtensionSync"}: return getattr(importlib.import_module(".apollo", __name__), name) if name in {"OpenTelemetryExtension", "OpenTelemetryExtensionSync"}: return getattr(importlib.import_module(".opentelemetry", __name__), name) raise AttributeError(f"module {__name__!r} has no attribute {name!r}") strawberry-graphql-0.287.0/strawberry/extensions/tracing/apollo.py000066400000000000000000000133771511033167500254370ustar00rootroot00000000000000from __future__ import annotations import dataclasses import time from datetime import datetime, timezone from inspect import isawaitable from typing import TYPE_CHECKING, Any from strawberry.extensions import SchemaExtension from strawberry.extensions.utils import get_path_from_info from .utils import should_skip_tracing if TYPE_CHECKING: from collections.abc import Callable, Generator from graphql import GraphQLResolveInfo DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ" if TYPE_CHECKING: from strawberry.types.execution import ExecutionContext @dataclasses.dataclass class ApolloStepStats: start_offset: int duration: int def to_json(self) -> dict[str, Any]: return {"startOffset": self.start_offset, "duration": self.duration} @dataclasses.dataclass class ApolloResolverStats: path: list[str] parent_type: Any field_name: str return_type: Any start_offset: int duration: int | None = None def to_json(self) -> dict[str, Any]: return { "path": self.path, "field_name": self.field_name, "parentType": str(self.parent_type), "returnType": str(self.return_type), "startOffset": self.start_offset, "duration": self.duration, } @dataclasses.dataclass class ApolloExecutionStats: resolvers: list[ApolloResolverStats] def to_json(self) -> dict[str, Any]: return {"resolvers": [resolver.to_json() for resolver in self.resolvers]} @dataclasses.dataclass class ApolloTracingStats: start_time: datetime end_time: datetime duration: int execution: ApolloExecutionStats validation: ApolloStepStats parsing: ApolloStepStats version: int = 1 def to_json(self) -> dict[str, Any]: return { "version": self.version, "startTime": self.start_time.strftime(DATETIME_FORMAT), "endTime": self.end_time.strftime(DATETIME_FORMAT), "duration": self.duration, "execution": self.execution.to_json(), "validation": self.validation.to_json(), "parsing": self.parsing.to_json(), } class ApolloTracingExtension(SchemaExtension): def __init__(self, execution_context: ExecutionContext) -> None: self._resolver_stats: list[ApolloResolverStats] = [] self.execution_context = execution_context def on_operation(self) -> Generator[None, None, None]: self.start_timestamp = self.now() self.start_time = datetime.now(timezone.utc) yield self.end_timestamp = self.now() self.end_time = datetime.now(timezone.utc) def on_parse(self) -> Generator[None, None, None]: self._start_parsing = self.now() yield self._end_parsing = self.now() def on_validate(self) -> Generator[None, None, None]: self._start_validation = self.now() yield self._end_validation = self.now() def now(self) -> int: return time.perf_counter_ns() @property def stats(self) -> ApolloTracingStats: return ApolloTracingStats( start_time=self.start_time, end_time=self.end_time, duration=self.end_timestamp - self.start_timestamp, execution=ApolloExecutionStats(self._resolver_stats), validation=ApolloStepStats( start_offset=self._start_validation - self.start_timestamp, duration=self._end_validation - self._start_validation, ), parsing=ApolloStepStats( start_offset=self._start_parsing - self.start_timestamp, duration=self._end_parsing - self._start_parsing, ), ) def get_results(self) -> dict[str, dict[str, Any]]: return {"tracing": self.stats.to_json()} async def resolve( self, _next: Callable, root: Any, info: GraphQLResolveInfo, *args: str, **kwargs: Any, ) -> Any: if should_skip_tracing(_next, info): result = _next(root, info, *args, **kwargs) if isawaitable(result): result = await result # pragma: no cover return result start_timestamp = self.now() resolver_stats = ApolloResolverStats( path=get_path_from_info(info), field_name=info.field_name, parent_type=info.parent_type, return_type=info.return_type, start_offset=start_timestamp - self.start_timestamp, ) try: result = _next(root, info, *args, **kwargs) if isawaitable(result): result = await result return result finally: end_timestamp = self.now() resolver_stats.duration = end_timestamp - start_timestamp self._resolver_stats.append(resolver_stats) class ApolloTracingExtensionSync(ApolloTracingExtension): def resolve( self, _next: Callable, root: Any, info: GraphQLResolveInfo, *args: str, **kwargs: Any, ) -> Any: if should_skip_tracing(_next, info): return _next(root, info, *args, **kwargs) start_timestamp = self.now() resolver_stats = ApolloResolverStats( path=get_path_from_info(info), field_name=info.field_name, parent_type=info.parent_type, return_type=info.return_type, start_offset=start_timestamp - self.start_timestamp, ) try: return _next(root, info, *args, **kwargs) finally: end_timestamp = self.now() resolver_stats.duration = end_timestamp - start_timestamp self._resolver_stats.append(resolver_stats) __all__ = ["ApolloTracingExtension", "ApolloTracingExtensionSync"] strawberry-graphql-0.287.0/strawberry/extensions/tracing/datadog.py000066400000000000000000000132331511033167500255430ustar00rootroot00000000000000from __future__ import annotations import hashlib from functools import cached_property from inspect import isawaitable from typing import TYPE_CHECKING, Any import ddtrace from packaging import version from strawberry.extensions import LifecycleStep, SchemaExtension from strawberry.extensions.tracing.utils import should_skip_tracing parsed_ddtrace_version = version.parse(ddtrace.__version__) if parsed_ddtrace_version >= version.parse("3.0.0"): from ddtrace.trace import Span, tracer else: from ddtrace import Span, tracer if TYPE_CHECKING: from collections.abc import Callable, Generator, Iterator from graphql import GraphQLResolveInfo from strawberry.types.execution import ExecutionContext class DatadogTracingExtension(SchemaExtension): def __init__( self, *, execution_context: ExecutionContext | None = None, ) -> None: if execution_context: self.execution_context = execution_context @cached_property def _resource_name(self) -> str: if self.execution_context.query is None: return "query_missing" query_hash = self.hash_query(self.execution_context.query) if self.execution_context.operation_name: return f"{self.execution_context.operation_name}:{query_hash}" return query_hash def create_span( self, lifecycle_step: LifecycleStep, name: str, **kwargs: Any, ) -> Span: """Create a span with the given name and kwargs. You can override this if you want to add more tags to the span. Example: ```python class CustomExtension(DatadogTracingExtension): def create_span(self, lifecycle_step, name, **kwargs): span = super().create_span(lifecycle_step, name, **kwargs) if lifecycle_step == LifeCycleStep.OPERATION: span.set_tag("graphql.query", self.execution_context.query) return span ``` """ return tracer.trace( name, span_type="graphql", **kwargs, ) def hash_query(self, query: str) -> str: return hashlib.md5(query.encode("utf-8")).hexdigest() # noqa: S324 def on_operation(self) -> Iterator[None]: self._operation_name = self.execution_context.operation_name span_name = ( f"{self._operation_name}" if self._operation_name else "Anonymous Query" ) self.request_span = self.create_span( LifecycleStep.OPERATION, span_name, resource=self._resource_name, service="strawberry", ) self.request_span.set_tag("graphql.operation_name", self._operation_name) query = self.execution_context.query if query is not None: query = query.strip() operation_type = "query" if query.startswith("mutation"): operation_type = "mutation" elif query.startswith("subscription"): # pragma: no cover operation_type = "subscription" else: operation_type = "query_missing" self.request_span.set_tag("graphql.operation_type", operation_type) yield self.request_span.finish() def on_validate(self) -> Generator[None, None, None]: self.validation_span = self.create_span( lifecycle_step=LifecycleStep.VALIDATION, name="Validation", ) yield self.validation_span.finish() def on_parse(self) -> Generator[None, None, None]: self.parsing_span = self.create_span( lifecycle_step=LifecycleStep.PARSE, name="Parsing", ) yield self.parsing_span.finish() async def resolve( self, _next: Callable, root: Any, info: GraphQLResolveInfo, *args: str, **kwargs: Any, ) -> Any: if should_skip_tracing(_next, info): result = _next(root, info, *args, **kwargs) if isawaitable(result): # pragma: no cover result = await result return result field_path = f"{info.parent_type}.{info.field_name}" with self.create_span( lifecycle_step=LifecycleStep.RESOLVE, name=f"Resolving: {field_path}", ) as span: span.set_tag("graphql.field_name", info.field_name) span.set_tag("graphql.parent_type", info.parent_type.name) span.set_tag("graphql.field_path", field_path) span.set_tag("graphql.path", ".".join(map(str, info.path.as_list()))) result = _next(root, info, *args, **kwargs) if isawaitable(result): result = await result return result class DatadogTracingExtensionSync(DatadogTracingExtension): def resolve( self, _next: Callable, root: Any, info: GraphQLResolveInfo, *args: str, **kwargs: Any, ) -> Any: if should_skip_tracing(_next, info): return _next(root, info, *args, **kwargs) field_path = f"{info.parent_type}.{info.field_name}" with self.create_span( lifecycle_step=LifecycleStep.RESOLVE, name=f"Resolving: {field_path}", ) as span: span.set_tag("graphql.field_name", info.field_name) span.set_tag("graphql.parent_type", info.parent_type.name) span.set_tag("graphql.field_path", field_path) span.set_tag("graphql.path", ".".join(map(str, info.path.as_list()))) return _next(root, info, *args, **kwargs) __all__ = ["DatadogTracingExtension", "DatadogTracingExtensionSync"] strawberry-graphql-0.287.0/strawberry/extensions/tracing/opentelemetry.py000066400000000000000000000161641511033167500270420ustar00rootroot00000000000000from __future__ import annotations from collections.abc import Callable from copy import deepcopy from inspect import isawaitable from typing import ( TYPE_CHECKING, Any, ) from opentelemetry import trace from opentelemetry.trace import SpanKind from strawberry.extensions import LifecycleStep, SchemaExtension from strawberry.extensions.utils import get_path_from_info from .utils import should_skip_tracing if TYPE_CHECKING: from collections.abc import Generator, Iterable from graphql import GraphQLResolveInfo from opentelemetry.trace import Span, Tracer from strawberry.types.execution import ExecutionContext DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ" ArgFilter = Callable[[dict[str, Any], "GraphQLResolveInfo"], dict[str, Any]] class OpenTelemetryExtension(SchemaExtension): _arg_filter: ArgFilter | None _span_holder: dict[LifecycleStep, Span] _tracer: Tracer def __init__( self, *, execution_context: ExecutionContext | None = None, arg_filter: ArgFilter | None = None, tracer_provider: trace.TracerProvider | None = None, ) -> None: self._arg_filter = arg_filter self._tracer = trace.get_tracer("strawberry", tracer_provider=tracer_provider) self._span_holder = {} if execution_context: self.execution_context = execution_context def on_operation(self) -> Generator[None, None, None]: self._operation_name = self.execution_context.operation_name span_name = ( f"GraphQL Query: {self._operation_name}" if self._operation_name else "GraphQL Query" ) self._span_holder[LifecycleStep.OPERATION] = self._tracer.start_span( span_name, kind=SpanKind.SERVER ) self._span_holder[LifecycleStep.OPERATION].set_attribute("component", "graphql") if self.execution_context.query: self._span_holder[LifecycleStep.OPERATION].set_attribute( "query", self.execution_context.query ) yield # If the client doesn't provide an operation name then GraphQL will # execute the first operation in the query string. This might be a named # operation but we don't know until the parsing stage has finished. If # that's the case we want to update the span name so that we have a more # useful name in our trace. if not self._operation_name and self.execution_context.operation_name: span_name = f"GraphQL Query: {self.execution_context.operation_name}" self._span_holder[LifecycleStep.OPERATION].update_name(span_name) self._span_holder[LifecycleStep.OPERATION].end() def on_validate(self) -> Generator[None, None, None]: ctx = trace.set_span_in_context(self._span_holder[LifecycleStep.OPERATION]) self._span_holder[LifecycleStep.VALIDATION] = self._tracer.start_span( "GraphQL Validation", context=ctx, ) yield self._span_holder[LifecycleStep.VALIDATION].end() def on_parse(self) -> Generator[None, None, None]: ctx = trace.set_span_in_context(self._span_holder[LifecycleStep.OPERATION]) self._span_holder[LifecycleStep.PARSE] = self._tracer.start_span( "GraphQL Parsing", context=ctx ) yield self._span_holder[LifecycleStep.PARSE].end() def filter_resolver_args( self, args: dict[str, Any], info: GraphQLResolveInfo ) -> dict[str, Any]: if not self._arg_filter: return args return self._arg_filter(deepcopy(args), info) def convert_dict_to_allowed_types(self, value: dict) -> str: return ( "{" + ", ".join( f"{k}: {self.convert_to_allowed_types(v)}" for k, v in value.items() ) + "}" ) def convert_to_allowed_types(self, value: Any) -> Any: # Put these in decreasing order of use-cases to exit as soon as possible if isinstance(value, (bool, str, bytes, int, float)): return value if isinstance(value, (list, tuple, range)): return self.convert_list_or_tuple_to_allowed_types(value) if isinstance(value, dict): return self.convert_dict_to_allowed_types(value) if isinstance(value, (set, frozenset)): return self.convert_set_to_allowed_types(value) if isinstance(value, complex): return str(value) # Convert complex numbers to strings if isinstance(value, (bytearray, memoryview)): return bytes(value) # Convert bytearray and memoryview to bytes return str(value) def convert_set_to_allowed_types(self, value: set | frozenset) -> str: return ( "{" + ", ".join(str(self.convert_to_allowed_types(x)) for x in value) + "}" ) def convert_list_or_tuple_to_allowed_types(self, value: Iterable) -> str: return ", ".join(map(str, map(self.convert_to_allowed_types, value))) def add_tags(self, span: Span, info: GraphQLResolveInfo, kwargs: Any) -> None: graphql_path = ".".join(map(str, get_path_from_info(info))) span.set_attribute("component", "graphql") span.set_attribute("graphql.parentType", info.parent_type.name) span.set_attribute("graphql.path", graphql_path) if kwargs: filtered_kwargs = self.filter_resolver_args(kwargs, info) for kwarg, value in filtered_kwargs.items(): converted_value = self.convert_to_allowed_types(value) span.set_attribute(f"graphql.param.{kwarg}", converted_value) async def resolve( self, _next: Callable, root: Any, info: GraphQLResolveInfo, *args: str, **kwargs: Any, ) -> Any: if should_skip_tracing(_next, info): result = _next(root, info, *args, **kwargs) if isawaitable(result): # pragma: no cover result = await result return result with self._tracer.start_as_current_span( f"GraphQL Resolving: {info.field_name}", context=trace.set_span_in_context( self._span_holder[LifecycleStep.OPERATION] ), ) as span: self.add_tags(span, info, kwargs) result = _next(root, info, *args, **kwargs) if isawaitable(result): result = await result return result class OpenTelemetryExtensionSync(OpenTelemetryExtension): def resolve( self, _next: Callable, root: Any, info: GraphQLResolveInfo, *args: str, **kwargs: Any, ) -> Any: if should_skip_tracing(_next, info): return _next(root, info, *args, **kwargs) with self._tracer.start_as_current_span( f"GraphQL Resolving: {info.field_name}", context=trace.set_span_in_context( self._span_holder[LifecycleStep.OPERATION] ), ) as span: self.add_tags(span, info, kwargs) return _next(root, info, *args, **kwargs) __all__ = ["OpenTelemetryExtension", "OpenTelemetryExtensionSync"] strawberry-graphql-0.287.0/strawberry/extensions/tracing/utils.py000066400000000000000000000012561511033167500253020ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any from strawberry.extensions.utils import is_introspection_field from strawberry.resolvers import is_default_resolver if TYPE_CHECKING: from collections.abc import Callable from graphql import GraphQLResolveInfo def should_skip_tracing(resolver: Callable[..., Any], info: GraphQLResolveInfo) -> bool: if info.field_name not in info.parent_type.fields: return True resolver = info.parent_type.fields[info.field_name].resolve return ( is_introspection_field(info) or is_default_resolver(resolver) or resolver is None ) __all__ = ["should_skip_tracing"] strawberry-graphql-0.287.0/strawberry/extensions/utils.py000066400000000000000000000017261511033167500236550ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: from graphql import GraphQLResolveInfo def is_introspection_key(key: str | int) -> bool: # from: https://spec.graphql.org/June2018/#sec-Schema # > All types and directives defined within a schema must not have a name which # > begins with "__" (two underscores), as this is used exclusively # > by GraphQL`s introspection system. return str(key).startswith("__") def is_introspection_field(info: GraphQLResolveInfo) -> bool: path = info.path while path: if is_introspection_key(path.key): return True path = path.prev return False def get_path_from_info(info: GraphQLResolveInfo) -> list[str]: path = info.path elements = [] while path: elements.append(path.key) path = path.prev return elements[::-1] __all__ = ["get_path_from_info", "is_introspection_field", "is_introspection_key"] strawberry-graphql-0.287.0/strawberry/extensions/validation_cache.py000066400000000000000000000026101511033167500257630ustar00rootroot00000000000000from collections.abc import Iterator from functools import lru_cache from strawberry.extensions.base_extension import SchemaExtension class ValidationCache(SchemaExtension): """Add LRU caching the validation step during execution to improve performance. Example: ```python import strawberry from strawberry.extensions import ValidationCache schema = strawberry.Schema( Query, extensions=[ ValidationCache(maxsize=100), ], ) ``` """ def __init__(self, maxsize: int | None = None) -> None: """Initialize the ValidationCache. Args: maxsize: Set the maxsize of the cache. If `maxsize` is set to `None` then the cache will grow without bound. More info: https://docs.python.org/3/library/functools.html#functools.lru_cache """ from strawberry.schema.schema import validate_document self.cached_validate_document = lru_cache(maxsize=maxsize)(validate_document) def on_validate(self) -> Iterator[None]: execution_context = self.execution_context errors = self.cached_validate_document( execution_context.schema._schema, execution_context.graphql_document, execution_context.validation_rules, ) execution_context.pre_execution_errors = errors yield __all__ = ["ValidationCache"] strawberry-graphql-0.287.0/strawberry/fastapi/000077500000000000000000000000001511033167500213655ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/fastapi/__init__.py000066400000000000000000000002231511033167500234730ustar00rootroot00000000000000from strawberry.fastapi.context import BaseContext from strawberry.fastapi.router import GraphQLRouter __all__ = ["BaseContext", "GraphQLRouter"] strawberry-graphql-0.287.0/strawberry/fastapi/context.py000066400000000000000000000012151511033167500234220ustar00rootroot00000000000000from typing import Any, Union from starlette.background import BackgroundTasks from starlette.requests import Request from starlette.responses import Response from starlette.websockets import WebSocket CustomContext = Union["BaseContext", dict[str, Any]] MergedContext = Union[ "BaseContext", dict[str, Any | BackgroundTasks | Request | Response | WebSocket] ] class BaseContext: connection_params: Any | None = None def __init__(self) -> None: self.request: Request | WebSocket | None = None self.background_tasks: BackgroundTasks | None = None self.response: Response | None = None __all__ = ["BaseContext"] strawberry-graphql-0.287.0/strawberry/fastapi/router.py000066400000000000000000000271241511033167500232650ustar00rootroot00000000000000from __future__ import annotations import warnings from datetime import timedelta from inspect import signature from typing import ( TYPE_CHECKING, Any, TypeGuard, cast, ) from fastapi import APIRouter, Depends, params from fastapi.datastructures import Default from fastapi.routing import APIRoute from fastapi.utils import generate_unique_id from lia import HTTPException, StarletteRequestAdapter from starlette import status from starlette.background import BackgroundTasks # noqa: TC002 from starlette.requests import HTTPConnection, Request from starlette.responses import ( HTMLResponse, JSONResponse, PlainTextResponse, Response, StreamingResponse, ) from starlette.websockets import WebSocket from strawberry.asgi import ASGIWebSocketAdapter from strawberry.exceptions import InvalidCustomContext from strawberry.fastapi.context import BaseContext, CustomContext from strawberry.http.async_base_view import AsyncBaseHTTPView from strawberry.http.typevars import Context, RootValue from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL if TYPE_CHECKING: from collections.abc import ( AsyncIterator, Awaitable, Callable, Sequence, ) from enum import Enum from starlette.routing import BaseRoute from starlette.types import ASGIApp, Lifespan from strawberry.fastapi.context import MergedContext from strawberry.http import GraphQLHTTPResponse from strawberry.http.ides import GraphQL_IDE from strawberry.schema import BaseSchema class GraphQLRouter( AsyncBaseHTTPView[ Request, Response, Response, WebSocket, WebSocket, Context, RootValue ], APIRouter, ): allow_queries_via_get = True request_adapter_class = StarletteRequestAdapter websocket_adapter_class = ASGIWebSocketAdapter # type: ignore @staticmethod async def __get_root_value() -> None: return None @staticmethod def __get_context_getter( custom_getter: Callable[ ..., CustomContext | None | Awaitable[CustomContext | None] ], ) -> Callable[..., Awaitable[CustomContext]]: async def dependency( custom_context: CustomContext | None, background_tasks: BackgroundTasks, connection: HTTPConnection, response: Response = None, # type: ignore ) -> MergedContext: request = cast("Request | WebSocket", connection) if isinstance(custom_context, BaseContext): custom_context.request = request custom_context.background_tasks = background_tasks custom_context.response = response return custom_context default_context = { "request": request, "background_tasks": background_tasks, "response": response, } if isinstance(custom_context, dict): return { **default_context, **custom_context, } if custom_context is None: return default_context raise InvalidCustomContext # replace the signature parameters of dependency... # ...with the old parameters minus the first argument as it will be replaced... # ...with the value obtained by injecting custom_getter context as a dependency. sig = signature(dependency) sig = sig.replace( parameters=[ *list(sig.parameters.values())[1:], sig.parameters["custom_context"].replace( default=Depends(custom_getter) ), ], ) # there is an ongoing issue with types and .__signature__ applied to Callables: # https://github.com/python/mypy/issues/5958, as of 14/12/21 # as such, the below line has its typing ignored by MyPy dependency.__signature__ = sig # type: ignore return dependency def __init__( self, schema: BaseSchema, path: str = "", graphiql: bool | None = None, graphql_ide: GraphQL_IDE | None = "graphiql", allow_queries_via_get: bool = True, keep_alive: bool = False, keep_alive_interval: float = 1, root_value_getter: Callable[[], RootValue] | None = None, context_getter: Callable[..., Context | None | Awaitable[Context | None]] | None = None, subscription_protocols: Sequence[str] = ( GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL, ), connection_init_wait_timeout: timedelta = timedelta(minutes=1), prefix: str = "", tags: list[str | Enum] | None = None, dependencies: Sequence[params.Depends] | None = None, default_response_class: type[Response] = Default(JSONResponse), responses: dict[int | str, dict[str, Any]] | None = None, callbacks: list[BaseRoute] | None = None, routes: list[BaseRoute] | None = None, redirect_slashes: bool = True, default: ASGIApp | None = None, dependency_overrides_provider: Any | None = None, route_class: type[APIRoute] = APIRoute, on_startup: Sequence[Callable[[], Any]] | None = None, on_shutdown: Sequence[Callable[[], Any]] | None = None, lifespan: Lifespan[Any] | None = None, deprecated: bool | None = None, include_in_schema: bool = True, generate_unique_id_function: Callable[[APIRoute], str] = Default( generate_unique_id ), multipart_uploads_enabled: bool = False, **kwargs: Any, ) -> None: super().__init__( prefix=prefix, tags=tags, dependencies=dependencies, default_response_class=default_response_class, responses=responses, callbacks=callbacks, routes=routes, redirect_slashes=redirect_slashes, default=default, dependency_overrides_provider=dependency_overrides_provider, route_class=route_class, on_startup=on_startup, on_shutdown=on_shutdown, lifespan=lifespan, deprecated=deprecated, include_in_schema=include_in_schema, generate_unique_id_function=generate_unique_id_function, **kwargs, ) self.schema = schema self.allow_queries_via_get = allow_queries_via_get self.keep_alive = keep_alive self.keep_alive_interval = keep_alive_interval self.root_value_getter = root_value_getter or self.__get_root_value # TODO: clean this type up self.context_getter = self.__get_context_getter( context_getter or (lambda: None) # type: ignore ) self.protocols = subscription_protocols self.connection_init_wait_timeout = connection_init_wait_timeout self.multipart_uploads_enabled = multipart_uploads_enabled if graphiql is not None: warnings.warn( "The `graphiql` argument is deprecated in favor of `graphql_ide`", DeprecationWarning, stacklevel=2, ) self.graphql_ide = "graphiql" if graphiql else None else: self.graphql_ide = graphql_ide @self.get( path, responses={ 200: { "description": "The GraphiQL integrated development environment.", }, 404: { "description": ( "Not found if GraphiQL or query via GET are not enabled." ) }, }, include_in_schema=graphiql or allow_queries_via_get, ) async def handle_http_get( # pyright: ignore request: Request, response: Response, context: Context = Depends(self.context_getter), root_value: RootValue = Depends(self.root_value_getter), ) -> Response: self.temporal_response = response try: return await self.run( request=request, context=context, root_value=root_value ) except HTTPException as e: return PlainTextResponse( e.reason, status_code=e.status_code, ) @self.post(path) async def handle_http_post( # pyright: ignore request: Request, response: Response, # TODO: use Annotated in future context: Context = Depends(self.context_getter), root_value: RootValue = Depends(self.root_value_getter), ) -> Response: self.temporal_response = response try: return await self.run( request=request, context=context, root_value=root_value ) except HTTPException as e: return PlainTextResponse( e.reason, status_code=e.status_code, ) @self.websocket(path) async def websocket_endpoint( # pyright: ignore websocket: WebSocket, context: Context = Depends(self.context_getter), root_value: RootValue = Depends(self.root_value_getter), ) -> None: await self.run(request=websocket, context=context, root_value=root_value) async def render_graphql_ide(self, request: Request) -> HTMLResponse: return HTMLResponse(self.graphql_ide_html) async def get_context( self, request: Request | WebSocket, response: Response | WebSocket ) -> Context: # pragma: no cover raise ValueError("`get_context` is not used by FastAPI GraphQL Router") async def get_root_value( self, request: Request | WebSocket ) -> RootValue | None: # pragma: no cover raise ValueError("`get_root_value` is not used by FastAPI GraphQL Router") async def get_sub_response(self, request: Request) -> Response: return self.temporal_response def create_response( self, response_data: GraphQLHTTPResponse | list[GraphQLHTTPResponse], sub_response: Response, ) -> Response: response = Response( self.encode_json(response_data), media_type="application/json", status_code=sub_response.status_code or status.HTTP_200_OK, ) response.headers.raw.extend(sub_response.headers.raw) return response async def create_streaming_response( self, request: Request, stream: Callable[[], AsyncIterator[str]], sub_response: Response, headers: dict[str, str], ) -> Response: return StreamingResponse( stream(), status_code=sub_response.status_code or status.HTTP_200_OK, headers={ **sub_response.headers, **headers, }, ) def is_websocket_request( self, request: Request | WebSocket ) -> TypeGuard[WebSocket]: return request.scope["type"] == "websocket" async def pick_websocket_subprotocol(self, request: WebSocket) -> str | None: protocols = request["subprotocols"] intersection = set(protocols) & set(self.protocols) sorted_intersection = sorted(intersection, key=protocols.index) return next(iter(sorted_intersection), None) async def create_websocket_response( self, request: WebSocket, subprotocol: str | None ) -> WebSocket: await request.accept(subprotocol=subprotocol) return request __all__ = ["GraphQLRouter"] strawberry-graphql-0.287.0/strawberry/federation/000077500000000000000000000000001511033167500220565ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/federation/__init__.py000066400000000000000000000010451511033167500241670ustar00rootroot00000000000000from .argument import argument from .enum import enum, enum_value from .field import field from .mutation import mutation from .object_type import input, interface, interface_object, type # noqa: A004 from .scalar import scalar from .schema import Schema from .schema_directive import schema_directive from .union import union __all__ = [ "Schema", "argument", "enum", "enum_value", "field", "input", "interface", "interface_object", "mutation", "scalar", "schema_directive", "type", "union", ] strawberry-graphql-0.287.0/strawberry/federation/argument.py000066400000000000000000000014641511033167500242570ustar00rootroot00000000000000from collections.abc import Iterable from strawberry.types.arguments import StrawberryArgumentAnnotation def argument( description: str | None = None, name: str | None = None, deprecation_reason: str | None = None, directives: Iterable[object] = (), inaccessible: bool = False, tags: Iterable[str] | None = (), ) -> StrawberryArgumentAnnotation: from strawberry.federation.schema_directives import Inaccessible, Tag directives = list(directives) if inaccessible: directives.append(Inaccessible()) if tags: directives.extend(Tag(name=tag) for tag in tags) return StrawberryArgumentAnnotation( description=description, name=name, deprecation_reason=deprecation_reason, directives=directives, ) __all__ = ["argument"] strawberry-graphql-0.287.0/strawberry/federation/enum.py000066400000000000000000000057741511033167500234110ustar00rootroot00000000000000from __future__ import annotations from typing import ( TYPE_CHECKING, Any, overload, ) from strawberry.types.enum import _process_enum from strawberry.types.enum import enum_value as base_enum_value if TYPE_CHECKING: from collections.abc import Callable, Iterable from strawberry.enum import EnumType, EnumValueDefinition def enum_value( value: Any, name: str | None = None, deprecation_reason: str | None = None, directives: Iterable[object] = (), inaccessible: bool = False, tags: Iterable[str] = (), ) -> EnumValueDefinition: from strawberry.federation.schema_directives import Inaccessible, Tag directives = list(directives) if inaccessible: directives.append(Inaccessible()) if tags: directives.extend(Tag(name=tag) for tag in tags) return base_enum_value( value=value, name=name, deprecation_reason=deprecation_reason, directives=directives, ) @overload def enum( _cls: EnumType, *, name: str | None = None, description: str | None = None, directives: Iterable[object] = (), authenticated: bool = False, inaccessible: bool = False, policy: list[list[str]] | None = None, requires_scopes: list[list[str]] | None = None, tags: Iterable[str] | None = (), ) -> EnumType: ... @overload def enum( _cls: None = None, *, name: str | None = None, description: str | None = None, directives: Iterable[object] = (), authenticated: bool = False, inaccessible: bool = False, policy: list[list[str]] | None = None, requires_scopes: list[list[str]] | None = None, tags: Iterable[str] | None = (), ) -> Callable[[EnumType], EnumType]: ... def enum( _cls: EnumType | None = None, *, name=None, description=None, directives=(), authenticated: bool = False, inaccessible: bool = False, policy: list[list[str]] | None = None, requires_scopes: list[list[str]] | None = None, tags: Iterable[str] | None = (), ) -> EnumType | Callable[[EnumType], EnumType]: """Registers the enum in the GraphQL type system. If name is passed, the name of the GraphQL type will be the value passed of name instead of the Enum class name. """ from strawberry.federation.schema_directives import ( Authenticated, Inaccessible, Policy, RequiresScopes, Tag, ) directives = list(directives) if authenticated: directives.append(Authenticated()) if inaccessible: directives.append(Inaccessible()) if policy: directives.append(Policy(policies=policy)) if requires_scopes: directives.append(RequiresScopes(scopes=requires_scopes)) if tags: directives.extend(Tag(name=tag) for tag in tags) def wrap(cls: EnumType) -> EnumType: return _process_enum(cls, name, description, directives=directives) if not _cls: return wrap return wrap(_cls) # pragma: no cover __all__ = ["enum", "enum_value"] strawberry-graphql-0.287.0/strawberry/federation/field.py000066400000000000000000000205441511033167500235200ustar00rootroot00000000000000from __future__ import annotations import dataclasses from typing import ( TYPE_CHECKING, Any, TypeVar, overload, ) from strawberry.types.field import field as base_field from strawberry.types.unset import UNSET if TYPE_CHECKING: from collections.abc import Callable, Iterable, Mapping, Sequence from typing import Literal from strawberry.extensions.field_extension import FieldExtension from strawberry.permission import BasePermission from strawberry.types.field import ( _RESOLVER_TYPE, _RESOLVER_TYPE_ASYNC, _RESOLVER_TYPE_SYNC, StrawberryField, ) from .schema_directives import Override T = TypeVar("T") # NOTE: we are separating the sync and async resolvers because using both # in the same function will cause mypy to raise an error. Not sure if it is a bug @overload def field( *, resolver: _RESOLVER_TYPE_ASYNC[T], name: str | None = None, is_subscription: bool = False, description: str | None = None, authenticated: bool = False, external: bool = False, inaccessible: bool = False, policy: list[list[str]] | None = None, provides: list[str] | None = None, override: Override | str | None = None, requires: list[str] | None = None, requires_scopes: list[list[str]] | None = None, tags: Iterable[str] | None = (), shareable: bool = False, init: Literal[False] = False, permission_classes: list[type[BasePermission]] | None = None, deprecation_reason: str | None = None, default: Any = dataclasses.MISSING, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: Mapping[Any, Any] | None = None, directives: Sequence[object] | None = (), extensions: list[FieldExtension] | None = None, graphql_type: Any | None = None, ) -> T: ... @overload def field( *, resolver: _RESOLVER_TYPE_SYNC[T], name: str | None = None, is_subscription: bool = False, description: str | None = None, authenticated: bool = False, external: bool = False, inaccessible: bool = False, policy: list[list[str]] | None = None, provides: list[str] | None = None, override: Override | str | None = None, requires: list[str] | None = None, requires_scopes: list[list[str]] | None = None, tags: Iterable[str] | None = (), shareable: bool = False, init: Literal[False] = False, permission_classes: list[type[BasePermission]] | None = None, deprecation_reason: str | None = None, default: Any = dataclasses.MISSING, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: Mapping[Any, Any] | None = None, directives: Sequence[object] | None = (), extensions: list[FieldExtension] | None = None, graphql_type: Any | None = None, ) -> T: ... @overload def field( *, name: str | None = None, is_subscription: bool = False, description: str | None = None, authenticated: bool = False, external: bool = False, inaccessible: bool = False, policy: list[list[str]] | None = None, provides: list[str] | None = None, override: Override | str | None = None, requires: list[str] | None = None, requires_scopes: list[list[str]] | None = None, tags: Iterable[str] | None = (), shareable: bool = False, init: Literal[True] = True, permission_classes: list[type[BasePermission]] | None = None, deprecation_reason: str | None = None, default: Any = dataclasses.MISSING, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: Mapping[Any, Any] | None = None, directives: Sequence[object] | None = (), extensions: list[FieldExtension] | None = None, graphql_type: Any | None = None, ) -> Any: ... @overload def field( resolver: _RESOLVER_TYPE_ASYNC[T], *, name: str | None = None, is_subscription: bool = False, description: str | None = None, authenticated: bool = False, external: bool = False, inaccessible: bool = False, policy: list[list[str]] | None = None, provides: list[str] | None = None, override: Override | str | None = None, requires: list[str] | None = None, requires_scopes: list[list[str]] | None = None, tags: Iterable[str] | None = (), shareable: bool = False, permission_classes: list[type[BasePermission]] | None = None, deprecation_reason: str | None = None, default: Any = dataclasses.MISSING, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: Mapping[Any, Any] | None = None, directives: Sequence[object] | None = (), extensions: list[FieldExtension] | None = None, graphql_type: Any | None = None, ) -> StrawberryField: ... @overload def field( resolver: _RESOLVER_TYPE_SYNC[T], *, name: str | None = None, is_subscription: bool = False, description: str | None = None, authenticated: bool = False, external: bool = False, inaccessible: bool = False, policy: list[list[str]] | None = None, provides: list[str] | None = None, override: Override | str | None = None, requires: list[str] | None = None, requires_scopes: list[list[str]] | None = None, tags: Iterable[str] | None = (), shareable: bool = False, permission_classes: list[type[BasePermission]] | None = None, deprecation_reason: str | None = None, default: Any = dataclasses.MISSING, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: Mapping[Any, Any] | None = None, directives: Sequence[object] | None = (), extensions: list[FieldExtension] | None = None, graphql_type: Any | None = None, ) -> StrawberryField: ... def field( resolver: _RESOLVER_TYPE[Any] | None = None, *, name: str | None = None, is_subscription: bool = False, description: str | None = None, authenticated: bool = False, external: bool = False, inaccessible: bool = False, policy: list[list[str]] | None = None, provides: list[str] | None = None, override: Override | str | None = None, requires: list[str] | None = None, requires_scopes: list[list[str]] | None = None, tags: Iterable[str] | None = (), shareable: bool = False, permission_classes: list[type[BasePermission]] | None = None, deprecation_reason: str | None = None, default: Any = dataclasses.MISSING, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: Mapping[Any, Any] | None = None, directives: Sequence[object] | None = (), extensions: list[FieldExtension] | None = None, graphql_type: Any | None = None, # This init parameter is used by PyRight to determine whether this field # is added in the constructor or not. It is not used to change # any behavior at the moment. init: Literal[True, False] | None = None, ) -> Any: from .schema_directives import ( Authenticated, External, Inaccessible, Override, Policy, Provides, Requires, RequiresScopes, Shareable, Tag, ) directives = list(directives or []) if authenticated: directives.append(Authenticated()) if external: directives.append(External()) if inaccessible: directives.append(Inaccessible()) if override: directives.append( Override(override_from=override, label=UNSET) if isinstance(override, str) else override ) if policy: directives.append(Policy(policies=policy)) if provides: directives.append(Provides(fields=" ".join(provides))) if requires: directives.append(Requires(fields=" ".join(requires))) if requires_scopes: directives.append(RequiresScopes(scopes=requires_scopes)) if shareable: directives.append(Shareable()) if tags: directives.extend(Tag(name=tag) for tag in tags) return base_field( # type: ignore resolver=resolver, # type: ignore name=name, is_subscription=is_subscription, description=description, permission_classes=permission_classes, deprecation_reason=deprecation_reason, default=default, default_factory=default_factory, init=init, # type: ignore directives=directives, metadata=metadata, extensions=extensions, graphql_type=graphql_type, ) __all__ = ["field"] strawberry-graphql-0.287.0/strawberry/federation/mutation.py000066400000000000000000000001031511033167500242620ustar00rootroot00000000000000from .field import field mutation = field __all__ = ["mutation"] strawberry-graphql-0.287.0/strawberry/federation/object_type.py000066400000000000000000000217561511033167500247520ustar00rootroot00000000000000import builtins from collections.abc import Callable, Iterable, Sequence from typing import ( TYPE_CHECKING, TypeVar, Union, overload, ) from typing_extensions import dataclass_transform from strawberry.types.field import StrawberryField from strawberry.types.field import field as base_field from strawberry.types.object_type import type as base_type from strawberry.types.unset import UNSET from .field import field if TYPE_CHECKING: from .schema_directives import Key T = TypeVar("T", bound=builtins.type) def _impl_type( cls: T | None, *, name: str | None = None, description: str | None = None, one_of: bool | None = None, directives: Iterable[object] = (), authenticated: bool = False, keys: Iterable[Union["Key", str]] = (), extend: bool = False, shareable: bool = False, inaccessible: bool = UNSET, policy: list[list[str]] | None = None, requires_scopes: list[list[str]] | None = None, tags: Iterable[str] = (), is_input: bool = False, is_interface: bool = False, is_interface_object: bool = False, ) -> T: from strawberry.federation.schema_directives import ( Authenticated, Inaccessible, InterfaceObject, Key, Policy, RequiresScopes, Shareable, Tag, ) from strawberry.schema_directives import OneOf directives = list(directives) directives.extend( Key(fields=key, resolvable=UNSET) if isinstance(key, str) else key for key in keys ) if authenticated: directives.append(Authenticated()) if inaccessible is not UNSET: directives.append(Inaccessible()) if policy: directives.append(Policy(policies=policy)) if requires_scopes: directives.append(RequiresScopes(scopes=requires_scopes)) if shareable: directives.append(Shareable()) if tags: directives.extend(Tag(name=tag) for tag in tags) if is_interface_object: directives.append(InterfaceObject()) if one_of: directives.append(OneOf()) return base_type( # type: ignore cls, name=name, description=description, directives=directives, extend=extend, is_input=is_input, is_interface=is_interface, ) @overload @dataclass_transform( order_default=True, kw_only_default=True, field_specifiers=(base_field, field, StrawberryField), ) def type( cls: T, *, name: str | None = None, description: str | None = None, directives: Iterable[object] = (), authenticated: bool = False, extend: bool = False, inaccessible: bool = UNSET, keys: Iterable[Union["Key", str]] = (), policy: list[list[str]] | None = None, requires_scopes: list[list[str]] | None = None, shareable: bool = False, tags: Iterable[str] = (), ) -> T: ... @overload @dataclass_transform( order_default=True, kw_only_default=True, field_specifiers=(base_field, field, StrawberryField), ) def type( *, name: str | None = None, description: str | None = None, directives: Iterable[object] = (), authenticated: bool = False, extend: bool = False, inaccessible: bool = UNSET, keys: Iterable[Union["Key", str]] = (), policy: list[list[str]] | None = None, requires_scopes: list[list[str]] | None = None, shareable: bool = False, tags: Iterable[str] = (), ) -> Callable[[T], T]: ... def type( cls: T | None = None, *, name: str | None = None, description: str | None = None, directives: Iterable[object] = (), authenticated: bool = False, extend: bool = False, inaccessible: bool = UNSET, keys: Iterable[Union["Key", str]] = (), policy: list[list[str]] | None = None, requires_scopes: list[list[str]] | None = None, shareable: bool = False, tags: Iterable[str] = (), ): return _impl_type( cls, name=name, description=description, directives=directives, authenticated=authenticated, keys=keys, extend=extend, inaccessible=inaccessible, policy=policy, requires_scopes=requires_scopes, shareable=shareable, tags=tags, ) @overload @dataclass_transform( order_default=True, kw_only_default=True, field_specifiers=(base_field, field, StrawberryField), ) def input( cls: T, *, name: str | None = None, one_of: bool | None = None, description: str | None = None, directives: Sequence[object] = (), inaccessible: bool = UNSET, tags: Iterable[str] = (), ) -> T: ... @overload @dataclass_transform( order_default=True, kw_only_default=True, field_specifiers=(base_field, field, StrawberryField), ) def input( *, name: str | None = None, description: str | None = None, one_of: bool | None = None, directives: Sequence[object] = (), inaccessible: bool = UNSET, tags: Iterable[str] = (), ) -> Callable[[T], T]: ... def input( cls: T | None = None, *, name: str | None = None, one_of: bool | None = None, description: str | None = None, directives: Sequence[object] = (), inaccessible: bool = UNSET, tags: Iterable[str] = (), ): return _impl_type( cls, name=name, description=description, directives=directives, inaccessible=inaccessible, is_input=True, one_of=one_of, tags=tags, ) @overload @dataclass_transform( order_default=True, kw_only_default=True, field_specifiers=(base_field, field, StrawberryField), ) def interface( cls: T, *, name: str | None = None, description: str | None = None, directives: Iterable[object] = (), authenticated: bool = False, inaccessible: bool = UNSET, keys: Iterable[Union["Key", str]] = (), policy: list[list[str]] | None = None, requires_scopes: list[list[str]] | None = None, tags: Iterable[str] = (), ) -> T: ... @overload @dataclass_transform( order_default=True, kw_only_default=True, field_specifiers=(base_field, field, StrawberryField), ) def interface( *, name: str | None = None, description: str | None = None, directives: Iterable[object] = (), authenticated: bool = False, inaccessible: bool = UNSET, keys: Iterable[Union["Key", str]] = (), policy: list[list[str]] | None = None, requires_scopes: list[list[str]] | None = None, tags: Iterable[str] = (), ) -> Callable[[T], T]: ... def interface( cls: T | None = None, *, name: str | None = None, description: str | None = None, directives: Iterable[object] = (), authenticated: bool = False, inaccessible: bool = UNSET, keys: Iterable[Union["Key", str]] = (), policy: list[list[str]] | None = None, requires_scopes: list[list[str]] | None = None, tags: Iterable[str] = (), ): return _impl_type( cls, name=name, description=description, directives=directives, authenticated=authenticated, keys=keys, inaccessible=inaccessible, policy=policy, requires_scopes=requires_scopes, tags=tags, is_interface=True, ) @overload @dataclass_transform( order_default=True, kw_only_default=True, field_specifiers=(base_field, field, StrawberryField), ) def interface_object( cls: T, *, name: str | None = None, description: str | None = None, directives: Iterable[object] = (), authenticated: bool = False, inaccessible: bool = UNSET, keys: Iterable[Union["Key", str]] = (), policy: list[list[str]] | None = None, requires_scopes: list[list[str]] | None = None, tags: Iterable[str] = (), ) -> T: ... @overload @dataclass_transform( order_default=True, kw_only_default=True, field_specifiers=(base_field, field, StrawberryField), ) def interface_object( *, name: str | None = None, description: str | None = None, directives: Iterable[object] = (), authenticated: bool = False, inaccessible: bool = UNSET, keys: Iterable[Union["Key", str]] = (), policy: list[list[str]] | None = None, requires_scopes: list[list[str]] | None = None, tags: Iterable[str] = (), ) -> Callable[[T], T]: ... def interface_object( cls: T | None = None, *, name: str | None = None, description: str | None = None, directives: Iterable[object] = (), authenticated: bool = False, inaccessible: bool = UNSET, keys: Iterable[Union["Key", str]] = (), policy: list[list[str]] | None = None, requires_scopes: list[list[str]] | None = None, tags: Iterable[str] = (), ): return _impl_type( cls, name=name, description=description, directives=directives, authenticated=authenticated, keys=keys, inaccessible=inaccessible, policy=policy, requires_scopes=requires_scopes, tags=tags, is_interface=False, is_interface_object=True, ) __all__ = ["input", "interface", "interface_object", "type"] strawberry-graphql-0.287.0/strawberry/federation/scalar.py000066400000000000000000000104761511033167500237050ustar00rootroot00000000000000from collections.abc import Callable, Iterable from typing import ( Any, NewType, TypeVar, overload, ) from strawberry.types.scalar import ScalarWrapper, _process_scalar _T = TypeVar("_T", bound=type | NewType) def identity(x: _T) -> _T: # pragma: no cover return x @overload def scalar( *, name: str | None = None, description: str | None = None, specified_by_url: str | None = None, serialize: Callable = identity, parse_value: Callable | None = None, parse_literal: Callable | None = None, directives: Iterable[object] = (), authenticated: bool = False, inaccessible: bool = False, policy: list[list[str]] | None = None, requires_scopes: list[list[str]] | None = None, tags: Iterable[str] | None = (), ) -> Callable[[_T], _T]: ... @overload def scalar( cls: _T, *, name: str | None = None, description: str | None = None, specified_by_url: str | None = None, serialize: Callable = identity, parse_value: Callable | None = None, parse_literal: Callable | None = None, directives: Iterable[object] = (), authenticated: bool = False, inaccessible: bool = False, policy: list[list[str]] | None = None, requires_scopes: list[list[str]] | None = None, tags: Iterable[str] | None = (), ) -> _T: ... def scalar( cls: _T | None = None, *, name: str | None = None, description: str | None = None, specified_by_url: str | None = None, serialize: Callable = identity, parse_value: Callable | None = None, parse_literal: Callable | None = None, directives: Iterable[object] = (), authenticated: bool = False, inaccessible: bool = False, policy: list[list[str]] | None = None, requires_scopes: list[list[str]] | None = None, tags: Iterable[str] | None = (), ) -> Any: """Annotates a class or type as a GraphQL custom scalar. Args: cls: The class or type to annotate name: The GraphQL name of the scalar description: The description of the scalar specified_by_url: The URL of the specification serialize: The function to serialize the scalar parse_value: The function to parse the value parse_literal: The function to parse the literal directives: The directives to apply to the scalar authenticated: Whether to add the @authenticated directive inaccessible: Whether to add the @inaccessible directive policy: The list of policy names to add to the @policy directive requires_scopes: The list of scopes to add to the @requires directive tags: The list of tags to add to the @tag directive Returns: The decorated class or type Example usages: ```python strawberry.federation.scalar( datetime.date, serialize=lambda value: value.isoformat(), parse_value=datetime.parse_date, ) Base64Encoded = strawberry.federation.scalar( NewType("Base64Encoded", bytes), serialize=base64.b64encode, parse_value=base64.b64decode, ) @strawberry.federation.scalar( serialize=lambda value: ",".join(value.items), parse_value=lambda value: CustomList(value.split(",")), ) class CustomList: def __init__(self, items): self.items = items ``` """ from strawberry.federation.schema_directives import ( Authenticated, Inaccessible, Policy, RequiresScopes, Tag, ) if parse_value is None: parse_value = cls directives = list(directives) if authenticated: directives.append(Authenticated()) if inaccessible: directives.append(Inaccessible()) if policy: directives.append(Policy(policies=policy)) if requires_scopes: directives.append(RequiresScopes(scopes=requires_scopes)) if tags: directives.extend(Tag(name=tag) for tag in tags) def wrap(cls: _T) -> ScalarWrapper: return _process_scalar( cls, name=name, description=description, specified_by_url=specified_by_url, serialize=serialize, parse_value=parse_value, parse_literal=parse_literal, directives=directives, ) if cls is None: return wrap return wrap(cls) __all__ = ["scalar"] strawberry-graphql-0.287.0/strawberry/federation/schema.py000066400000000000000000000343751511033167500237040ustar00rootroot00000000000000from collections import defaultdict from collections.abc import Iterable, Mapping from functools import cached_property from itertools import chain from typing import ( TYPE_CHECKING, Any, Literal, NewType, Optional, Union, cast, ) from strawberry.annotation import StrawberryAnnotation from strawberry.printer import print_schema from strawberry.schema import Schema as BaseSchema from strawberry.types.base import ( StrawberryContainer, StrawberryObjectDefinition, WithStrawberryObjectDefinition, get_object_definition, ) from strawberry.types.info import Info from strawberry.types.scalar import scalar from strawberry.types.union import StrawberryUnion from strawberry.utils.inspect import get_func_args from .schema_directive import StrawberryFederationSchemaDirective from .versions import format_version, parse_version if TYPE_CHECKING: from graphql import ExecutionContext as GraphQLExecutionContext from strawberry.extensions import SchemaExtension from strawberry.federation.schema_directives import ComposeDirective from strawberry.schema.config import StrawberryConfig from strawberry.schema_directive import StrawberrySchemaDirective from strawberry.types.enum import StrawberryEnumDefinition from strawberry.types.scalar import ScalarDefinition, ScalarWrapper FederationAny = scalar(NewType("_Any", object), name="_Any") # type: ignore class Schema(BaseSchema): def __init__( self, query: type | None = None, mutation: type | None = None, subscription: type | None = None, # TODO: we should update directives' type in the main schema directives: Iterable[type] = (), types: Iterable[type] = (), extensions: Iterable[Union[type["SchemaExtension"], "SchemaExtension"]] = (), execution_context_class: type["GraphQLExecutionContext"] | None = None, config: Optional["StrawberryConfig"] = None, scalar_overrides: dict[object, Union[type, "ScalarWrapper", "ScalarDefinition"]] | None = None, schema_directives: Iterable[object] = (), federation_version: Literal[ "2.0", "2.1", "2.2", "2.3", "2.4", "2.5", "2.6", "2.7", "2.8", "2.9", "2.10", "2.11", ] = "2.11", ) -> None: # Convert version string (e.g., "2.5") to version tuple (e.g., (2, 5)) self.federation_version = parse_version(federation_version) query = self._get_federation_query_type(query, mutation, subscription, types) types = [*types, FederationAny] super().__init__( query=query, mutation=mutation, subscription=subscription, directives=directives, # type: ignore types=types, extensions=extensions, execution_context_class=execution_context_class, config=config, scalar_overrides=scalar_overrides, schema_directives=schema_directives, ) self.schema_directives = list(schema_directives) # Validate directive compatibility with federation version self._validate_directive_compatibility() composed_directives = self._add_compose_directives() self._add_link_directives(composed_directives) # type: ignore def _get_federation_query_type( self, query: type[WithStrawberryObjectDefinition] | None, mutation: type[WithStrawberryObjectDefinition] | None, subscription: type[WithStrawberryObjectDefinition] | None, additional_types: Iterable[type[WithStrawberryObjectDefinition]], ) -> type: """Returns a new query type that includes the _service field. If the query type is provided, it will be used as the base for the new query type. Otherwise, a new query type will be created. Federation needs the following two fields to be present in the query type: - _service: This field is used by the gateway to query for the capabilities of the federated service. - _entities: This field is used by the gateway to query for the entities that are part of the federated service. The _service field is added by default, but the _entities field is only added if the schema contains an entity type. """ import strawberry from strawberry.tools.create_type import create_type from strawberry.tools.merge_types import merge_types @strawberry.type(name="_Service") class Service: sdl: str = strawberry.field( resolver=lambda: print_schema(self), ) @strawberry.field(name="_service") def service() -> Service: return Service() fields = [service] entity_type = _get_entity_type(query, mutation, subscription, additional_types) if entity_type: self.entities_resolver.__annotations__["return"] = list[ entity_type | None # type: ignore ] entities_field = strawberry.field( name="_entities", resolver=self.entities_resolver ) fields.insert(0, entities_field) FederationQuery = create_type(name="Query", fields=fields) if query is None: return FederationQuery query_type = merge_types( "Query", (FederationQuery, query), ) # TODO: this should be probably done in merge_types if query.__strawberry_definition__.extend: query_type.__strawberry_definition__.extend = True # type: ignore return query_type def entities_resolver( self, info: Info, representations: list[FederationAny] ) -> list[FederationAny]: results = [] for representation in representations: type_name = representation.pop("__typename") type_ = self.schema_converter.type_map[type_name] definition = cast("StrawberryObjectDefinition", type_.definition) if hasattr(definition.origin, "resolve_reference"): resolve_reference = definition.origin.resolve_reference func_args = get_func_args(resolve_reference) kwargs = representation # TODO: use the same logic we use for other resolvers if "info" in func_args: kwargs["info"] = info try: result = resolve_reference(**kwargs) except Exception as e: # noqa: BLE001 result = e else: from strawberry.types.arguments import convert_argument config = info.schema.config scalar_registry = info.schema.schema_converter.scalar_registry try: result = convert_argument( representation, type_=definition.origin, scalar_registry=scalar_registry, config=config, ) except Exception: # noqa: BLE001 result = TypeError(f"Unable to resolve reference for {type_name}") results.append(result) return results @cached_property def schema_directives_in_use(self) -> list[object]: all_graphql_types = self._schema.type_map.values() directives: list[object] = [] for type_ in all_graphql_types: strawberry_definition = type_.extensions.get("strawberry-definition") if not strawberry_definition: continue directives.extend(strawberry_definition.directives) fields = getattr(strawberry_definition, "fields", []) values = getattr(strawberry_definition, "values", []) for field in chain(fields, values): directives.extend(field.directives) return directives def _add_link_for_composed_directive( self, directive: "StrawberrySchemaDirective", directive_by_url: Mapping[str, set[str]], ) -> None: if not isinstance(directive, StrawberryFederationSchemaDirective): return if not directive.compose_options: return import_url = directive.compose_options.import_url name = self.config.name_converter.from_directive(directive) # import url is required by Apollo Federation, this might change in # future to be optional, so for now, when it is not passed we # define a mock one. The URL isn't used for validation anyway. if import_url is None: import_url = f"https://directives.strawberry.rocks/{name}/v0.1" directive_by_url[import_url].add(f"@{name}") def _add_link_for_federation_directive( self, directive: object, directive_by_url: Mapping[str, set[str]], ) -> None: from .schema_directives import FederationDirective if not isinstance(directive, FederationDirective): return # Use the schema's federation version to construct the URL url = f"https://specs.apollo.dev/federation/{format_version(self.federation_version)}" name = directive.imported_from.name directive_by_url[url].add(f"@{name}") def _add_link_directives( self, additional_directives: list[object] | None = None ) -> None: from .schema_directives import Link directive_by_url: defaultdict[str, set[str]] = defaultdict(set) additional_directives = additional_directives or [] for directive in self.schema_directives_in_use + additional_directives: definition = directive.__strawberry_directive__ # type: ignore self._add_link_for_composed_directive(definition, directive_by_url) self._add_link_for_federation_directive(directive, directive_by_url) link_directives: list[object] = [ Link( url=url, import_=sorted(directives), # type: ignore[arg-type] ) for url, directives in directive_by_url.items() ] self.schema_directives = self.schema_directives + link_directives def _add_compose_directives(self) -> list["ComposeDirective"]: from .schema_directives import ComposeDirective compose_directives: list[ComposeDirective] = [] for directive in self.schema_directives_in_use: definition = directive.__strawberry_directive__ # type: ignore is_federation_schema_directive = isinstance( definition, StrawberryFederationSchemaDirective ) if is_federation_schema_directive and definition.compose_options: name = self.config.name_converter.from_directive(definition) compose_directives.append( ComposeDirective( name=f"@{name}", ) ) self.schema_directives = self.schema_directives + compose_directives return compose_directives def _validate_directive_compatibility(self) -> None: """Validate that all federation directives are compatible with the schema's federation version.""" from .schema_directives import FederationDirective from .versions import format_version for directive in self.schema_directives_in_use: if isinstance(directive, FederationDirective) and hasattr( directive.__class__, "minimum_version" ): min_version = directive.__class__.minimum_version if self.federation_version < min_version: raise ValueError( f"Directive @{directive.imported_from.name} requires " f"federation version {format_version(min_version)} or higher, " f"but schema uses version {format_version(self.federation_version)}" ) def _warn_for_federation_directives(self) -> None: # this is used in the main schema to raise if there's a directive # that's for federation, but in this class we don't want to warn, # since it is expected to have federation directives pass def _get_entity_type( query: type[WithStrawberryObjectDefinition] | None, mutation: type[WithStrawberryObjectDefinition] | None, subscription: type[WithStrawberryObjectDefinition] | None, additional_types: Iterable[type[WithStrawberryObjectDefinition]], ) -> StrawberryUnion | None: # recursively iterate over the schema to find all types annotated with @key # if no types are annotated with @key, then the _Entity union and Query._entities # field should not be added to the schema entity_types = set() # need a stack to keep track of the types we need to visit stack: list[Any] = [query, mutation, subscription, *additional_types] seen = set() while stack: type_ = stack.pop() if type_ is None: continue while isinstance(type_, StrawberryContainer): type_ = type_.of_type type_definition = get_object_definition(type_, strict=False) if type_definition is None: continue if type_definition.is_object_type and _has_federation_keys(type_definition): entity_types.add(type_) for field in type_definition.fields: if field.type and field.type in seen: continue seen.add(field.type) stack.append(field.type) if not entity_types: return None sorted_types = sorted(entity_types, key=lambda t: t.__strawberry_definition__.name) return StrawberryUnion( "_Entity", type_annotations=tuple(StrawberryAnnotation(type_) for type_ in sorted_types), ) def _is_key(directive: Any) -> bool: from .schema_directives import Key return isinstance(directive, Key) def _has_federation_keys( definition: Union[ StrawberryObjectDefinition, "ScalarDefinition", "StrawberryEnumDefinition", "StrawberryUnion", ], ) -> bool: if isinstance(definition, StrawberryObjectDefinition): return any(_is_key(directive) for directive in definition.directives or []) return False __all__ = ["Schema"] strawberry-graphql-0.287.0/strawberry/federation/schema_directive.py000066400000000000000000000033531511033167500257320ustar00rootroot00000000000000import dataclasses from collections.abc import Callable from typing import TypeVar from typing_extensions import dataclass_transform from strawberry.directive import directive_field from strawberry.schema_directive import Location, StrawberrySchemaDirective from strawberry.types.field import StrawberryField, field from strawberry.types.object_type import _wrap_dataclass from strawberry.types.type_resolver import _get_fields @dataclasses.dataclass class ComposeOptions: import_url: str | None @dataclasses.dataclass class StrawberryFederationSchemaDirective(StrawberrySchemaDirective): compose_options: ComposeOptions | None = None T = TypeVar("T", bound=type) @dataclass_transform( order_default=True, kw_only_default=True, field_specifiers=(directive_field, field, StrawberryField), ) def schema_directive( *, locations: list[Location], description: str | None = None, name: str | None = None, repeatable: bool = False, print_definition: bool = True, compose: bool = False, import_url: str | None = None, ) -> Callable[[T], T]: def _wrap(cls: T) -> T: cls = _wrap_dataclass(cls) # type: ignore fields = _get_fields(cls, {}) cls.__strawberry_directive__ = StrawberryFederationSchemaDirective( # type: ignore[attr-defined] python_name=cls.__name__, graphql_name=name, locations=locations, description=description, repeatable=repeatable, fields=fields, print_definition=print_definition, origin=cls, compose_options=ComposeOptions(import_url=import_url) if compose else None, ) return cls return _wrap __all__ = ["Location", "schema_directive"] strawberry-graphql-0.287.0/strawberry/federation/schema_directives.py000066400000000000000000000223151511033167500261140ustar00rootroot00000000000000from dataclasses import dataclass from typing import ClassVar from strawberry import directive_field from strawberry.schema_directive import Location, schema_directive from strawberry.types.unset import UNSET from .types import ( FieldSet, LinkImport, LinkPurpose, ) from .versions import FederationVersion @dataclass class ImportedFrom: name: str url: str = "https://specs.apollo.dev/federation/v2.7" def with_version(self, version: str) -> "ImportedFrom": """Return a new ImportedFrom with the URL updated for the given version.""" return ImportedFrom( name=self.name, url=f"https://specs.apollo.dev/federation/{version}" ) class FederationDirective: imported_from: ClassVar[ImportedFrom] minimum_version: ClassVar[FederationVersion] @schema_directive( locations=[Location.FIELD_DEFINITION], name="external", print_definition=False ) class External(FederationDirective): imported_from: ClassVar[ImportedFrom] = ImportedFrom( name="external", url="https://specs.apollo.dev/federation/v2.7" ) minimum_version: ClassVar[FederationVersion] = (2, 0) @schema_directive( locations=[Location.FIELD_DEFINITION], name="requires", print_definition=False ) class Requires(FederationDirective): fields: FieldSet imported_from: ClassVar[ImportedFrom] = ImportedFrom( name="requires", url="https://specs.apollo.dev/federation/v2.7" ) minimum_version: ClassVar[FederationVersion] = (2, 0) @schema_directive( locations=[Location.FIELD_DEFINITION], name="provides", print_definition=False ) class Provides(FederationDirective): fields: FieldSet imported_from: ClassVar[ImportedFrom] = ImportedFrom( name="provides", url="https://specs.apollo.dev/federation/v2.7" ) minimum_version: ClassVar[FederationVersion] = (2, 0) @schema_directive( locations=[Location.OBJECT, Location.INTERFACE], name="key", repeatable=True, print_definition=False, ) class Key(FederationDirective): fields: FieldSet resolvable: bool | None = True imported_from: ClassVar[ImportedFrom] = ImportedFrom( name="key", url="https://specs.apollo.dev/federation/v2.7" ) minimum_version: ClassVar[FederationVersion] = (2, 0) @schema_directive( locations=[Location.FIELD_DEFINITION, Location.OBJECT], name="shareable", repeatable=True, print_definition=False, ) class Shareable(FederationDirective): imported_from: ClassVar[ImportedFrom] = ImportedFrom( name="shareable", url="https://specs.apollo.dev/federation/v2.7" ) minimum_version: ClassVar[FederationVersion] = (2, 0) @schema_directive( locations=[Location.SCHEMA], name="link", repeatable=True, print_definition=False ) class Link: url: str | None as_: str | None = directive_field(name="as") for_: LinkPurpose | None = directive_field(name="for") import_: list[LinkImport | None] | None = directive_field(name="import") def __init__( self, url: str | None = UNSET, as_: str | None = UNSET, for_: LinkPurpose | None = UNSET, import_: list[LinkImport | None] | None = UNSET, ) -> None: self.url = url self.as_ = as_ self.for_ = for_ self.import_ = import_ @schema_directive( locations=[ Location.FIELD_DEFINITION, Location.INTERFACE, Location.OBJECT, Location.UNION, Location.ARGUMENT_DEFINITION, Location.SCALAR, Location.ENUM, Location.ENUM_VALUE, Location.INPUT_OBJECT, Location.INPUT_FIELD_DEFINITION, ], name="tag", repeatable=True, print_definition=False, ) class Tag(FederationDirective): name: str imported_from: ClassVar[ImportedFrom] = ImportedFrom( name="tag", url="https://specs.apollo.dev/federation/v2.7" ) minimum_version: ClassVar[FederationVersion] = (2, 0) @schema_directive( locations=[Location.FIELD_DEFINITION], name="override", print_definition=False ) class Override(FederationDirective): override_from: str = directive_field(name="from") label: str | None = UNSET imported_from: ClassVar[ImportedFrom] = ImportedFrom( name="override", url="https://specs.apollo.dev/federation/v2.7" ) minimum_version: ClassVar[FederationVersion] = (2, 0) @schema_directive( locations=[ Location.FIELD_DEFINITION, Location.OBJECT, Location.INTERFACE, Location.UNION, Location.ARGUMENT_DEFINITION, Location.SCALAR, Location.ENUM, Location.ENUM_VALUE, Location.INPUT_OBJECT, Location.INPUT_FIELD_DEFINITION, ], name="inaccessible", print_definition=False, ) class Inaccessible(FederationDirective): imported_from: ClassVar[ImportedFrom] = ImportedFrom( name="inaccessible", url="https://specs.apollo.dev/federation/v2.7" ) minimum_version: ClassVar[FederationVersion] = (2, 0) @schema_directive( locations=[Location.SCHEMA], name="composeDirective", print_definition=False ) class ComposeDirective(FederationDirective): name: str imported_from: ClassVar[ImportedFrom] = ImportedFrom( name="composeDirective", url="https://specs.apollo.dev/federation/v2.7" ) minimum_version: ClassVar[FederationVersion] = (2, 1) @schema_directive( locations=[Location.OBJECT], name="interfaceObject", print_definition=False ) class InterfaceObject(FederationDirective): imported_from: ClassVar[ImportedFrom] = ImportedFrom( name="interfaceObject", url="https://specs.apollo.dev/federation/v2.7" ) minimum_version: ClassVar[FederationVersion] = (2, 3) @schema_directive( locations=[ Location.FIELD_DEFINITION, Location.OBJECT, Location.INTERFACE, Location.SCALAR, Location.ENUM, ], name="authenticated", print_definition=False, ) class Authenticated(FederationDirective): imported_from: ClassVar[ImportedFrom] = ImportedFrom( name="authenticated", url="https://specs.apollo.dev/federation/v2.7" ) minimum_version: ClassVar[FederationVersion] = (2, 5) @schema_directive( locations=[ Location.FIELD_DEFINITION, Location.OBJECT, Location.INTERFACE, Location.SCALAR, Location.ENUM, ], name="requiresScopes", print_definition=False, ) class RequiresScopes(FederationDirective): scopes: "list[list[str]]" imported_from: ClassVar[ImportedFrom] = ImportedFrom( name="requiresScopes", url="https://specs.apollo.dev/federation/v2.7" ) minimum_version: ClassVar[FederationVersion] = (2, 5) @schema_directive( locations=[ Location.FIELD_DEFINITION, Location.OBJECT, Location.INTERFACE, Location.SCALAR, Location.ENUM, ], name="policy", print_definition=False, ) class Policy(FederationDirective): policies: "list[list[str]]" imported_from: ClassVar[ImportedFrom] = ImportedFrom( name="policy", url="https://specs.apollo.dev/federation/v2.7" ) minimum_version: ClassVar[FederationVersion] = (2, 6) @schema_directive( locations=[Location.OBJECT, Location.INTERFACE, Location.UNION], name="context", print_definition=False, ) class Context(FederationDirective): name: str imported_from: ClassVar[ImportedFrom] = ImportedFrom( name="context", url="https://specs.apollo.dev/federation/v2.7" ) minimum_version: ClassVar[FederationVersion] = (2, 8) @schema_directive( locations=[Location.ARGUMENT_DEFINITION], name="fromContext", print_definition=False, ) class FromContext(FederationDirective): field: str imported_from: ClassVar[ImportedFrom] = ImportedFrom( name="fromContext", url="https://specs.apollo.dev/federation/v2.7" ) minimum_version: ClassVar[FederationVersion] = (2, 8) @schema_directive( locations=[ Location.ARGUMENT_DEFINITION, Location.ENUM, Location.FIELD_DEFINITION, Location.INPUT_FIELD_DEFINITION, Location.OBJECT, Location.SCALAR, Location.ENUM, ], name="cost", print_definition=False, ) class Cost(FederationDirective): weight: int imported_from: ClassVar[ImportedFrom] = ImportedFrom( name="cost", url="https://specs.apollo.dev/federation/v2.7" ) minimum_version: ClassVar[FederationVersion] = (2, 9) @schema_directive( locations=[Location.FIELD_DEFINITION], name="listSize", print_definition=False, ) class ListSize(FederationDirective): assumed_size: int | None = directive_field(name="assumedSize") slicing_arguments: list[str] | None = directive_field(name="slicingArguments") sized_fields: list[str] | None = directive_field(name="sizedFields") require_one_slicing_argument: bool | None = True imported_from: ClassVar[ImportedFrom] = ImportedFrom( name="listSize", url="https://specs.apollo.dev/federation/v2.7" ) minimum_version: ClassVar[FederationVersion] = (2, 9) __all__ = [ "Authenticated", "ComposeDirective", "External", "FederationDirective", "ImportedFrom", "Inaccessible", "InterfaceObject", "Key", "Link", "Override", "Policy", "Provides", "Requires", "RequiresScopes", "Shareable", "Tag", ] strawberry-graphql-0.287.0/strawberry/federation/types.py000066400000000000000000000005501511033167500235740ustar00rootroot00000000000000from enum import Enum from strawberry.types.enum import enum from strawberry.types.scalar import scalar FieldSet = scalar(str, name="_FieldSet") LinkImport = scalar(object, name="link__Import") @enum(name="link__Purpose") class LinkPurpose(Enum): SECURITY = "SECURITY" EXECUTION = "EXECUTION" __all__ = ["FieldSet", "LinkImport", "LinkPurpose"] strawberry-graphql-0.287.0/strawberry/federation/union.py000066400000000000000000000031751511033167500235660ustar00rootroot00000000000000from collections.abc import Collection, Iterable from typing import Any from strawberry.types.union import StrawberryUnion from strawberry.types.union import union as base_union def union( name: str, types: Collection[type[Any]] | None = None, *, description: str | None = None, directives: Iterable[object] = (), inaccessible: bool = False, tags: Iterable[str] | None = (), ) -> StrawberryUnion: """Creates a new named Union type. Args: name: The GraphQL name of the Union type. types: The types that the Union can be. (Deprecated, use `Annotated[U, strawberry.federation.union("Name")]` instead) description: The GraphQL description of the Union type. directives: The directives to attach to the Union type. inaccessible: Whether the Union type is inaccessible. tags: The federation tags to attach to the Union type. Example usages: ```python import strawberry from typing import Annotated @strawberry.federation.type(keys=["id"]) class A: id: strawberry.ID @strawberry.federation.type(keys=["id"]) class B: id: strawberry.ID MyUnion = Annotated[A | B, strawberry.federation.union("Name", tags=["tag"])] """ from strawberry.federation.schema_directives import Inaccessible, Tag directives = list(directives) if inaccessible: directives.append(Inaccessible()) if tags: directives.extend(Tag(name=tag) for tag in tags) return base_union( name, types, description=description, directives=directives, ) __all__ = ["union"] strawberry-graphql-0.287.0/strawberry/federation/versions.py000066400000000000000000000014471511033167500243060ustar00rootroot00000000000000# Type alias for federation version as (major, minor) tuple FederationVersion = tuple[int, int] # Mapping from version string (e.g., "2.5") to version tuple (e.g., (2, 5)) FEDERATION_VERSIONS: dict[str, FederationVersion] = { "2.0": (2, 0), "2.1": (2, 1), "2.2": (2, 2), "2.3": (2, 3), "2.4": (2, 4), "2.5": (2, 5), "2.6": (2, 6), "2.7": (2, 7), "2.8": (2, 8), "2.9": (2, 9), "2.10": (2, 10), "2.11": (2, 11), } def parse_version(version_str: str) -> FederationVersion: """Parse a version string like '2.5' into a tuple (2, 5).""" return FEDERATION_VERSIONS[version_str] def format_version(version: FederationVersion) -> str: """Format a version tuple (2, 5) into a string 'v2.5'.""" major, minor = version return f"v{major}.{minor}" strawberry-graphql-0.287.0/strawberry/field_extensions/000077500000000000000000000000001511033167500233005ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/field_extensions/__init__.py000066400000000000000000000001401511033167500254040ustar00rootroot00000000000000from .input_mutation import InputMutationExtension __all__ = [ "InputMutationExtension", ] strawberry-graphql-0.287.0/strawberry/field_extensions/input_mutation.py000066400000000000000000000052031511033167500267310ustar00rootroot00000000000000from __future__ import annotations from typing import ( TYPE_CHECKING, Any, ) import strawberry from strawberry.annotation import StrawberryAnnotation from strawberry.extensions.field_extension import ( AsyncExtensionResolver, FieldExtension, SyncExtensionResolver, ) from strawberry.types.arguments import StrawberryArgument from strawberry.types.field import StrawberryField from strawberry.utils.str_converters import capitalize_first, to_camel_case if TYPE_CHECKING: from strawberry.types.info import Info class InputMutationExtension(FieldExtension): def apply(self, field: StrawberryField) -> None: resolver = field.base_resolver assert resolver name = field.graphql_name or to_camel_case(resolver.name) type_dict: dict[str, Any] = { "__doc__": f"Input data for `{name}` mutation", "__annotations__": {}, } annotations = resolver.wrapped_func.__annotations__ for arg in resolver.arguments: arg_field = StrawberryField( python_name=arg.python_name, graphql_name=arg.graphql_name, description=arg.description, default=arg.default, type_annotation=arg.type_annotation, directives=tuple(arg.directives), ) type_dict[arg_field.python_name] = arg_field type_dict["__annotations__"][arg_field.python_name] = annotations[ arg.python_name ] caps_name = capitalize_first(name) new_type = strawberry.input(type(f"{caps_name}Input", (), type_dict)) field.arguments = [ StrawberryArgument( python_name="input", graphql_name=None, type_annotation=StrawberryAnnotation( new_type, namespace=resolver._namespace, ), description=type_dict["__doc__"], ) ] def resolve( self, next_: SyncExtensionResolver, source: Any, info: Info, **kwargs: Any, ) -> Any: input_args = kwargs.pop("input") return next_( source, info, **kwargs, **vars(input_args), ) async def resolve_async( self, next_: AsyncExtensionResolver, source: Any, info: Info, **kwargs: Any, ) -> Any: input_args = kwargs.pop("input") return await next_( source, info, **kwargs, **vars(input_args), ) __all__ = ["InputMutationExtension"] strawberry-graphql-0.287.0/strawberry/file_uploads/000077500000000000000000000000001511033167500224045ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/file_uploads/__init__.py000066400000000000000000000000621511033167500245130ustar00rootroot00000000000000from .scalars import Upload __all__ = ["Upload"] strawberry-graphql-0.287.0/strawberry/file_uploads/scalars.py000066400000000000000000000002411511033167500244030ustar00rootroot00000000000000from typing import NewType from strawberry.types.scalar import scalar Upload = scalar(NewType("Upload", bytes), parse_value=lambda x: x) __all__ = ["Upload"] strawberry-graphql-0.287.0/strawberry/file_uploads/utils.py000066400000000000000000000022351511033167500241200ustar00rootroot00000000000000import copy from collections.abc import Mapping from typing import Any def replace_placeholders_with_files( operations_with_placeholders: dict[str, Any], files_map: Mapping[str, Any], files: Mapping[str, Any], ) -> dict[str, Any]: # TODO: test this with missing variables in operations_with_placeholders operations = copy.deepcopy(operations_with_placeholders) for multipart_form_field_name, operations_paths in files_map.items(): file_object = files[multipart_form_field_name] for path in operations_paths: operations_path_keys = path.split(".") value_key = operations_path_keys.pop() target_object = operations for key in operations_path_keys: if isinstance(target_object, list): target_object = target_object[int(key)] else: target_object = target_object[key] if isinstance(target_object, list): target_object[int(value_key)] = file_object else: target_object[value_key] = file_object return operations __all__ = ["replace_placeholders_with_files"] strawberry-graphql-0.287.0/strawberry/flask/000077500000000000000000000000001511033167500210365ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/flask/__init__.py000066400000000000000000000000001511033167500231350ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/flask/views.py000066400000000000000000000107551511033167500225550ustar00rootroot00000000000000from __future__ import annotations import warnings from typing import ( TYPE_CHECKING, ClassVar, TypeGuard, ) from lia import AsyncFlaskHTTPRequestAdapter, FlaskHTTPRequestAdapter, HTTPException from flask import Request, Response, render_template_string, request from flask.views import View from strawberry.http.async_base_view import AsyncBaseHTTPView from strawberry.http.sync_base_view import SyncBaseHTTPView from strawberry.http.typevars import Context, RootValue if TYPE_CHECKING: from flask.typing import ResponseReturnValue from strawberry.http import GraphQLHTTPResponse from strawberry.http.ides import GraphQL_IDE from strawberry.schema.base import BaseSchema class BaseGraphQLView: graphql_ide: GraphQL_IDE | None def __init__( self, schema: BaseSchema, graphiql: bool | None = None, graphql_ide: GraphQL_IDE | None = "graphiql", allow_queries_via_get: bool = True, multipart_uploads_enabled: bool = False, ) -> None: self.schema = schema self.graphiql = graphiql self.allow_queries_via_get = allow_queries_via_get self.multipart_uploads_enabled = multipart_uploads_enabled if graphiql is not None: warnings.warn( "The `graphiql` argument is deprecated in favor of `graphql_ide`", DeprecationWarning, stacklevel=2, ) self.graphql_ide = "graphiql" if graphiql else None else: self.graphql_ide = graphql_ide def create_response( self, response_data: GraphQLHTTPResponse | list[GraphQLHTTPResponse], sub_response: Response, ) -> Response: sub_response.set_data(self.encode_json(response_data)) # type: ignore return sub_response class GraphQLView( BaseGraphQLView, SyncBaseHTTPView[Request, Response, Response, Context, RootValue], View, ): methods: ClassVar[list[str]] = ["GET", "POST"] allow_queries_via_get: bool = True request_adapter_class = FlaskHTTPRequestAdapter def get_context(self, request: Request, response: Response) -> Context: return {"request": request, "response": response} # type: ignore def get_root_value(self, request: Request) -> RootValue | None: return None def get_sub_response(self, request: Request) -> Response: return Response(status=200, content_type="application/json") def dispatch_request(self) -> ResponseReturnValue: try: return self.run(request=request) except HTTPException as e: return Response( response=e.reason, status=e.status_code, content_type="text/plain", ) def render_graphql_ide(self, request: Request) -> Response: return render_template_string(self.graphql_ide_html) # type: ignore class AsyncGraphQLView( BaseGraphQLView, AsyncBaseHTTPView[ Request, Response, Response, Request, Response, Context, RootValue ], View, ): methods: ClassVar[list[str]] = ["GET", "POST"] allow_queries_via_get: bool = True request_adapter_class = AsyncFlaskHTTPRequestAdapter async def get_context(self, request: Request, response: Response) -> Context: return {"request": request, "response": response} # type: ignore async def get_root_value(self, request: Request) -> RootValue | None: return None async def get_sub_response(self, request: Request) -> Response: return Response(status=200, content_type="application/json") async def dispatch_request(self) -> ResponseReturnValue: # type: ignore try: return await self.run(request=request) except HTTPException as e: return Response( response=e.reason, status=e.status_code, content_type="text/plain", ) async def render_graphql_ide(self, request: Request) -> Response: content = render_template_string(self.graphql_ide_html) return Response(content, status=200, content_type="text/html") def is_websocket_request(self, request: Request) -> TypeGuard[Request]: return False async def pick_websocket_subprotocol(self, request: Request) -> str | None: raise NotImplementedError async def create_websocket_response( self, request: Request, subprotocol: str | None ) -> Response: raise NotImplementedError __all__ = [ "AsyncGraphQLView", "GraphQLView", ] strawberry-graphql-0.287.0/strawberry/http/000077500000000000000000000000001511033167500207155ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/http/__init__.py000066400000000000000000000026341511033167500230330ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from typing import Any, Literal from typing_extensions import TypedDict from strawberry.schema._graphql_core import ( GraphQLIncrementalExecutionResults, ResultType, ) class GraphQLHTTPResponse(TypedDict, total=False): data: dict[str, object] | None errors: list[object] | None extensions: dict[str, object] | None hasNext: bool | None completed: list[Any] | None pending: list[Any] | None initial: list[Any] | None incremental: list[Any] | None def process_result(result: ResultType) -> GraphQLHTTPResponse: if isinstance(result, GraphQLIncrementalExecutionResults): return result errors, extensions = result.errors, result.extensions data: GraphQLHTTPResponse = { "data": result.data, **({"errors": [err.formatted for err in errors]} if errors else {}), **({"extensions": extensions} if extensions else {}), } return data @dataclass class GraphQLRequestData: # query is optional here as it can be added by an extensions # (for example an extension for persisted queries) query: str | None variables: dict[str, Any] | None operation_name: str | None extensions: dict[str, Any] | None protocol: Literal["http", "multipart-subscription"] = "http" __all__ = [ "GraphQLHTTPResponse", "GraphQLRequestData", "process_result", ] strawberry-graphql-0.287.0/strawberry/http/async_base_view.py000066400000000000000000000615341511033167500244410ustar00rootroot00000000000000import abc import asyncio import contextlib import json from collections.abc import AsyncGenerator, Callable, Mapping from datetime import timedelta from typing import ( Any, Generic, Literal, TypeGuard, cast, overload, ) from graphql import GraphQLError from lia import AsyncHTTPRequestAdapter, HTTPException from strawberry.exceptions import MissingQueryError from strawberry.file_uploads.utils import replace_placeholders_with_files from strawberry.http import ( GraphQLHTTPResponse, GraphQLRequestData, process_result, ) from strawberry.http.ides import GraphQL_IDE from strawberry.schema._graphql_core import ( GraphQLIncrementalExecutionResults, ) from strawberry.schema.base import BaseSchema from strawberry.schema.exceptions import ( CannotGetOperationTypeError, InvalidOperationTypeError, ) from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL from strawberry.subscriptions.protocols.graphql_transport_ws.handlers import ( BaseGraphQLTransportWSHandler, ) from strawberry.subscriptions.protocols.graphql_ws.handlers import BaseGraphQLWSHandler from strawberry.types import ExecutionResult, SubscriptionExecutionResult from strawberry.types.graphql import OperationType from strawberry.types.unset import UNSET, UnsetType from .base import BaseView from .parse_content_type import parse_content_type from .typevars import ( Context, Request, Response, RootValue, SubResponse, WebSocketRequest, WebSocketResponse, ) class AsyncWebSocketAdapter(abc.ABC): def __init__(self, view: "AsyncBaseHTTPView") -> None: self.view = view @abc.abstractmethod def iter_json( self, *, ignore_parsing_errors: bool = False ) -> AsyncGenerator[object, None]: ... @abc.abstractmethod async def send_json(self, message: Mapping[str, object]) -> None: ... @abc.abstractmethod async def close(self, code: int, reason: str) -> None: ... class AsyncBaseHTTPView( abc.ABC, BaseView[Request], Generic[ Request, Response, SubResponse, WebSocketRequest, WebSocketResponse, Context, RootValue, ], ): schema: BaseSchema graphql_ide: GraphQL_IDE | None keep_alive = False keep_alive_interval: float | None = None connection_init_wait_timeout: timedelta = timedelta(minutes=1) request_adapter_class: Callable[[Request], AsyncHTTPRequestAdapter] websocket_adapter_class: Callable[ [ "AsyncBaseHTTPView[Any, Any, Any, Any, Any, Context, RootValue]", WebSocketRequest, WebSocketResponse, ], AsyncWebSocketAdapter, ] graphql_transport_ws_handler_class: type[ BaseGraphQLTransportWSHandler[Context, RootValue] ] = BaseGraphQLTransportWSHandler[Context, RootValue] graphql_ws_handler_class: type[BaseGraphQLWSHandler[Context, RootValue]] = ( BaseGraphQLWSHandler[Context, RootValue] ) @property @abc.abstractmethod def allow_queries_via_get(self) -> bool: ... @abc.abstractmethod async def get_sub_response(self, request: Request) -> SubResponse: ... @abc.abstractmethod async def get_context( self, request: Request | WebSocketRequest, response: SubResponse | WebSocketResponse, ) -> Context: ... @abc.abstractmethod async def get_root_value( self, request: Request | WebSocketRequest ) -> RootValue | None: ... @abc.abstractmethod def create_response( self, response_data: GraphQLHTTPResponse | list[GraphQLHTTPResponse], sub_response: SubResponse, ) -> Response: ... @abc.abstractmethod async def render_graphql_ide(self, request: Request) -> Response: ... async def create_streaming_response( self, request: Request, stream: Callable[[], AsyncGenerator[str, None]], sub_response: SubResponse, headers: dict[str, str], ) -> Response: raise ValueError("Multipart responses are not supported") @abc.abstractmethod def is_websocket_request( self, request: Request | WebSocketRequest ) -> TypeGuard[WebSocketRequest]: ... @abc.abstractmethod async def pick_websocket_subprotocol( self, request: WebSocketRequest ) -> str | None: ... @abc.abstractmethod async def create_websocket_response( self, request: WebSocketRequest, subprotocol: str | None ) -> WebSocketResponse: ... async def execute_operation( self, request: Request, context: Context, root_value: RootValue | None, sub_response: SubResponse, ) -> ExecutionResult | list[ExecutionResult] | SubscriptionExecutionResult: request_adapter = self.request_adapter_class(request) try: request_data = await self.parse_http_body(request_adapter) except json.decoder.JSONDecodeError as e: raise HTTPException(400, "Unable to parse request body as JSON") from e # DO this only when doing files except KeyError as e: raise HTTPException(400, "File(s) missing in form data") from e allowed_operation_types = OperationType.from_http(request_adapter.method) if not self.allow_queries_via_get and request_adapter.method == "GET": allowed_operation_types = allowed_operation_types - {OperationType.QUERY} if isinstance(request_data, list): # batch GraphQL requests return await asyncio.gather( *[ self.execute_single( request=request, request_adapter=request_adapter, sub_response=sub_response, context=context, root_value=root_value, request_data=data, ) for data in request_data ] ) if request_data.protocol == "multipart-subscription": return await self.schema.subscribe( request_data.query, # type: ignore variable_values=request_data.variables, context_value=context, root_value=root_value, operation_name=request_data.operation_name, operation_extensions=request_data.extensions, ) return await self.execute_single( request=request, request_adapter=request_adapter, sub_response=sub_response, context=context, root_value=root_value, request_data=request_data, ) async def execute_single( self, request: Request, request_adapter: AsyncHTTPRequestAdapter, sub_response: SubResponse, context: Context, root_value: RootValue | None, request_data: GraphQLRequestData, ) -> ExecutionResult: allowed_operation_types = OperationType.from_http(request_adapter.method) if not self.allow_queries_via_get and request_adapter.method == "GET": allowed_operation_types = allowed_operation_types - {OperationType.QUERY} try: result = await self.schema.execute( request_data.query, root_value=root_value, variable_values=request_data.variables, context_value=context, operation_name=request_data.operation_name, allowed_operation_types=allowed_operation_types, operation_extensions=request_data.extensions, ) except CannotGetOperationTypeError as e: raise HTTPException(400, e.as_http_error_reason()) from e except InvalidOperationTypeError as e: raise HTTPException( 400, e.as_http_error_reason(request_adapter.method) ) from e except MissingQueryError as e: raise HTTPException(400, "No GraphQL query found in the request") from e return result async def parse_multipart(self, request: AsyncHTTPRequestAdapter) -> dict[str, str]: try: form_data = await request.get_form_data() except ValueError as e: raise HTTPException(400, "Unable to parse the multipart body") from e operations = form_data.form.get("operations", "{}") files_map = form_data.form.get("map", "{}") files = form_data.files if isinstance(operations, (bytes, str)): operations = self.parse_json(operations) if isinstance(files_map, (bytes, str)): files_map = self.parse_json(files_map) try: return replace_placeholders_with_files(operations, files_map, files) except KeyError as e: raise HTTPException(400, "File(s) missing in form data") from e def _handle_errors( self, errors: list[GraphQLError], response_data: GraphQLHTTPResponse ) -> None: """Hook to allow custom handling of errors, used by the Sentry Integration.""" @overload async def run( self, request: Request, context: Context = UNSET, root_value: RootValue | None = UNSET, ) -> Response: ... @overload async def run( self, request: WebSocketRequest, context: Context = UNSET, root_value: RootValue | None = UNSET, ) -> WebSocketResponse: ... async def run( self, request: Request | WebSocketRequest, context: Context = UNSET, root_value: RootValue | None = UNSET, ) -> Response | WebSocketResponse: root_value = ( await self.get_root_value(request) if root_value is UNSET else root_value ) if self.is_websocket_request(request): websocket_subprotocol = await self.pick_websocket_subprotocol(request) websocket_response = await self.create_websocket_response( request, websocket_subprotocol ) websocket = self.websocket_adapter_class(self, request, websocket_response) context = ( await self.get_context(request, response=websocket_response) if context is UNSET else context ) if websocket_subprotocol == GRAPHQL_TRANSPORT_WS_PROTOCOL: await self.graphql_transport_ws_handler_class( view=self, websocket=websocket, context=context, root_value=root_value, schema=self.schema, connection_init_wait_timeout=self.connection_init_wait_timeout, ).handle() elif websocket_subprotocol == GRAPHQL_WS_PROTOCOL: await self.graphql_ws_handler_class( view=self, websocket=websocket, context=context, root_value=root_value, schema=self.schema, keep_alive=self.keep_alive, keep_alive_interval=self.keep_alive_interval, ).handle() else: await websocket.close(4406, "Subprotocol not acceptable") return websocket_response request = cast("Request", request) request_adapter = self.request_adapter_class(request) sub_response = await self.get_sub_response(request) context = ( await self.get_context(request, response=sub_response) if context is UNSET else context ) if not self.is_request_allowed(request_adapter): raise HTTPException(405, "GraphQL only supports GET and POST requests.") if self.should_render_graphql_ide(request_adapter): if self.graphql_ide: return await self.render_graphql_ide(request) raise HTTPException(404, "Not Found") result = await self.execute_operation( request=request, context=context, root_value=root_value, sub_response=sub_response, ) if isinstance(result, SubscriptionExecutionResult): stream = self._get_stream(request, result) return await self.create_streaming_response( request, stream, sub_response, headers={ "Content-Type": "multipart/mixed;boundary=graphql;subscriptionSpec=1.0,application/json", }, ) if isinstance(result, GraphQLIncrementalExecutionResults): async def stream() -> AsyncGenerator[str, None]: yield "---" response = await self.process_result(request, result.initial_result) response["hasNext"] = result.initial_result.has_next response["pending"] = [ p.formatted for p in result.initial_result.pending ] response["extensions"] = result.initial_result.extensions yield self.encode_multipart_data(response, "-") all_pending = result.initial_result.pending async for value in result.subsequent_results: response = { "hasNext": value.has_next, "extensions": value.extensions, } if value.pending: response["pending"] = [p.formatted for p in value.pending] if value.completed: response["completed"] = [p.formatted for p in value.completed] if value.incremental: incremental = [] all_pending.extend(value.pending) for incremental_value in value.incremental: pending_value = next( ( v for v in all_pending if v.id == incremental_value.id ), None, ) assert pending_value incremental.append( { **incremental_value.formatted, "path": pending_value.path, "label": pending_value.label, } ) response["incremental"] = incremental yield self.encode_multipart_data(response, "-") yield "--\r\n" return await self.create_streaming_response( request, stream, sub_response, headers={ "Content-Type": 'multipart/mixed; boundary="-"', }, ) response_data: GraphQLHTTPResponse | list[GraphQLHTTPResponse] if isinstance(result, list): response_data = [] for execution_result in result: processed_result = await self.process_result( request=request, result=execution_result ) if execution_result.errors: self._handle_errors(execution_result.errors, processed_result) response_data.append(processed_result) else: response_data = await self.process_result(request=request, result=result) if result.errors: self._handle_errors(result.errors, response_data) return self.create_response( response_data=response_data, sub_response=sub_response ) def encode_multipart_data(self, data: Any, separator: str) -> str: encoded_data = self.encode_json(data) if isinstance(encoded_data, bytes): encoded_data = encoded_data.decode() return "".join( [ "\r\n", "Content-Type: application/json; charset=utf-8\r\n", "Content-Length: " + str(len(encoded_data)) + "\r\n", "\r\n", encoded_data, f"\r\n--{separator}", ] ) def _stream_with_heartbeat( self, stream: Callable[[], AsyncGenerator[str, None]], separator: str ) -> Callable[[], AsyncGenerator[str, None]]: """Add heartbeat messages to a GraphQL stream to prevent connection timeouts. This method wraps an async stream generator with heartbeat functionality by: 1. Creating a queue to coordinate between data and heartbeat messages 2. Running two concurrent tasks: one for original stream data, one for heartbeats 3. Merging both message types into a single output stream Messages in the queue are tuples of (raised, done, data) where: - raised (bool): True if this contains an exception to be re-raised - done (bool): True if this is the final signal indicating stream completion - data: The actual message content to yield, or exception if raised=True Note: data is always None when done=True and can be ignored Note: This implementation addresses two critical concerns: 1. Race condition: There's a potential race between checking task.done() and processing the final message. We solve this by having the drain task send an explicit (False, True, None) completion signal as its final action. Without this signal, we might exit before processing the final boundary. Since the queue size is 1 and the drain task will only complete after successfully queueing the done signal, task.done() guarantees the done signal is either in the queue or has already been processed. This ensures we never miss the final boundary. 2. Flow control: The queue has maxsize=1, which is essential because: - It provides natural backpressure between producers and consumer - Prevents heartbeat messages from accumulating when drain is active - Ensures proper task coordination without complex synchronization - Guarantees the done signal is queued before drain task completes Heartbeats are sent every 5 seconds when the drain task isn't sending data. Note: Due to the asynchronous nature of the heartbeat task, an extra heartbeat message may be sent after the final stream boundary message. This is safe because both the MIME specification (RFC 2046) and Apollo's GraphQL Multipart HTTP protocol require clients to ignore any content after the final boundary marker. Additionally, Apollo's protocol defines heartbeats as empty JSON objects that clients must silently ignore. """ queue: asyncio.Queue[tuple[bool, bool, Any]] = asyncio.Queue( maxsize=1, # Critical: maxsize=1 for flow control. ) cancelling = False async def drain() -> None: try: async for item in stream(): await queue.put((False, False, item)) except Exception as e: if not cancelling: await queue.put((True, False, e)) else: raise # Send completion signal to prevent race conditions. The queue.put() # blocks until space is available (due to maxsize=1), guaranteeing that # when task.done() is True, the final stream message has been dequeued. await queue.put((False, True, None)) # Always use None with done=True async def heartbeat() -> None: while True: item = self.encode_multipart_data({}, separator) await queue.put((False, False, item)) await asyncio.sleep(5) async def merged() -> AsyncGenerator[str, None]: heartbeat_task = asyncio.create_task(heartbeat()) task = asyncio.create_task(drain()) async def cancel_tasks() -> None: nonlocal cancelling cancelling = True task.cancel() with contextlib.suppress(asyncio.CancelledError): await task heartbeat_task.cancel() with contextlib.suppress(asyncio.CancelledError): await heartbeat_task try: # When task.done() is True, the final stream message has been # dequeued due to queue size 1 and the blocking nature of queue.put(). while not task.done(): raised, done, data = await queue.get() if done: # Received done signal (data is None), stream is complete. # Note that we may not get here because of the race between # task.done() and queue.get(), but that's OK because if # task.done() is True, the actual final message (including any # exception) has been consumed. The only intent here is to # ensure that data=None is not yielded. break if raised: await cancel_tasks() raise data yield data finally: await cancel_tasks() return merged def _get_stream( self, request: Request, result: SubscriptionExecutionResult, separator: str = "graphql", ) -> Callable[[], AsyncGenerator[str, None]]: async def stream() -> AsyncGenerator[str, None]: async for value in result: response = await self.process_result(request, value) yield self.encode_multipart_data({"payload": response}, separator) yield f"\r\n--{separator}--\r\n" return self._stream_with_heartbeat(stream, separator) async def parse_multipart_subscriptions( self, request: AsyncHTTPRequestAdapter ) -> dict[str, str]: if request.method == "GET": return self.parse_query_params(request.query_params) return self.parse_json(await request.get_body()) async def parse_http_body( self, request: AsyncHTTPRequestAdapter ) -> GraphQLRequestData | list[GraphQLRequestData]: headers = {key.lower(): value for key, value in request.headers.items()} content_type, _ = parse_content_type(request.content_type or "") accept = headers.get("accept", "") protocol: Literal["http", "multipart-subscription"] = ( "multipart-subscription" if self._is_multipart_subscriptions(*parse_content_type(accept)) else "http" ) if request.method == "GET": data = self.parse_query_params(request.query_params) elif "application/json" in content_type: data = self.parse_json(await request.get_body()) elif self.multipart_uploads_enabled and content_type == "multipart/form-data": data = await self.parse_multipart(request) else: raise HTTPException(400, "Unsupported content type") if isinstance(data, list): self._validate_batch_request(data, protocol=protocol) return [ GraphQLRequestData( query=item.get("query"), variables=item.get("variables"), operation_name=item.get("operationName"), extensions=item.get("extensions"), protocol=protocol, ) for item in data ] query = data.get("query") if not isinstance(query, (str, type(None))): raise HTTPException( 400, "The GraphQL operation's `query` must be a string or null, if provided.", ) variables = data.get("variables") if not isinstance(variables, (dict, type(None))): raise HTTPException( 400, "The GraphQL operation's `variables` must be an object or null, if provided.", ) extensions = data.get("extensions") if not isinstance(extensions, (dict, type(None))): raise HTTPException( 400, "The GraphQL operation's `extensions` must be an object or null, if provided.", ) return GraphQLRequestData( query=query, variables=variables, operation_name=data.get("operationName"), extensions=extensions, protocol=protocol, ) async def process_result( self, request: Request, result: ExecutionResult ) -> GraphQLHTTPResponse: return process_result(result) async def on_ws_connect( self, context: Context ) -> UnsetType | None | dict[str, object]: return UNSET __all__ = ["AsyncBaseHTTPView"] strawberry-graphql-0.287.0/strawberry/http/base.py000066400000000000000000000062571511033167500222130ustar00rootroot00000000000000import json from collections.abc import Mapping from typing import Any, Generic from typing_extensions import Protocol from lia import HTTPException from strawberry.http import GraphQLRequestData from strawberry.http.ides import GraphQL_IDE, get_graphql_ide_html from strawberry.http.types import HTTPMethod, QueryParams from strawberry.schema.base import BaseSchema from .typevars import Request class BaseRequestProtocol(Protocol): @property def query_params(self) -> Mapping[str, str | list[str] | None]: ... @property def method(self) -> HTTPMethod: ... @property def headers(self) -> Mapping[str, str]: ... class BaseView(Generic[Request]): graphql_ide: GraphQL_IDE | None multipart_uploads_enabled: bool = False schema: BaseSchema def should_render_graphql_ide(self, request: BaseRequestProtocol) -> bool: return ( request.method == "GET" and request.query_params.get("query") is None and any( supported_header in request.headers.get("accept", "") for supported_header in ("text/html", "*/*") ) ) def is_request_allowed(self, request: BaseRequestProtocol) -> bool: return request.method in ("GET", "POST") def parse_json(self, data: str | bytes) -> Any: try: return self.decode_json(data) except json.JSONDecodeError as e: raise HTTPException(400, "Unable to parse request body as JSON") from e def decode_json(self, data: str | bytes) -> object: return json.loads(data) def encode_json(self, data: object) -> str | bytes: return json.dumps(data) def parse_query_params(self, params: QueryParams) -> dict[str, Any]: params = dict(params) if "variables" in params: variables = params["variables"] if variables: params["variables"] = self.parse_json(variables) if "extensions" in params: extensions = params["extensions"] if extensions: params["extensions"] = self.parse_json(extensions) return params @property def graphql_ide_html(self) -> str: return get_graphql_ide_html(graphql_ide=self.graphql_ide) def _is_multipart_subscriptions( self, content_type: str, params: dict[str, str] ) -> bool: subscription_spec = params.get("subscriptionspec", "").strip("'\"") return ( content_type == "multipart/mixed" and ("boundary" not in params or params["boundary"] == "graphql") and subscription_spec.startswith("1.0") ) def _validate_batch_request( self, request_data: list[GraphQLRequestData], protocol: str ) -> None: if self.schema.config.batching_config is None: raise HTTPException(400, "Batching is not enabled") if protocol == "multipart-subscription": raise HTTPException( 400, "Batching is not supported for multipart subscriptions" ) if len(request_data) > self.schema.config.batching_config["max_operations"]: raise HTTPException(400, "Too many operations") __all__ = ["BaseView"] strawberry-graphql-0.287.0/strawberry/http/exceptions.py000066400000000000000000000004021511033167500234440ustar00rootroot00000000000000class NonTextMessageReceived(Exception): pass class NonJsonMessageReceived(Exception): pass class WebSocketDisconnected(Exception): pass __all__ = [ "NonJsonMessageReceived", "NonTextMessageReceived", "WebSocketDisconnected", ] strawberry-graphql-0.287.0/strawberry/http/ides.py000066400000000000000000000010731511033167500222140ustar00rootroot00000000000000import pathlib from typing import Literal GraphQL_IDE = Literal["graphiql", "apollo-sandbox", "pathfinder"] def get_graphql_ide_html( graphql_ide: GraphQL_IDE | None = "graphiql", ) -> str: here = pathlib.Path(__file__).parents[1] if graphql_ide == "apollo-sandbox": path = here / "static/apollo-sandbox.html" elif graphql_ide == "pathfinder": path = here / "static/pathfinder.html" else: path = here / "static/graphiql.html" return path.read_text(encoding="utf-8") __all__ = ["GraphQL_IDE", "get_graphql_ide_html"] strawberry-graphql-0.287.0/strawberry/http/parse_content_type.py000066400000000000000000000005751511033167500252030ustar00rootroot00000000000000from email.message import Message def parse_content_type(content_type: str) -> tuple[str, dict[str, str]]: """Parse a content type header into a mime-type and a dictionary of parameters.""" email = Message() email["content-type"] = content_type params = email.get_params() assert params mime_type, _ = params.pop(0) return mime_type, dict(params) strawberry-graphql-0.287.0/strawberry/http/sync_base_view.py000066400000000000000000000237571511033167500243050ustar00rootroot00000000000000import abc import json from collections.abc import Callable from typing import ( Generic, Literal, ) from graphql import GraphQLError from lia import HTTPException, SyncHTTPRequestAdapter from strawberry.exceptions import MissingQueryError from strawberry.file_uploads.utils import replace_placeholders_with_files from strawberry.http import ( GraphQLHTTPResponse, GraphQLRequestData, process_result, ) from strawberry.http.ides import GraphQL_IDE from strawberry.schema import BaseSchema from strawberry.schema.exceptions import ( CannotGetOperationTypeError, InvalidOperationTypeError, ) from strawberry.types import ExecutionResult from strawberry.types.graphql import OperationType from strawberry.types.unset import UNSET from .base import BaseView from .parse_content_type import parse_content_type from .typevars import Context, Request, Response, RootValue, SubResponse class SyncBaseHTTPView( abc.ABC, BaseView[Request], Generic[Request, Response, SubResponse, Context, RootValue], ): schema: BaseSchema graphiql: bool | None graphql_ide: GraphQL_IDE | None request_adapter_class: Callable[[Request], SyncHTTPRequestAdapter] # Methods that need to be implemented by individual frameworks @property @abc.abstractmethod def allow_queries_via_get(self) -> bool: ... @abc.abstractmethod def get_sub_response(self, request: Request) -> SubResponse: ... @abc.abstractmethod def get_context(self, request: Request, response: SubResponse) -> Context: ... @abc.abstractmethod def get_root_value(self, request: Request) -> RootValue | None: ... @abc.abstractmethod def create_response( self, response_data: GraphQLHTTPResponse | list[GraphQLHTTPResponse], sub_response: SubResponse, ) -> Response: ... @abc.abstractmethod def render_graphql_ide(self, request: Request) -> Response: ... def execute_operation( self, request: Request, context: Context, root_value: RootValue | None, sub_response: SubResponse, ) -> ExecutionResult | list[ExecutionResult]: request_adapter = self.request_adapter_class(request) try: request_data = self.parse_http_body(request_adapter) except json.decoder.JSONDecodeError as e: raise HTTPException(400, "Unable to parse request body as JSON") from e # DO this only when doing files except KeyError as e: raise HTTPException(400, "File(s) missing in form data") from e allowed_operation_types = OperationType.from_http(request_adapter.method) if not self.allow_queries_via_get and request_adapter.method == "GET": allowed_operation_types = allowed_operation_types - {OperationType.QUERY} if isinstance(request_data, list): # batch GraphQL requests return [ self.execute_single( request=request, request_adapter=request_adapter, sub_response=sub_response, context=context, root_value=root_value, request_data=data, ) for data in request_data ] return self.execute_single( request=request, request_adapter=request_adapter, sub_response=sub_response, context=context, root_value=root_value, request_data=request_data, ) def execute_single( self, request: Request, request_adapter: SyncHTTPRequestAdapter, sub_response: SubResponse, context: Context, root_value: RootValue | None, request_data: GraphQLRequestData, ) -> ExecutionResult: allowed_operation_types = OperationType.from_http(request_adapter.method) if not self.allow_queries_via_get and request_adapter.method == "GET": allowed_operation_types = allowed_operation_types - {OperationType.QUERY} try: result = self.schema.execute_sync( request_data.query, root_value=root_value, variable_values=request_data.variables, context_value=context, operation_name=request_data.operation_name, allowed_operation_types=allowed_operation_types, operation_extensions=request_data.extensions, ) except CannotGetOperationTypeError as e: raise HTTPException(400, e.as_http_error_reason()) from e except InvalidOperationTypeError as e: raise HTTPException( 400, e.as_http_error_reason(request_adapter.method) ) from e except MissingQueryError as e: raise HTTPException(400, "No GraphQL query found in the request") from e return result def parse_multipart(self, request: SyncHTTPRequestAdapter) -> dict[str, str]: operations = self.parse_json(request.post_data.get("operations", "{}")) files_map = self.parse_json(request.post_data.get("map", "{}")) try: return replace_placeholders_with_files(operations, files_map, request.files) except KeyError as e: raise HTTPException(400, "File(s) missing in form data") from e def parse_http_body( self, request: SyncHTTPRequestAdapter ) -> GraphQLRequestData | list[GraphQLRequestData]: headers = {key.lower(): value for key, value in request.headers.items()} content_type, params = parse_content_type(request.content_type or "") accept = headers.get("accept", "") protocol: Literal["http", "multipart-subscription"] = ( "multipart-subscription" if self._is_multipart_subscriptions(*parse_content_type(accept)) else "http" ) if request.method == "GET": data = self.parse_query_params(request.query_params) elif "application/json" in content_type: data = self.parse_json(request.body) # TODO: multipart via get? elif self.multipart_uploads_enabled and content_type == "multipart/form-data": data = self.parse_multipart(request) elif self._is_multipart_subscriptions(content_type, params): raise HTTPException( 400, "Multipart subcriptions are not supported in sync mode" ) else: raise HTTPException(400, "Unsupported content type") if isinstance(data, list): self._validate_batch_request(data, protocol=protocol) return [ GraphQLRequestData( query=item.get("query"), variables=item.get("variables"), operation_name=item.get("operationName"), extensions=item.get("extensions"), ) for item in data ] query = data.get("query") if not isinstance(query, (str, type(None))): raise HTTPException( 400, "The GraphQL operation's `query` must be a string or null, if provided.", ) variables = data.get("variables") if not isinstance(variables, (dict, type(None))): raise HTTPException( 400, "The GraphQL operation's `variables` must be an object or null, if provided.", ) extensions = data.get("extensions") if not isinstance(extensions, (dict, type(None))): raise HTTPException( 400, "The GraphQL operation's `extensions` must be an object or null, if provided.", ) return GraphQLRequestData( query=query, variables=variables, operation_name=data.get("operationName"), extensions=extensions, ) def _handle_errors( self, errors: list[GraphQLError], response_data: GraphQLHTTPResponse ) -> None: """Hook to allow custom handling of errors, used by the Sentry Integration.""" def run( self, request: Request, context: Context = UNSET, root_value: RootValue | None = UNSET, ) -> Response: request_adapter = self.request_adapter_class(request) if not self.is_request_allowed(request_adapter): raise HTTPException(405, "GraphQL only supports GET and POST requests.") if self.should_render_graphql_ide(request_adapter): if self.graphql_ide: return self.render_graphql_ide(request) raise HTTPException(404, "Not Found") sub_response = self.get_sub_response(request) context = ( self.get_context(request, response=sub_response) if context is UNSET else context ) root_value = self.get_root_value(request) if root_value is UNSET else root_value result = self.execute_operation( request=request, context=context, root_value=root_value, sub_response=sub_response, ) response_data: GraphQLHTTPResponse | list[GraphQLHTTPResponse] if isinstance(result, list): response_data = [] for execution_result in result: processed_result = self.process_result( request=request, result=execution_result ) if execution_result.errors: self._handle_errors(execution_result.errors, processed_result) response_data.append(processed_result) else: response_data = self.process_result(request=request, result=result) if result.errors: self._handle_errors(result.errors, response_data) return self.create_response( response_data=response_data, sub_response=sub_response ) def process_result( self, request: Request, result: ExecutionResult ) -> GraphQLHTTPResponse: return process_result(result) __all__ = ["SyncBaseHTTPView"] strawberry-graphql-0.287.0/strawberry/http/temporal_response.py000066400000000000000000000003041511033167500250250ustar00rootroot00000000000000from dataclasses import dataclass, field @dataclass class TemporalResponse: status_code: int = 200 headers: dict[str, str] = field(default_factory=dict) __all__ = ["TemporalResponse"] strawberry-graphql-0.287.0/strawberry/http/types.py000066400000000000000000000006011511033167500224300ustar00rootroot00000000000000from collections.abc import Mapping from typing import Any, Literal from typing_extensions import TypedDict HTTPMethod = Literal[ "GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "TRACE" ] QueryParams = Mapping[str, str | None] class FormData(TypedDict): files: Mapping[str, Any] form: Mapping[str, Any] __all__ = ["FormData", "HTTPMethod", "QueryParams"] strawberry-graphql-0.287.0/strawberry/http/typevars.py000066400000000000000000000007511511033167500231470ustar00rootroot00000000000000from typing_extensions import TypeVar Request = TypeVar("Request", contravariant=True) Response = TypeVar("Response") SubResponse = TypeVar("SubResponse") WebSocketRequest = TypeVar("WebSocketRequest") WebSocketResponse = TypeVar("WebSocketResponse") Context = TypeVar("Context", default=None) RootValue = TypeVar("RootValue", default=None) __all__ = [ "Context", "Request", "Response", "RootValue", "SubResponse", "WebSocketRequest", "WebSocketResponse", ] strawberry-graphql-0.287.0/strawberry/litestar/000077500000000000000000000000001511033167500215655ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/litestar/__init__.py000066400000000000000000000003551511033167500237010ustar00rootroot00000000000000from .controller import ( BaseContext, HTTPContextType, WebSocketContextType, make_graphql_controller, ) __all__ = [ "BaseContext", "HTTPContextType", "WebSocketContextType", "make_graphql_controller", ] strawberry-graphql-0.287.0/strawberry/litestar/controller.py000066400000000000000000000314301511033167500243230ustar00rootroot00000000000000"""Litestar integration for strawberry-graphql.""" from __future__ import annotations import json import warnings from datetime import timedelta from typing import ( TYPE_CHECKING, Any, ClassVar, TypeAlias, TypedDict, TypeGuard, ) from lia import HTTPException, LitestarRequestAdapter from litestar import ( Controller, MediaType, Request, Response, WebSocket, get, post, websocket, ) from litestar.background_tasks import BackgroundTasks from litestar.di import Provide from litestar.exceptions import ( NotFoundException, ValidationException, WebSocketDisconnect, ) from litestar.response.streaming import Stream from litestar.status_codes import HTTP_200_OK from msgspec import Struct from strawberry.exceptions import InvalidCustomContext from strawberry.http.async_base_view import ( AsyncBaseHTTPView, AsyncWebSocketAdapter, ) from strawberry.http.exceptions import ( NonJsonMessageReceived, NonTextMessageReceived, WebSocketDisconnected, ) from strawberry.http.typevars import Context, RootValue from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL if TYPE_CHECKING: from collections.abc import ( AsyncGenerator, AsyncIterator, Callable, Mapping, Sequence, ) from litestar.types import AnyCallable, Dependencies from strawberry.http import GraphQLHTTPResponse from strawberry.http.ides import GraphQL_IDE from strawberry.schema import BaseSchema class BaseContext(Struct, kw_only=True): request: Request | None = None websocket: WebSocket | None = None response: Response | None = None class HTTPContextType: """This class does not exists at runtime, it only set proper types for context attributes.""" request: Request response: Response class WebSocketContextType: """This class does not exists at runtime, it only set proper types for context attributes.""" websocket: WebSocket class HTTPContextDict(TypedDict): request: Request[Any, Any, Any] response: Response[Any] class WebSocketContextDict(TypedDict): socket: WebSocket MergedContext: TypeAlias = ( BaseContext | WebSocketContextDict | HTTPContextDict | dict[str, Any] ) async def _none_custom_context_getter() -> None: return None async def _none_root_value_getter() -> None: return None async def _context_getter_ws( custom_context: Any | None, socket: WebSocket ) -> MergedContext: if isinstance(custom_context, BaseContext): custom_context.websocket = socket return custom_context default_context = WebSocketContextDict(socket=socket) if isinstance(custom_context, dict): return {**default_context, **custom_context} if custom_context is None: return default_context raise InvalidCustomContext def _response_getter() -> Response: return Response({}, background=BackgroundTasks([])) async def _context_getter_http( custom_context: Any | None, response: Response, request: Request[Any, Any, Any], ) -> MergedContext: if isinstance(custom_context, BaseContext): custom_context.request = request custom_context.response = response return custom_context default_context = HTTPContextDict(request=request, response=response) if isinstance(custom_context, dict): return {**default_context, **custom_context} if custom_context is None: return default_context raise InvalidCustomContext class GraphQLResource(Struct): data: dict[str, object] | None errors: list[object] | None extensions: dict[str, object] | None class LitestarWebSocketAdapter(AsyncWebSocketAdapter): def __init__( self, view: AsyncBaseHTTPView, request: WebSocket, response: WebSocket ) -> None: super().__init__(view) self.ws = response async def iter_json( self, *, ignore_parsing_errors: bool = False ) -> AsyncGenerator[object, None]: try: while self.ws.connection_state != "disconnect": text = await self.ws.receive_text() # Litestar internally defaults to an empty string for non-text messages if text == "": raise NonTextMessageReceived try: yield self.view.decode_json(text) except json.JSONDecodeError as e: if not ignore_parsing_errors: raise NonJsonMessageReceived from e except WebSocketDisconnect: pass async def send_json(self, message: Mapping[str, object]) -> None: try: await self.ws.send_data(data=self.view.encode_json(message)) except WebSocketDisconnect as exc: raise WebSocketDisconnected from exc async def close(self, code: int, reason: str) -> None: await self.ws.close(code=code, reason=reason) class GraphQLController( Controller, AsyncBaseHTTPView[ Request[Any, Any, Any], Response[Any], Response[Any], WebSocket, WebSocket, Context, RootValue, ], ): path: str = "" dependencies: ClassVar[Dependencies] = { # type: ignore[misc] "custom_context": Provide(_none_custom_context_getter), "context": Provide(_context_getter_http), "context_ws": Provide(_context_getter_ws), "root_value": Provide(_none_root_value_getter), "response": Provide(_response_getter, sync_to_thread=True), } request_adapter_class = LitestarRequestAdapter websocket_adapter_class = LitestarWebSocketAdapter # type: ignore allow_queries_via_get: bool = True graphiql_allowed_accept: frozenset[str] = frozenset({"text/html", "*/*"}) graphql_ide: GraphQL_IDE | None = "graphiql" connection_init_wait_timeout: timedelta = timedelta(minutes=1) protocols: Sequence[str] = ( GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL, ) keep_alive: bool = False keep_alive_interval: float = 1 def is_websocket_request( self, request: Request | WebSocket ) -> TypeGuard[WebSocket]: return isinstance(request, WebSocket) async def pick_websocket_subprotocol(self, request: WebSocket) -> str | None: subprotocols = request.scope["subprotocols"] intersection = set(subprotocols) & set(self.protocols) sorted_intersection = sorted(intersection, key=subprotocols.index) return next(iter(sorted_intersection), None) async def create_websocket_response( self, request: WebSocket, subprotocol: str | None ) -> WebSocket: await request.accept(subprotocols=subprotocol) return request async def execute_request( self, request: Request[Any, Any, Any], context: Any, root_value: Any, ) -> Response[GraphQLResource | str]: try: return await self.run( request, context=context, root_value=root_value, ) except HTTPException as e: return Response( e.reason, status_code=e.status_code, media_type=MediaType.TEXT, ) async def render_graphql_ide( self, request: Request[Any, Any, Any] ) -> Response[str]: return Response(self.graphql_ide_html, media_type=MediaType.HTML) def create_response( self, response_data: GraphQLHTTPResponse | list[GraphQLHTTPResponse], sub_response: Response[bytes], ) -> Response[bytes]: encoded_data = self.encode_json(response_data) if isinstance(encoded_data, str): encoded_data = encoded_data.encode() response = Response( encoded_data, status_code=HTTP_200_OK, media_type=MediaType.JSON, ) response.headers.update(sub_response.headers) response.cookies.extend(sub_response.cookies) response.background = sub_response.background if sub_response.status_code: response.status_code = sub_response.status_code return response async def create_streaming_response( self, request: Request, stream: Callable[[], AsyncIterator[str]], sub_response: Response, headers: dict[str, str], ) -> Response: return Stream( stream(), status_code=sub_response.status_code, headers={ **sub_response.headers, **headers, }, ) @get(raises=[ValidationException, NotFoundException]) async def handle_http_get( self, request: Request[Any, Any, Any], context: Any, root_value: Any, response: Response, ) -> Response[GraphQLResource | str]: self.temporal_response = response return await self.execute_request( request=request, context=context, root_value=root_value, ) @post(status_code=HTTP_200_OK) async def handle_http_post( self, request: Request[Any, Any, Any], context: Any, root_value: Any, response: Response, ) -> Response[GraphQLResource | str]: self.temporal_response = response return await self.execute_request( request=request, context=context, root_value=root_value, ) @websocket() async def websocket_endpoint( self, socket: WebSocket, context_ws: Any, root_value: Any, ) -> None: await self.run( request=socket, context=context_ws, root_value=root_value, ) async def get_context( self, request: Request[Any, Any, Any] | WebSocket, response: Response | WebSocket, ) -> Context: # pragma: no cover msg = "`get_context` is not used by Litestar's controller" raise ValueError(msg) async def get_root_value( self, request: Request[Any, Any, Any] | WebSocket ) -> RootValue | None: # pragma: no cover msg = "`get_root_value` is not used by Litestar's controller" raise ValueError(msg) async def get_sub_response(self, request: Request[Any, Any, Any]) -> Response: return self.temporal_response def make_graphql_controller( schema: BaseSchema, path: str = "", graphiql: bool | None = None, graphql_ide: GraphQL_IDE | None = "graphiql", allow_queries_via_get: bool = True, keep_alive: bool = False, keep_alive_interval: float = 1, # TODO: root typevar root_value_getter: AnyCallable | None = None, # TODO: context typevar context_getter: AnyCallable | None = None, subscription_protocols: Sequence[str] = ( GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL, ), connection_init_wait_timeout: timedelta = timedelta(minutes=1), multipart_uploads_enabled: bool = False, ) -> type[GraphQLController]: # sourcery skip: move-assign if context_getter is None: custom_context_getter_ = _none_custom_context_getter else: custom_context_getter_ = context_getter if root_value_getter is None: root_value_getter_ = _none_root_value_getter else: root_value_getter_ = root_value_getter schema_: BaseSchema = schema allow_queries_via_get_: bool = allow_queries_via_get graphql_ide_: GraphQL_IDE | None if graphiql is not None: warnings.warn( "The `graphiql` argument is deprecated in favor of `graphql_ide`", DeprecationWarning, stacklevel=2, ) graphql_ide_ = "graphiql" if graphiql else None else: graphql_ide_ = graphql_ide routes_path: str = path class _GraphQLController(GraphQLController): path: str = routes_path dependencies: ClassVar[Dependencies] = { # type: ignore[misc] "custom_context": Provide(custom_context_getter_), "context": Provide(_context_getter_http), "context_ws": Provide(_context_getter_ws), "root_value": Provide(root_value_getter_), "response": Provide(_response_getter, sync_to_thread=True), } _GraphQLController.keep_alive = keep_alive _GraphQLController.keep_alive_interval = keep_alive_interval _GraphQLController.protocols = subscription_protocols _GraphQLController.connection_init_wait_timeout = connection_init_wait_timeout _GraphQLController.graphiql_allowed_accept = frozenset({"text/html", "*/*"}) _GraphQLController.schema = schema_ _GraphQLController.allow_queries_via_get = allow_queries_via_get_ _GraphQLController.graphql_ide = graphql_ide_ _GraphQLController.multipart_uploads_enabled = multipart_uploads_enabled return _GraphQLController __all__ = [ "GraphQLController", "make_graphql_controller", ] strawberry-graphql-0.287.0/strawberry/parent.py000066400000000000000000000025221511033167500216020ustar00rootroot00000000000000import re from typing import Annotated, Any, ForwardRef, TypeVar _parent_re = re.compile(r"^(?:strawberry\.)?Parent\[(.*)\]$") class StrawberryParent: ... T = TypeVar("T") Parent = Annotated[T, StrawberryParent()] """Represents a parameter holding the parent resolver's value. This can be used when defining a resolver on a type when the parent isn't expected to return the type itself. Example: ```python import strawberry from dataclasses import dataclass @dataclass class UserRow: id_: str @strawberry.type class User: @strawberry.field @staticmethod async def name(parent: strawberry.Parent[UserRow]) -> str: return f"User Number {parent.id_}" @strawberry.type class Query: @strawberry.field def user(self) -> User: return UserRow(id_="1234") ``` """ def resolve_parent_forward_arg(annotation: Any) -> Any: if isinstance(annotation, str): str_annotation = annotation elif isinstance(annotation, ForwardRef): str_annotation = annotation.__forward_arg__ else: # If neither, return the annotation as is return annotation if parent_match := _parent_re.match(str_annotation): annotation = Parent[ForwardRef(parent_match.group(1))] # type: ignore[misc] return annotation __all__ = ["Parent", "StrawberryParent", "resolve_parent_forward_arg"] strawberry-graphql-0.287.0/strawberry/permission.py000066400000000000000000000165111511033167500225040ustar00rootroot00000000000000from __future__ import annotations import abc import inspect from functools import cached_property from inspect import iscoroutinefunction from typing import ( TYPE_CHECKING, Any, ) from strawberry.exceptions import StrawberryGraphQLError from strawberry.exceptions.permission_fail_silently_requires_optional import ( PermissionFailSilentlyRequiresOptionalError, ) from strawberry.extensions import FieldExtension from strawberry.schema_directive import Location, StrawberrySchemaDirective from strawberry.types.base import StrawberryList, StrawberryOptional from strawberry.utils.await_maybe import await_maybe if TYPE_CHECKING: from collections.abc import Awaitable from graphql import GraphQLError, GraphQLErrorExtensions from strawberry.extensions.field_extension import ( AsyncExtensionResolver, SyncExtensionResolver, ) from strawberry.types import Info from strawberry.types.field import StrawberryField class BasePermission(abc.ABC): """Base class for permissions. All permissions should inherit from this class. Example: ```python from strawberry.permission import BasePermission class IsAuthenticated(BasePermission): message = "User is not authenticated" def has_permission(self, source, info, **kwargs): return info.context["user"].is_authenticated ``` """ message: str | None = None error_extensions: GraphQLErrorExtensions | None = None error_class: type[GraphQLError] = StrawberryGraphQLError _schema_directive: object | None = None @abc.abstractmethod def has_permission( self, source: Any, info: Info, **kwargs: Any ) -> bool | Awaitable[bool]: """Check if the permission should be accepted. This method should be overridden by the subclasses. """ raise NotImplementedError( "Permission classes should override has_permission method" ) def on_unauthorized(self) -> None: """Default error raising for permissions. This method can be overridden to customize the error raised when the permission is not granted. Example: ```python from strawberry.permission import BasePermission class CustomPermissionError(PermissionError): pass class IsAuthenticated(BasePermission): message = "User is not authenticated" def has_permission(self, source, info, **kwargs): return info.context["user"].is_authenticated def on_unauthorized(self) -> None: raise CustomPermissionError(self.message) ``` """ # Instantiate error class error = self.error_class(self.message or "") if self.error_extensions: # Add our extensions to the error if not error.extensions: error.extensions = {} error.extensions.update(self.error_extensions) raise error @property def schema_directive(self) -> object: if not self._schema_directive: class AutoDirective: __strawberry_directive__ = StrawberrySchemaDirective( self.__class__.__name__, self.__class__.__name__, [Location.FIELD_DEFINITION], [], ) self._schema_directive = AutoDirective() return self._schema_directive class PermissionExtension(FieldExtension): """Handles permissions for a field. Instantiate this as a field extension with all of the permissions you want to apply. Note: Currently, this is automatically added to the field, when using field.permission_classes This is deprecated behaviour, please manually add the extension to field.extensions """ def __init__( self, permissions: list[BasePermission], use_directives: bool = True, fail_silently: bool = False, ) -> None: """Initialize the permission extension. Args: permissions: List of permissions to apply. fail_silently: If True, return None or [] instead of raising an exception. This is only valid for optional or list fields. use_directives: If True, add schema directives to the field. """ self.permissions = permissions self.fail_silently = fail_silently self.return_empty_list = False self.use_directives = use_directives def apply(self, field: StrawberryField) -> None: """Applies all of the permission directives (deduped) to the schema and sets up silent permissions.""" if self.use_directives: permission_directives = [ perm.schema_directive for perm in self.permissions if perm.schema_directive ] # Iteration, because we want to keep order for perm_directive in permission_directives: # Dedupe multiple directives if perm_directive in field.directives: continue field.directives.append(perm_directive) # We can only fail silently if the field is optional or a list if self.fail_silently: if isinstance(field.type, StrawberryOptional): if isinstance(field.type.of_type, StrawberryList): self.return_empty_list = True elif isinstance(field.type, StrawberryList): self.return_empty_list = True else: raise PermissionFailSilentlyRequiresOptionalError(field) def _on_unauthorized(self, permission: BasePermission) -> Any: if self.fail_silently: return [] if self.return_empty_list else None return permission.on_unauthorized() def resolve( self, next_: SyncExtensionResolver, source: Any, info: Info, **kwargs: dict[str, Any], ) -> Any: """Checks if the permission should be accepted and raises an exception if not.""" for permission in self.permissions: if not permission.has_permission(source, info, **kwargs): return self._on_unauthorized(permission) return next_(source, info, **kwargs) async def resolve_async( self, next_: AsyncExtensionResolver, source: Any, info: Info, **kwargs: dict[str, Any], ) -> Any: for permission in self.permissions: has_permission = await await_maybe( permission.has_permission(source, info, **kwargs) ) if not has_permission: return self._on_unauthorized(permission) next = next_(source, info, **kwargs) if inspect.isasyncgen(next): return next return await next @cached_property def supports_sync(self) -> bool: """Whether this extension can be resolved synchronously or not. The Permission extension always supports async checking using await_maybe, but only supports sync checking if there are no async permissions. """ async_permissions = [ True for permission in self.permissions if iscoroutinefunction(permission.has_permission) ] return len(async_permissions) == 0 __all__ = [ "BasePermission", "PermissionExtension", ] strawberry-graphql-0.287.0/strawberry/printer/000077500000000000000000000000001511033167500214215ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/printer/__init__.py000066400000000000000000000000761511033167500235350ustar00rootroot00000000000000from .printer import print_schema __all__ = ["print_schema"] strawberry-graphql-0.287.0/strawberry/printer/ast_from_value.py000066400000000000000000000115451511033167500250070ustar00rootroot00000000000000from __future__ import annotations import re from collections.abc import Mapping from math import isfinite from typing import TYPE_CHECKING, Any, cast from graphql.language import ( BooleanValueNode, EnumValueNode, FloatValueNode, IntValueNode, ListValueNode, NameNode, NullValueNode, ObjectFieldNode, ObjectValueNode, StringValueNode, ) from graphql.pyutils import Undefined, inspect, is_iterable from graphql.type import ( GraphQLID, is_enum_type, is_input_object_type, is_leaf_type, is_list_type, is_non_null_type, ) import strawberry from strawberry.types.base import has_object_definition if TYPE_CHECKING: from graphql.language import ValueNode from graphql.type import ( GraphQLInputObjectType, GraphQLInputType, GraphQLList, GraphQLNonNull, ) __all__ = ["ast_from_value"] _re_integer_string = re.compile("^-?(?:0|[1-9][0-9]*)$") def ast_from_leaf_type(serialized: object, type_: GraphQLInputType | None) -> ValueNode: # Others serialize based on their corresponding Python scalar types. if isinstance(serialized, bool): return BooleanValueNode(value=serialized) # Python ints and floats correspond nicely to Int and Float values. if isinstance(serialized, int): return IntValueNode(value=str(serialized)) if isinstance(serialized, float) and isfinite(serialized): value = str(serialized) value = value.removesuffix(".0") return FloatValueNode(value=value) if isinstance(serialized, str): # Enum types use Enum literals. if type_ and is_enum_type(type_): return EnumValueNode(value=serialized) # ID types can use Int literals. if type_ is GraphQLID and _re_integer_string.match(serialized): return IntValueNode(value=serialized) return StringValueNode(value=serialized) if isinstance(serialized, dict): return ObjectValueNode( fields=[ ObjectFieldNode( name=NameNode(value=key), value=ast_from_leaf_type(value, None), ) for key, value in serialized.items() ] ) raise TypeError( f"Cannot convert value to AST: {inspect(serialized)}." ) # pragma: no cover def ast_from_value(value: Any, type_: GraphQLInputType) -> ValueNode | None: # custom ast_from_value that allows to also serialize custom scalar that aren't # basic types, namely JSON scalar types if is_non_null_type(type_): type_ = cast("GraphQLNonNull", type_) ast_value = ast_from_value(value, type_.of_type) if isinstance(ast_value, NullValueNode): return None return ast_value # only explicit None, not Undefined or NaN if value is None: return NullValueNode() # undefined if value is Undefined: return None # Convert Python list to GraphQL list. If the GraphQLType is a list, but the value # is not a list, convert the value using the list's item type. if is_list_type(type_): type_ = cast("GraphQLList", type_) item_type = type_.of_type if is_iterable(value): maybe_value_nodes = (ast_from_value(item, item_type) for item in value) value_nodes = tuple(node for node in maybe_value_nodes if node) return ListValueNode(values=value_nodes) return ast_from_value(value, item_type) # Populate the fields of the input object by creating ASTs from each value in the # Python dict according to the fields in the input type. if is_input_object_type(type_): if has_object_definition(value): value = strawberry.asdict(value) if value is None or not isinstance(value, Mapping): return None type_ = cast("GraphQLInputObjectType", type_) field_items = ( (field_name, ast_from_value(value[field_name], field.type)) for field_name, field in type_.fields.items() if field_name in value ) field_nodes = tuple( ObjectFieldNode(name=NameNode(value=field_name), value=field_value) for field_name, field_value in field_items if field_value ) return ObjectValueNode(fields=field_nodes) if is_leaf_type(type_): # Since value is an internally represented value, it must be serialized to an # externally represented value before converting into an AST. serialized = type_.serialize(value) # type: ignore if serialized is None or serialized is Undefined: return None # pragma: no cover return ast_from_leaf_type(serialized, type_) # Not reachable. All possible input types have been considered. raise TypeError(f"Unexpected input type: {inspect(type_)}.") # pragma: no cover __all__ = ["ast_from_value"] strawberry-graphql-0.287.0/strawberry/printer/printer.py000066400000000000000000000453371511033167500234720ustar00rootroot00000000000000from __future__ import annotations import dataclasses from itertools import chain from typing import ( TYPE_CHECKING, Any, TypeVar, cast, overload, ) from graphql import GraphQLObjectType, GraphQLSchema, is_union_type from graphql.language.printer import print_ast from graphql.type import ( is_enum_type, is_input_type, is_interface_type, is_object_type, is_scalar_type, is_specified_directive, ) from graphql.utilities.print_schema import ( is_defined_type, print_block, print_deprecated, print_description, print_implemented_interfaces, print_specified_by_url, ) from graphql.utilities.print_schema import print_type as original_print_type from strawberry.schema_directive import Location, StrawberrySchemaDirective from strawberry.types.base import ( StrawberryContainer, StrawberryObjectDefinition, has_object_definition, ) from strawberry.types.enum import StrawberryEnumDefinition from strawberry.types.scalar import ScalarWrapper from strawberry.types.unset import UNSET from .ast_from_value import ast_from_value if TYPE_CHECKING: from collections.abc import Callable from graphql import ( GraphQLArgument, GraphQLEnumType, GraphQLEnumValue, GraphQLScalarType, GraphQLUnionType, ) from graphql.type.directives import GraphQLDirective from strawberry.schema import BaseSchema from strawberry.types.field import StrawberryField _T = TypeVar("_T") @dataclasses.dataclass class PrintExtras: directives: set[str] = dataclasses.field(default_factory=set) types: set[type] = dataclasses.field(default_factory=set) @overload def _serialize_dataclasses( value: dict[_T, object], *, name_converter: Callable[[str], str] | None = None, ) -> dict[_T, object]: ... @overload def _serialize_dataclasses( value: list[object] | tuple[object], *, name_converter: Callable[[str], str] | None = None, ) -> list[object]: ... @overload def _serialize_dataclasses( value: object, *, name_converter: Callable[[str], str] | None = None, ) -> object: ... def _serialize_dataclasses( value, *, name_converter: Callable[[str], str] | None = None, ): if name_converter is None: name_converter = lambda x: x # noqa: E731 if dataclasses.is_dataclass(value): return { name_converter(k): v for k, v in dataclasses.asdict(value).items() # type: ignore if v is not UNSET } if isinstance(value, (list, tuple)): return [_serialize_dataclasses(v, name_converter=name_converter) for v in value] if isinstance(value, dict): return { name_converter(k): _serialize_dataclasses(v, name_converter=name_converter) for k, v in value.items() } return value def print_schema_directive_params( directive: GraphQLDirective, values: dict[str, Any], *, schema: BaseSchema, ) -> str: params = [] for name, arg in directive.args.items(): value = values.get(name, arg.default_value) if value is UNSET: value = None else: ast = ast_from_value( _serialize_dataclasses( value, name_converter=schema.config.name_converter.apply_naming_config, ), arg.type, ) value = ast and f"{name}: {print_ast(ast)}" if value: params.append(value) if not params: return "" return "(" + ", ".join(params) + ")" def print_schema_directive( directive: Any, schema: BaseSchema, *, extras: PrintExtras ) -> str: strawberry_directive = cast( "StrawberrySchemaDirective", directive.__class__.__strawberry_directive__ ) schema_converter = schema.schema_converter gql_directive = schema_converter.from_schema_directive(directive.__class__) params = print_schema_directive_params( gql_directive, { schema.config.name_converter.get_graphql_name(f): getattr( directive, f.python_name or f.name, UNSET ) for f in strawberry_directive.fields }, schema=schema, ) printed_directive = print_directive(gql_directive, schema=schema) if printed_directive is not None: extras.directives.add(printed_directive) for field in strawberry_directive.fields: f_type = field.type while isinstance(f_type, StrawberryContainer): f_type = f_type.of_type if has_object_definition(f_type): extras.types.add(cast("type", f_type)) if hasattr(f_type, "_scalar_definition"): extras.types.add(cast("type", f_type)) if isinstance(f_type, StrawberryEnumDefinition): extras.types.add(cast("type", f_type)) return f" @{gql_directive.name}{params}" def print_field_directives( field: StrawberryField | None, schema: BaseSchema, *, extras: PrintExtras ) -> str: if not field: return "" directives = ( directive for directive in field.directives if any( location in [Location.FIELD_DEFINITION, Location.INPUT_FIELD_DEFINITION] for location in directive.__strawberry_directive__.locations # type: ignore ) ) return "".join( print_schema_directive(directive, schema=schema, extras=extras) for directive in directives ) def print_argument_directives( argument: GraphQLArgument, *, schema: BaseSchema, extras: PrintExtras ) -> str: strawberry_type = argument.extensions.get("strawberry-definition") directives = strawberry_type.directives if strawberry_type else [] return "".join( print_schema_directive(directive, schema=schema, extras=extras) for directive in directives ) def print_args( args: dict[str, GraphQLArgument], indentation: str = "", *, schema: BaseSchema, extras: PrintExtras, ) -> str: if not args: return "" # If every arg does not have a description, print them on one line. if not any(arg.description for arg in args.values()): return ( "(" + ", ".join( ( f"{print_input_value(name, arg)}" f"{print_argument_directives(arg, schema=schema, extras=extras)}" ) for name, arg in args.items() ) + ")" ) return ( "(\n" + "\n".join( print_description(arg, f" {indentation}", not i) + f" {indentation}" + print_input_value(name, arg) + print_argument_directives(arg, schema=schema, extras=extras) for i, (name, arg) in enumerate(args.items()) ) + f"\n{indentation})" ) def print_fields( type_: GraphQLObjectType, schema: BaseSchema, *, extras: PrintExtras, ) -> str: from strawberry.schema.schema_converter import GraphQLCoreConverter fields = [] for i, (name, field) in enumerate(type_.fields.items()): strawberry_field = field.extensions and field.extensions.get( GraphQLCoreConverter.DEFINITION_BACKREF ) args = ( print_args(field.args, " ", schema=schema, extras=extras) if hasattr(field, "args") else "" ) fields.append( print_description(field, " ", not i) + f" {name}" + args + f": {field.type}" + print_field_directives(strawberry_field, schema=schema, extras=extras) + print_deprecated(field.deprecation_reason) ) return print_block(fields) def print_scalar( type_: GraphQLScalarType, *, schema: BaseSchema, extras: PrintExtras ) -> str: # TODO: refactor this strawberry_type = type_.extensions.get("strawberry-definition") directives = strawberry_type.directives if strawberry_type else [] printed_directives = "".join( print_schema_directive(directive, schema=schema, extras=extras) for directive in directives ) return ( print_description(type_) + f"scalar {type_.name}" + print_specified_by_url(type_) + printed_directives ).strip() def print_enum_value( name: str, value: GraphQLEnumValue, first_in_block: bool, *, schema: BaseSchema, extras: PrintExtras, ) -> str: strawberry_type = value.extensions.get("strawberry-definition") directives = strawberry_type.directives if strawberry_type else [] printed_directives = "".join( print_schema_directive(directive, schema=schema, extras=extras) for directive in directives ) return ( print_description(value, " ", first_in_block) + f" {name}" + print_deprecated(value.deprecation_reason) + printed_directives ) def print_enum( type_: GraphQLEnumType, *, schema: BaseSchema, extras: PrintExtras ) -> str: strawberry_type = type_.extensions.get("strawberry-definition") directives = strawberry_type.directives if strawberry_type else [] printed_directives = "".join( print_schema_directive(directive, schema=schema, extras=extras) for directive in directives ) values = [ print_enum_value(name, value, not i, schema=schema, extras=extras) for i, (name, value) in enumerate(type_.values.items()) ] return ( print_description(type_) + f"enum {type_.name}" + printed_directives + print_block(values) ) def print_extends(type_: GraphQLObjectType, schema: BaseSchema) -> str: from strawberry.schema.schema_converter import GraphQLCoreConverter strawberry_type = cast( "StrawberryObjectDefinition | None", type_.extensions and type_.extensions.get(GraphQLCoreConverter.DEFINITION_BACKREF), ) if strawberry_type and strawberry_type.extend: return "extend " return "" def print_type_directives( type_: GraphQLObjectType, schema: BaseSchema, *, extras: PrintExtras ) -> str: from strawberry.schema.schema_converter import GraphQLCoreConverter strawberry_type = cast( "StrawberryObjectDefinition | None", type_.extensions and type_.extensions.get(GraphQLCoreConverter.DEFINITION_BACKREF), ) if not strawberry_type: return "" allowed_locations = ( [Location.INPUT_OBJECT] if strawberry_type.is_input else [Location.OBJECT] ) directives = ( directive for directive in strawberry_type.directives or [] if any( location in allowed_locations for location in directive.__strawberry_directive__.locations # type: ignore[attr-defined] ) ) return "".join( print_schema_directive(directive, schema=schema, extras=extras) for directive in directives ) def _print_object(type_: Any, schema: BaseSchema, *, extras: PrintExtras) -> str: return ( print_description(type_) + print_extends(type_, schema) + f"type {type_.name}" + print_implemented_interfaces(type_) + print_type_directives(type_, schema, extras=extras) + print_fields(type_, schema, extras=extras) ) def _print_interface(type_: Any, schema: BaseSchema, *, extras: PrintExtras) -> str: return ( print_description(type_) + print_extends(type_, schema) + f"interface {type_.name}" + print_implemented_interfaces(type_) + print_type_directives(type_, schema, extras=extras) + print_fields(type_, schema, extras=extras) ) def print_input_value(name: str, arg: GraphQLArgument) -> str: default_ast = ast_from_value(arg.default_value, arg.type) arg_decl = f"{name}: {arg.type}" if default_ast: arg_decl += f" = {print_ast(default_ast)}" return arg_decl + print_deprecated(arg.deprecation_reason) def _print_input_object(type_: Any, schema: BaseSchema, *, extras: PrintExtras) -> str: from strawberry.schema.schema_converter import GraphQLCoreConverter fields = [] for i, (name, field) in enumerate(type_.fields.items()): strawberry_field = field.extensions and field.extensions.get( GraphQLCoreConverter.DEFINITION_BACKREF ) fields.append( print_description(field, " ", not i) + " " + print_input_value(name, field) + print_field_directives(strawberry_field, schema=schema, extras=extras) ) return ( print_description(type_) + f"input {type_.name}" + print_type_directives(type_, schema, extras=extras) + print_block(fields) ) def print_union( type_: GraphQLUnionType, *, schema: BaseSchema, extras: PrintExtras ) -> str: strawberry_type = type_.extensions.get("strawberry-definition") directives = strawberry_type.directives if strawberry_type else [] printed_directives = "".join( print_schema_directive(directive, schema=schema, extras=extras) for directive in directives ) types = type_.types possible_types = " = " + " | ".join(t.name for t in types) if types else "" return ( print_description(type_) + f"union {type_.name}{printed_directives}" + possible_types ) def _print_type(type_: Any, schema: BaseSchema, *, extras: PrintExtras) -> str: # prevents us from trying to print a scalar as an input type if is_scalar_type(type_): return print_scalar(type_, schema=schema, extras=extras) if is_enum_type(type_): return print_enum(type_, schema=schema, extras=extras) if is_object_type(type_): return _print_object(type_, schema, extras=extras) if is_input_type(type_): return _print_input_object(type_, schema, extras=extras) if is_interface_type(type_): return _print_interface(type_, schema, extras=extras) if is_union_type(type_): return print_union(type_, schema=schema, extras=extras) return original_print_type(type_) def print_schema_directives(schema: BaseSchema, *, extras: PrintExtras) -> str: directives = ( directive for directive in schema.schema_directives if any( location in [Location.SCHEMA] for location in directive.__strawberry_directive__.locations # type: ignore ) ) return "".join( print_schema_directive(directive, schema=schema, extras=extras) for directive in directives ) def _all_root_names_are_common_names(schema: BaseSchema) -> bool: query = schema.query.__strawberry_definition__ mutation = schema.mutation.__strawberry_definition__ if schema.mutation else None subscription = ( schema.subscription.__strawberry_definition__ if schema.subscription else None ) return ( query.name == "Query" and (mutation is None or mutation.name == "Mutation") and (subscription is None or subscription.name == "Subscription") ) def print_schema_definition(schema: BaseSchema, *, extras: PrintExtras) -> str | None: # TODO: add support for description if _all_root_names_are_common_names(schema) and not schema.schema_directives: return None query_type = schema.query.__strawberry_definition__ operation_types = [f" query: {query_type.name}"] if schema.mutation: mutation_type = schema.mutation.__strawberry_definition__ operation_types.append(f" mutation: {mutation_type.name}") if schema.subscription: subscription_type = schema.subscription.__strawberry_definition__ operation_types.append(f" subscription: {subscription_type.name}") directives = print_schema_directives(schema, extras=extras) return f"schema{directives} {{\n" + "\n".join(operation_types) + "\n}" def print_directive(directive: GraphQLDirective, *, schema: BaseSchema) -> str | None: strawberry_directive = directive.extensions.get("strawberry-definition") if strawberry_directive is None or ( isinstance(strawberry_directive, StrawberrySchemaDirective) and not strawberry_directive.print_definition ): return None return ( print_description(directive) + f"directive @{directive.name}" # TODO: add support for directives on arguments directives + print_args(directive.args, schema=schema, extras=PrintExtras()) + (" repeatable" if directive.is_repeatable else "") + " on " + " | ".join(location.name for location in directive.locations) ) def is_builtin_directive(directive: GraphQLDirective) -> bool: # this allows to force print the builtin directives if there's a # directive that was implemented using the schema_directive if is_specified_directive(directive): strawberry_definition = directive.extensions.get("strawberry-definition") return strawberry_definition is None return False def print_schema(schema: BaseSchema) -> str: graphql_core_schema = cast( "GraphQLSchema", schema._schema, # type: ignore ) extras = PrintExtras() filtered_directives = [ directive for directive in graphql_core_schema.directives if not is_builtin_directive(directive) ] type_map = graphql_core_schema.type_map types = [ type_ for type_name in sorted(type_map) if is_defined_type(type_ := type_map[type_name]) ] types_printed = [_print_type(type_, schema, extras=extras) for type_ in types] schema_definition = print_schema_definition(schema, extras=extras) directives = [ printed_directive for directive in filtered_directives if (printed_directive := print_directive(directive, schema=schema)) is not None ] if schema.config.enable_experimental_incremental_execution: directives.append( "directive @defer(if: Boolean, label: String) on FRAGMENT_SPREAD | INLINE_FRAGMENT" ) directives.append( "directive @stream(if: Boolean, label: String, initialCount: Int = 0) on FIELD" ) def _name_getter(type_: Any) -> str: if hasattr(type_, "name"): return type_.name if isinstance(type_, ScalarWrapper): return type_._scalar_definition.name return type_.__name__ return "\n\n".join( chain( sorted(extras.directives), filter(None, [schema_definition]), directives, types_printed, ( _print_type( schema.schema_converter.from_type(type_), schema, extras=extras ) # Make sure extra types are ordered for predictive printing for type_ in sorted(extras.types, key=_name_getter) ), ) ) __all__ = ["print_schema"] strawberry-graphql-0.287.0/strawberry/py.typed000066400000000000000000000000001511033167500214230ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/quart/000077500000000000000000000000001511033167500210725ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/quart/__init__.py000066400000000000000000000000001511033167500231710ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/quart/views.py000066400000000000000000000150221511033167500226010ustar00rootroot00000000000000import asyncio import warnings from collections.abc import AsyncGenerator, Callable, Mapping, Sequence from datetime import timedelta from json.decoder import JSONDecodeError from typing import TYPE_CHECKING, ClassVar, TypeGuard, Union from lia import HTTPException, QuartHTTPRequestAdapter from quart import Request, Response, Websocket, request, websocket from quart.ctx import has_websocket_context from quart.views import View from strawberry.http.async_base_view import ( AsyncBaseHTTPView, AsyncWebSocketAdapter, ) from strawberry.http.exceptions import ( NonJsonMessageReceived, NonTextMessageReceived, WebSocketDisconnected, ) from strawberry.http.ides import GraphQL_IDE from strawberry.http.typevars import Context, RootValue from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL if TYPE_CHECKING: from quart.typing import ResponseReturnValue from strawberry.http import GraphQLHTTPResponse from strawberry.schema.base import BaseSchema class QuartWebSocketAdapter(AsyncWebSocketAdapter): def __init__( self, view: AsyncBaseHTTPView, request: Websocket, response: Response ) -> None: super().__init__(view) self.ws = request async def iter_json( self, *, ignore_parsing_errors: bool = False ) -> AsyncGenerator[object, None]: try: while True: # Raises asyncio.CancelledError when the connection is closed. # https://quart.palletsprojects.com/en/latest/how_to_guides/websockets.html#detecting-disconnection message = await self.ws.receive() if not isinstance(message, str): raise NonTextMessageReceived try: yield self.view.decode_json(message) except JSONDecodeError as e: if not ignore_parsing_errors: raise NonJsonMessageReceived from e except asyncio.CancelledError: pass async def send_json(self, message: Mapping[str, object]) -> None: try: # Raises asyncio.CancelledError when the connection is closed. # https://quart.palletsprojects.com/en/latest/how_to_guides/websockets.html#detecting-disconnection await self.ws.send(self.view.encode_json(message)) # type:ignore # quart is misusing AnyStr, leading to type errors for unions, see https://github.com/pallets/quart/issues/451 except asyncio.CancelledError as exc: raise WebSocketDisconnected from exc async def close(self, code: int, reason: str) -> None: await self.ws.close(code, reason=reason) class GraphQLView( AsyncBaseHTTPView[ Request, Response, Response, Websocket, Response, Context, RootValue ], View, ): methods: ClassVar[list[str]] = ["GET", "POST"] allow_queries_via_get: bool = True request_adapter_class = QuartHTTPRequestAdapter websocket_adapter_class = QuartWebSocketAdapter # type: ignore def __init__( self, schema: "BaseSchema", graphiql: bool | None = None, graphql_ide: GraphQL_IDE | None = "graphiql", allow_queries_via_get: bool = True, keep_alive: bool = True, keep_alive_interval: float = 1, subscription_protocols: Sequence[str] = ( GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL, ), connection_init_wait_timeout: timedelta = timedelta(minutes=1), multipart_uploads_enabled: bool = False, ) -> None: self.schema = schema self.allow_queries_via_get = allow_queries_via_get self.keep_alive = keep_alive self.keep_alive_interval = keep_alive_interval self.subscription_protocols = subscription_protocols self.connection_init_wait_timeout = connection_init_wait_timeout self.multipart_uploads_enabled = multipart_uploads_enabled if graphiql is not None: warnings.warn( "The `graphiql` argument is deprecated in favor of `graphql_ide`", DeprecationWarning, stacklevel=2, ) self.graphql_ide = "graphiql" if graphiql else None else: self.graphql_ide = graphql_ide async def render_graphql_ide(self, request: Request) -> Response: return Response(self.graphql_ide_html) def create_response( self, response_data: Union["GraphQLHTTPResponse", list["GraphQLHTTPResponse"]], sub_response: Response, ) -> Response: sub_response.set_data(self.encode_json(response_data)) return sub_response async def get_context( self, request: Request | Websocket, response: Response ) -> Context: return {"request": request, "response": response} # type: ignore async def get_root_value(self, request: Request | Websocket) -> RootValue | None: return None async def get_sub_response(self, request: Request) -> Response: return Response(status=200, content_type="application/json") async def dispatch_request(self, **kwargs: object) -> "ResponseReturnValue": try: return await self.run( request=websocket if has_websocket_context() else request ) except HTTPException as e: return Response( response=e.reason, status=e.status_code, content_type="text/plain", ) async def create_streaming_response( self, request: Request, stream: Callable[[], AsyncGenerator[str, None]], sub_response: Response, headers: dict[str, str], ) -> Response: return ( stream(), sub_response.status_code, { # type: ignore **sub_response.headers, **headers, }, ) def is_websocket_request( self, request: Request | Websocket ) -> TypeGuard[Websocket]: return has_websocket_context() async def pick_websocket_subprotocol(self, request: Websocket) -> str | None: protocols = request.requested_subprotocols intersection = set(protocols) & set(self.subscription_protocols) sorted_intersection = sorted(intersection, key=protocols.index) return next(iter(sorted_intersection), None) async def create_websocket_response( self, request: Websocket, subprotocol: str | None ) -> Response: await request.accept(subprotocol=subprotocol) return Response() __all__ = ["GraphQLView"] strawberry-graphql-0.287.0/strawberry/relay/000077500000000000000000000000001511033167500210525ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/relay/__init__.py000066400000000000000000000010511511033167500231600ustar00rootroot00000000000000from .fields import ConnectionExtension, NodeExtension, connection, node from .types import ( Connection, Edge, GlobalID, GlobalIDValueError, ListConnection, Node, NodeID, NodeType, PageInfo, ) from .utils import from_base64, to_base64 __all__ = [ "Connection", "ConnectionExtension", "Edge", "GlobalID", "GlobalIDValueError", "ListConnection", "Node", "NodeExtension", "NodeID", "NodeType", "PageInfo", "connection", "from_base64", "node", "to_base64", ] strawberry-graphql-0.287.0/strawberry/relay/exceptions.py000066400000000000000000000076611511033167500236170ustar00rootroot00000000000000from __future__ import annotations from functools import cached_property from typing import TYPE_CHECKING, cast from strawberry.exceptions.exception import StrawberryException from strawberry.exceptions.utils.source_finder import SourceFinder if TYPE_CHECKING: from collections.abc import Callable from strawberry.exceptions.exception_source import ExceptionSource from strawberry.types.fields.resolver import StrawberryResolver class NodeIDAnnotationError(StrawberryException): def __init__(self, message: str, cls: type) -> None: self.cls = cls self.message = message self.rich_message = ( "Expected exactly one `relay.NodeID` annotated field to be " f"defined in `[underline]{self.cls.__name__}[/]` type." ) self.suggestion = ( "To fix this error you should annotate exactly one of your fields " "using `relay.NodeID`. That field should be unique among " "your type objects (usually its `id` for ORM objects)." ) self.annotation_message = "node missing node id private annotation" super().__init__(self.message) @cached_property def exception_source(self) -> ExceptionSource | None: if self.cls is None: return None # pragma: no cover source_finder = SourceFinder() return source_finder.find_class_from_object(self.cls) class RelayWrongAnnotationError(StrawberryException): def __init__(self, field_name: str, cls: type) -> None: self.cls = cls self.field_name = field_name self.message = ( f'Wrong annotation used on field "{field_name}". It should be ' 'annotated with a "Connection" subclass.' ) self.rich_message = ( f"Wrong annotation for field `[underline]{self.field_name}[/]`" ) self.suggestion = ( "To fix this error you can add a valid annotation, " f"like [italic]`{self.field_name}: relay.Connection[{cls}]` " f"or [italic]`@relay.connection(relay.Connection[{cls}])`" ) self.annotation_message = "relay wrong annotation" super().__init__(self.message) @cached_property def exception_source(self) -> ExceptionSource | None: if self.cls is None: return None # pragma: no cover source_finder = SourceFinder() return source_finder.find_class_attribute_from_object(self.cls, self.field_name) class RelayWrongResolverAnnotationError(StrawberryException): def __init__(self, field_name: str, resolver: StrawberryResolver) -> None: self.function = resolver.wrapped_func self.field_name = field_name self.message = ( f'Wrong annotation used on "{field_name}" resolver. ' "It should be return an iterable or async iterable object." ) self.rich_message = ( f"Wrong annotation used on `{field_name}` resolver. " "It should be return an `iterable` or `async iterable` object." ) self.suggestion = ( "To fix this error you can annootate your resolver to return " "one of the following options: `list[]`, " "`Iterator[]`, `Iterable[]`, " "`AsyncIterator[]`, `AsyncIterable[]`, " "`Generator[, Any, Any]` and " "`AsyncGenerator[, Any]`." ) self.annotation_message = "relay wrong resolver annotation" super().__init__(self.message) @cached_property def exception_source(self) -> ExceptionSource | None: if self.function is None: return None # pragma: no cover source_finder = SourceFinder() return source_finder.find_function_from_object(cast("Callable", self.function)) __all__ = [ "NodeIDAnnotationError", "RelayWrongAnnotationError", "RelayWrongResolverAnnotationError", ] strawberry-graphql-0.287.0/strawberry/relay/fields.py000066400000000000000000000435111511033167500226760ustar00rootroot00000000000000from __future__ import annotations import asyncio import dataclasses import inspect from collections import defaultdict from collections.abc import ( AsyncIterable, AsyncIterator, Awaitable, Callable, Iterable, Iterator, Mapping, Sequence, ) from typing import ( TYPE_CHECKING, Annotated, Any, ForwardRef, Optional, cast, get_args, get_origin, ) from strawberry.annotation import StrawberryAnnotation from strawberry.extensions.field_extension import ( AsyncExtensionResolver, FieldExtension, SyncExtensionResolver, ) from strawberry.relay.exceptions import ( RelayWrongAnnotationError, RelayWrongResolverAnnotationError, ) from strawberry.types.arguments import StrawberryArgument, argument from strawberry.types.base import StrawberryList, StrawberryOptional from strawberry.types.cast import cast as strawberry_cast from strawberry.types.field import _RESOLVER_TYPE, StrawberryField, field from strawberry.types.fields.resolver import StrawberryResolver from strawberry.types.lazy_type import LazyType from strawberry.utils.aio import asyncgen_to_list from strawberry.utils.typing import eval_type, is_generic_alias, is_optional, is_union from .types import Connection, GlobalID, Node if TYPE_CHECKING: from typing import Literal from strawberry.permission import BasePermission from strawberry.types.info import Info class NodeExtension(FieldExtension): def apply(self, field: StrawberryField) -> None: assert field.base_resolver is None if isinstance(field.type, StrawberryList): resolver = self.get_node_list_resolver(field) else: resolver = self.get_node_resolver(field) # type: ignore field.base_resolver = StrawberryResolver(resolver, type_override=field.type) def resolve( self, next_: SyncExtensionResolver, source: Any, info: Info, **kwargs: Any ) -> Any: return next_(source, info, **kwargs) async def resolve_async( self, next_: SyncExtensionResolver, source: Any, info: Info, **kwargs: Any ) -> Any: retval = next_(source, info, **kwargs) # If the resolve_nodes method is not async, retval will not actually # be awaitable. We still need the `resolve_async` in here because # otherwise this extension can't be used together with other # async extensions. return await retval if inspect.isawaitable(retval) else retval def get_node_resolver( self, field: StrawberryField ) -> Callable[[Info, GlobalID], Node | None | Awaitable[Node | None]]: type_ = field.type is_optional = isinstance(type_, StrawberryOptional) def resolver( info: Info, id: Annotated[GlobalID, argument(description="The ID of the object.")], ) -> Node | None | Awaitable[Node | None]: node_type = id.resolve_type(info) resolved_node = node_type.resolve_node( id.node_id, info=info, required=not is_optional, ) # We are using `strawberry_cast` here to cast the resolved node to make # sure `is_type_of` will not try to find its type again. Very important # when returning a non type (e.g. Django/SQLAlchemy/Pydantic model), as # we could end up resolving to a different type in case more than one # are registered. if inspect.isawaitable(resolved_node): async def resolve() -> Any: return strawberry_cast(node_type, await resolved_node) return resolve() return cast("Node", strawberry_cast(node_type, resolved_node)) return resolver def get_node_list_resolver( self, field: StrawberryField ) -> Callable[[Info, list[GlobalID]], list[Node] | Awaitable[list[Node]]]: type_ = field.type assert isinstance(type_, StrawberryList) is_optional = isinstance(type_.of_type, StrawberryOptional) def resolver( info: Info, ids: Annotated[ list[GlobalID], argument(description="The IDs of the objects.") ], ) -> list[Node] | Awaitable[list[Node]]: nodes_map: defaultdict[type[Node], list[str]] = defaultdict(list) # Store the index of the node in the list of nodes of the same type # so that we can return them in the same order while also supporting # different types index_map: dict[GlobalID, tuple[type[Node], int]] = {} for gid in ids: node_t = gid.resolve_type(info) nodes_map[node_t].append(gid.node_id) index_map[gid] = (node_t, len(nodes_map[node_t]) - 1) resolved_nodes = { node_t: node_t.resolve_nodes( info=info, node_ids=node_ids, required=not is_optional, ) for node_t, node_ids in nodes_map.items() } awaitable_nodes = { node_t: nodes for node_t, nodes in resolved_nodes.items() if inspect.isawaitable(nodes) } # Async generators are not awaitable, so we need to handle them separately asyncgen_nodes = { node_t: nodes for node_t, nodes in resolved_nodes.items() if inspect.isasyncgen(nodes) } # We are using `strawberry_cast` here to cast the resolved node to make # sure `is_type_of` will not try to find its type again. Very important # when returning a non type (e.g. Django/SQLAlchemy/Pydantic model), as # we could end up resolving to a different type in case more than one # are registered def cast_nodes(node_t: type[Node], nodes: Iterable[Any]) -> list[Node]: return [cast("Node", strawberry_cast(node_t, node)) for node in nodes] if awaitable_nodes or asyncgen_nodes: async def resolve(resolved: Any = resolved_nodes) -> list[Node]: resolved.update( zip( [ *awaitable_nodes.keys(), *asyncgen_nodes.keys(), ], # Resolve all awaitable nodes concurrently await asyncio.gather( *awaitable_nodes.values(), *( asyncgen_to_list(nodes) # type: ignore for nodes in asyncgen_nodes.values() ), ), strict=True, ) ) # Resolve any generator to lists resolved = { node_t: cast_nodes(node_t, nodes) for node_t, nodes in resolved.items() } return [ resolved[index_map[gid][0]][index_map[gid][1]] for gid in ids ] return resolve() # Resolve any generator to lists resolved = { node_t: cast_nodes(node_t, cast("Iterable[Node]", nodes)) for node_t, nodes in resolved_nodes.items() } return [resolved[index_map[gid][0]][index_map[gid][1]] for gid in ids] return resolver class ConnectionExtension(FieldExtension): connection_type: type[Connection[Node]] def __init__(self, max_results: int | None = None) -> None: self.max_results = max_results def apply(self, field: StrawberryField) -> None: field.arguments = [ *field.arguments, StrawberryArgument( python_name="before", graphql_name=None, type_annotation=StrawberryAnnotation(Optional[str]), # noqa: UP045 description=( "Returns the items in the list that come before the " "specified cursor." ), default=None, ), StrawberryArgument( python_name="after", graphql_name=None, type_annotation=StrawberryAnnotation(Optional[str]), # noqa: UP045 description=( "Returns the items in the list that come after the " "specified cursor." ), default=None, ), StrawberryArgument( python_name="first", graphql_name=None, type_annotation=StrawberryAnnotation(Optional[int]), # noqa: UP045 description="Returns the first n items from the list.", default=None, ), StrawberryArgument( python_name="last", graphql_name=None, type_annotation=StrawberryAnnotation(Optional[int]), # noqa: UP045 description=( "Returns the items in the list that come after the " "specified cursor." ), default=None, ), ] f_type = field.type if isinstance(f_type, LazyType): f_type = f_type.resolve_type() field.type = f_type if isinstance(f_type, StrawberryOptional): f_type = f_type.of_type if isinstance(f_type, LazyType): f_type = f_type.resolve_type() type_origin = get_origin(f_type) if is_generic_alias(f_type) else f_type if isinstance(type_origin, LazyType): type_origin = type_origin.resolve_type() if not isinstance(type_origin, type) or not issubclass(type_origin, Connection): raise RelayWrongAnnotationError(field.name, cast("type", field.origin)) assert field.base_resolver # TODO: We are not using resolver_type.type because it will call # StrawberryAnnotation.resolve, which will strip async types from the # type (i.e. AsyncGenerator[Fruit] will become Fruit). This is done there # for subscription support, but we can't use it here. Maybe we can refactor # this in the future. resolver_type = field.base_resolver.signature.return_annotation if isinstance(resolver_type, str): resolver_type = ForwardRef(resolver_type) if isinstance(resolver_type, ForwardRef): resolver_type = eval_type( resolver_type, field.base_resolver._namespace, None, ) if is_union(resolver_type): assert is_optional(resolver_type) resolver_type = get_args(resolver_type)[0] origin = get_origin(resolver_type) if origin is None or not issubclass( origin, (Iterator, Iterable, AsyncIterator, AsyncIterable) ): raise RelayWrongResolverAnnotationError(field.name, field.base_resolver) self.connection_type = cast("type[Connection[Node]]", f_type) def resolve( self, next_: SyncExtensionResolver, source: Any, info: Info, *, before: str | None = None, after: str | None = None, first: int | None = None, last: int | None = None, **kwargs: Any, ) -> Any: assert self.connection_type is not None return self.connection_type.resolve_connection( cast("Iterable[Node]", next_(source, info, **kwargs)), info=info, before=before, after=after, first=first, last=last, max_results=self.max_results, ) async def resolve_async( self, next_: AsyncExtensionResolver, source: Any, info: Info, *, before: str | None = None, after: str | None = None, first: int | None = None, last: int | None = None, **kwargs: Any, ) -> Any: assert self.connection_type is not None nodes = next_(source, info, **kwargs) # nodes might be an AsyncIterable/AsyncIterator # In this case we don't await for it if inspect.isawaitable(nodes): nodes = await nodes resolved = self.connection_type.resolve_connection( cast("Iterable[Node]", nodes), info=info, before=before, after=after, first=first, last=last, max_results=self.max_results, ) # If nodes was an AsyncIterable/AsyncIterator, resolve_connection # will return a coroutine which we need to await if inspect.isawaitable(resolved): resolved = await resolved return resolved if TYPE_CHECKING: node = field else: def node(*args: Any, **kwargs: Any) -> StrawberryField: kwargs["extensions"] = [*kwargs.get("extensions", []), NodeExtension()] return field(*args, **kwargs) # we used to have `Type[Connection[NodeType]]` here, but that when we added # support for making the Connection type optional, we had to change it to # `Any` because otherwise it wouldn't be type check since `Optional[Connection[Something]]` # is not a `Type`, but a special form, see https://discuss.python.org/t/is-annotated-compatible-with-type-t/43898/46 # for more information, and also https://peps.python.org/pep-0747/, which is currently # in draft status (and no type checker supports it yet) ConnectionGraphQLType = Any def connection( graphql_type: ConnectionGraphQLType | None = None, *, resolver: _RESOLVER_TYPE[Any] | None = None, name: str | None = None, is_subscription: bool = False, description: str | None = None, permission_classes: list[type[BasePermission]] | None = None, deprecation_reason: str | None = None, default: Any = dataclasses.MISSING, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: Mapping[Any, Any] | None = None, directives: Sequence[object] | None = (), extensions: list[FieldExtension] | None = None, max_results: int | None = None, # This init parameter is used by pyright to determine whether this field # is added in the constructor or not. It is not used to change # any behaviour at the moment. init: Literal[True, False] | None = None, ) -> Any: """Annotate a property or a method to create a relay connection field. Relay connections are mostly used for pagination purposes. This decorator helps creating a complete relay endpoint that provides default arguments and has a default implementation for the connection slicing. Note that when setting a resolver to this field, it is expected for this resolver to return an iterable of the expected node type, not the connection itself. That iterable will then be paginated accordingly. So, the main use case for this is to provide a filtered iterable of nodes by using some custom filter arguments. Args: graphql_type: The type of the nodes in the connection. This is used to determine the type of the edges and the node field in the connection. resolver: The resolver for the connection. This is expected to return an iterable of the expected node type. name: The GraphQL name of the field. is_subscription: Whether the field is a subscription. description: The GraphQL description of the field. permission_classes: The permission classes to apply to the field. deprecation_reason: The deprecation reason of the field. default: The default value of the field. default_factory: The default factory of the field. metadata: The metadata of the field. directives: The directives to apply to the field. extensions: The extensions to apply to the field. max_results: The maximum number of results this connection can return. Can be set to override the default value of 100 defined in the schema configuration. init: Used only for type checking purposes. Examples: Annotating something like this: ```python @strawberry.type class X: some_node: relay.Connection[SomeType] = relay.connection( resolver=get_some_nodes, description="ABC", ) @relay.connection(relay.Connection[SomeType], description="ABC") def get_some_nodes(self, age: int) -> Iterable[SomeType]: ... ``` Will produce a query like this: ```graphql query { someNode ( before: String after: String first: String after: String age: Int ) { totalCount pageInfo { hasNextPage hasPreviousPage startCursor endCursor } edges { cursor node { id ... } } } } ``` .. _Relay connections: https://relay.dev/graphql/connections.htm """ extensions = extensions or [] f = StrawberryField( python_name=None, graphql_name=name, description=description, type_annotation=StrawberryAnnotation.from_annotation(graphql_type), is_subscription=is_subscription, permission_classes=permission_classes or [], deprecation_reason=deprecation_reason, default=default, default_factory=default_factory, metadata=metadata, directives=directives or (), extensions=[*extensions, ConnectionExtension(max_results=max_results)], ) if resolver is not None: f = f(resolver) return f __all__ = ["connection", "node"] strawberry-graphql-0.287.0/strawberry/relay/types.py000066400000000000000000000732041511033167500225760ustar00rootroot00000000000000from __future__ import annotations import dataclasses import inspect import itertools import sys from collections.abc import ( AsyncIterable, AsyncIterator, Awaitable, Iterable, Iterator, Sequence, ) from typing import ( TYPE_CHECKING, Annotated, Any, ClassVar, ForwardRef, Generic, Literal, TypeAlias, TypeVar, Union, cast, get_args, get_origin, overload, ) from typing_extensions import Self from strawberry.relay.exceptions import NodeIDAnnotationError from strawberry.types.base import ( StrawberryContainer, StrawberryObjectDefinition, get_object_definition, ) from strawberry.types.field import field from strawberry.types.info import Info # noqa: TC001 from strawberry.types.lazy_type import LazyType from strawberry.types.object_type import interface from strawberry.types.object_type import type as strawberry_type from strawberry.types.private import StrawberryPrivate from strawberry.utils.aio import aclosing, aenumerate, aislice, resolve_awaitable from strawberry.utils.inspect import in_async_context from strawberry.utils.typing import eval_type, is_classvar from .utils import ( SliceMetadata, from_base64, should_resolve_list_connection_edges, to_base64, ) if TYPE_CHECKING: from strawberry.scalars import ID from strawberry.utils.await_maybe import AwaitableOrValue _T = TypeVar("_T") NodeIterableType: TypeAlias = ( Iterator[_T] | Iterable[_T] | AsyncIterator[_T] | AsyncIterable[_T] ) NodeType = TypeVar("NodeType", bound="Node") PREFIX = "arrayconnection" class GlobalIDValueError(ValueError): """GlobalID value error, usually related to parsing or serialization.""" @dataclasses.dataclass(order=True, frozen=True) class GlobalID: """Global ID for relay types. Different from `strawberry.ID`, this ID wraps the original object ID in a string that contains both its GraphQL type name and the ID itself, and encodes it to a base64_ string. This object contains helpers to work with that, including method to retrieve the python object type or even the encoded node itself. Attributes: type_name: The type name part of the id node_id: The node id part of the id .. _base64: https://en.wikipedia.org/wiki/Base64 """ type_name: str node_id: str def __post_init__(self) -> None: if not isinstance(self.type_name, str): raise GlobalIDValueError( f"type_name is expected to be a string, found {self.type_name!r}" ) if not isinstance(self.node_id, str): raise GlobalIDValueError( f"node_id is expected to be a string, found {self.node_id!r}" ) def __str__(self) -> str: return to_base64(self.type_name, self.node_id) @classmethod def from_id(cls, value: str | ID) -> Self: """Create a new GlobalID from parsing the given value. Args: value: The value to be parsed, as a base64 string in the "TypeName:NodeID" format Returns: An instance of GLobalID Raises: GlobalIDValueError: If the value is not in a GLobalID format """ try: type_name, node_id = from_base64(value) except ValueError as e: raise GlobalIDValueError(str(e)) from e return cls(type_name=type_name, node_id=node_id) @overload async def resolve_node( self, info: Info, *, required: Literal[True] = ..., ensure_type: type[_T], ) -> _T: ... @overload async def resolve_node( self, info: Info, *, required: Literal[True], ensure_type: None = ..., ) -> Node: ... @overload async def resolve_node( self, info: Info, *, required: bool = ..., ensure_type: None = ..., ) -> Node | None: ... async def resolve_node(self, info, *, required=False, ensure_type=None) -> Any: """Resolve the type name and node id info to the node itself. Tip: When you know the expected type, calling `ensure_type` should help not only to enforce it, but also help with typing since it will know that, if this function returns successfully, the retval should be of that type and not `Node`. Args: info: The strawberry execution info resolve the type name from required: If the value is required to exist. Note that asking to ensure the type automatically makes required true. ensure_type: Optionally check if the returned node is really an instance of this type. Returns: The resolved node Raises: TypeError: If ensure_type was provided and the type is not an instance of it """ n_type = self.resolve_type(info) node: Node | Awaitable[Node] = cast( "Awaitable[Node]", n_type.resolve_node( self.node_id, info=info, required=required or ensure_type is not None, ), ) if node is not None and inspect.isawaitable(node): node = await node if ensure_type is not None: origin = get_origin(ensure_type) if origin and origin is Union: ensure_type = tuple(get_args(ensure_type)) if not isinstance(node, ensure_type): msg = ( f"Cannot resolve. GlobalID requires {ensure_type}, received {node!r}. " "Verify that the supplied ID is intended for this Query/Mutation/Subscription." ) raise TypeError(msg) return node def resolve_type(self, info: Info) -> type[Node]: """Resolve the internal type name to its type itself. Args: info: The strawberry execution info resolve the type name from Returns: The resolved GraphQL type for the execution info """ type_def = info.schema.get_type_by_name(self.type_name) if not isinstance(type_def, StrawberryObjectDefinition): raise GlobalIDValueError( f"Cannot resolve. GlobalID requires a GraphQL type, " f"received `{self.type_name}`." ) origin = ( type_def.origin.resolve_type if isinstance(type_def.origin, LazyType) else type_def.origin ) if not issubclass(origin, Node): raise GlobalIDValueError( f"Cannot resolve. GlobalID requires a GraphQL Node type, " f"received `{self.type_name}`." ) return origin @overload def resolve_node_sync( self, info: Info, *, required: Literal[True] = ..., ensure_type: type[_T], ) -> _T: ... @overload def resolve_node_sync( self, info: Info, *, required: Literal[True], ensure_type: None = ..., ) -> Node: ... @overload def resolve_node_sync( self, info: Info, *, required: bool = ..., ensure_type: None = ..., ) -> Node | None: ... def resolve_node_sync(self, info, *, required=False, ensure_type=None) -> Any: """Resolve the type name and node id info to the node itself. Tip: When you know the expected type, calling `ensure_type` should help not only to enforce it, but also help with typing since it will know that, if this function returns successfully, the retval should be of that type and not `Node`. Args: info: The strawberry execution info resolve the type name from required: If the value is required to exist. Note that asking to ensure the type automatically makes required true. ensure_type: Optionally check if the returned node is really an instance of this type. Returns: The resolved node Raises: TypeError: If ensure_type was provided and the type is not an instance of it """ n_type = self.resolve_type(info) node = n_type.resolve_node( self.node_id, info=info, required=required or ensure_type is not None, ) if node is not None and ensure_type is not None: origin = get_origin(ensure_type) if origin and origin is Union: ensure_type = tuple(get_args(ensure_type)) if not isinstance(node, ensure_type): msg = ( f"Cannot resolve. GlobalID requires {ensure_type}, received {node!r}. " "Verify that the supplied ID is intended for this Query/Mutation/Subscription." ) raise TypeError(msg) return node class NodeIDPrivate(StrawberryPrivate): """Annotate a type attribute as its id. The `Node` interface will automatically create and resolve GlobalIDs based on the field annotated with `NodeID`. e.g: ```python import strawberry @strawberry.type class Fruit(Node): code: NodeID[str] ``` In this case, `code` will be used to generate a global ID in the format `Fruit:` and will be exposed as `id: GlobalID!` in the `Fruit` type. """ NodeID: TypeAlias = Annotated[_T, NodeIDPrivate()] @interface(description="An object with a Globally Unique ID") class Node: """Node interface for GraphQL types. Subclasses must type the id field using `NodeID`. It will be private to the schema because it will be converted to a global ID and exposed as `id: GlobalID!` The following methods can also be implemented: resolve_id: (Optional) Called to resolve the node's id. Can be overriden to customize how the id is retrieved (e.g. in case you don't want to define a `NodeID` field) resolve_nodes: Called to retrieve an iterable of node given their ids resolve_node: (Optional) Called to retrieve a node given its id. If not defined the default implementation will call `.resolve_nodes` with that single node id. Example: ```python import strawberry @strawberry.type class Fruit(strawberry.relay.Node): id: strawberry.relay.NodeID[int] name: str @classmethod def resolve_nodes(cls, *, info, node_ids, required=False): # Return an iterable of fruits in here ... ``` """ _id_attr: ClassVar[str | None] = None @field(name="id", description="The Globally Unique ID of this object") @classmethod def _id(cls, root: Node, info: Info) -> GlobalID: # NOTE: root might not be a Node instance when using integrations which # return an object that is compatible with the type (e.g. the django one). # In that case, we can retrieve the type itself from info if isinstance(root, Node): resolve_id = root.__class__.resolve_id resolve_typename = root.__class__.resolve_typename else: parent_type = info._raw_info.parent_type type_def = info.schema.get_type_by_name(parent_type.name) assert isinstance(type_def, StrawberryObjectDefinition) origin = cast("type[Node]", type_def.origin) resolve_id = origin.resolve_id resolve_typename = origin.resolve_typename type_name = resolve_typename(root, info) assert isinstance(type_name, str) node_id = resolve_id(root, info=info) assert node_id is not None if inspect.isawaitable(node_id): return cast( "GlobalID", resolve_awaitable( node_id, lambda resolved: GlobalID( type_name=type_name, node_id=str(resolved), ), ), ) # If node_id is not str, GlobalID will raise an error for us return GlobalID(type_name=type_name, node_id=str(node_id)) @classmethod def resolve_id_attr(cls) -> str: if cls._id_attr is not None: return cls._id_attr candidates: list[str] = [] for base in cls.__mro__: base_namespace = sys.modules[base.__module__].__dict__ for attr_name, attr in getattr(base, "__annotations__", {}).items(): # Some ClassVar might raise TypeError when being resolved # on some python versions. This is fine to skip since # we are not interested in ClassVars here if is_classvar(base, attr): continue evaled = eval_type( ForwardRef(attr) if isinstance(attr, str) else attr, globalns=base_namespace, ) if get_origin(evaled) is Annotated and any( isinstance(a, NodeIDPrivate) for a in get_args(evaled) ): candidates.append(attr_name) # If we found candidates in this base, stop looking for more # This is to support subclasses to define something else than # its superclass as a NodeID if candidates: break if len(candidates) == 0: raise NodeIDAnnotationError( f'No field annotated with `NodeID` found in "{cls.__name__}"', cls ) if len(candidates) > 1: raise NodeIDAnnotationError( ( "More than one field annotated with `NodeID` " f'found in "{cls.__name__}"' ), cls, ) cls._id_attr = candidates[0] return cls._id_attr @classmethod def resolve_id( cls, root: Self, *, info: Info, ) -> AwaitableOrValue[str]: """Resolve the node id. By default this will return `getattr(root, )`, where is the field typed with `NodeID`. You can override this method to provide a custom implementation. Args: info: The strawberry execution info resolve the type name from. root: The node to resolve. Returns: The resolved id (which is expected to be str) """ return getattr(root, cls.resolve_id_attr()) @classmethod def resolve_typename(cls, root: Self, info: Info) -> str: typename = info.path.typename assert typename is not None return typename @overload @classmethod def resolve_nodes( cls, *, info: Info, node_ids: Iterable[str], required: Literal[True], ) -> AwaitableOrValue[Iterable[Self]]: ... @overload @classmethod def resolve_nodes( cls, *, info: Info, node_ids: Iterable[str], required: Literal[False] = ..., ) -> AwaitableOrValue[Iterable[Self | None]]: ... @overload @classmethod def resolve_nodes( cls, *, info: Info, node_ids: Iterable[str], required: bool, ) -> AwaitableOrValue[Iterable[Self]] | AwaitableOrValue[Iterable[Self | None]]: ... @classmethod def resolve_nodes( cls, *, info: Info, node_ids: Iterable[str], required: bool = False, ): """Resolve a list of nodes. This method *should* be defined by anyone implementing the `Node` interface. The nodes should be returned in the same order as the provided ids. Also, if `required` is `True`, all ids must be resolved or an error should be raised. If `required` is `False`, missing nodes should be returned as `None`. Args: info: The strawberry execution info resolve the type name from. node_ids: List of node ids that should be returned. required: If `True`, all `node_ids` requested must exist. If they don't, an error must be raised. If `False`, missing nodes should be returned as `None`. It only makes sense when passing a list of `node_ids`, otherwise it will should ignored. Returns: An iterable of resolved nodes. """ raise NotImplementedError @overload @classmethod def resolve_node( cls, node_id: str, *, info: Info, required: Literal[True], ) -> AwaitableOrValue[Self]: ... @overload @classmethod def resolve_node( cls, node_id: str, *, info: Info, required: Literal[False] = ..., ) -> AwaitableOrValue[Self | None]: ... @overload @classmethod def resolve_node( cls, node_id: str, *, info: Info, required: bool, ) -> AwaitableOrValue[Self | None]: ... @classmethod def resolve_node( cls, node_id: str, *, info: Info, required: bool = False, ) -> AwaitableOrValue[Self | None]: """Resolve a node given its id. This method is a convenience method that calls `resolve_nodes` for a single node id. Args: info: The strawberry execution info resolve the type name from. node_id: The id of the node to be retrieved. required: if the node is required or not to exist. If not, then None should be returned if it doesn't exist. Otherwise an exception should be raised. Returns: The resolved node or None if it was not found """ retval = cls.resolve_nodes(info=info, node_ids=[node_id], required=required) if inspect.isawaitable(retval): return resolve_awaitable(retval, lambda resolved: next(iter(resolved))) return next(iter(cast("Iterable[Self]", retval))) @strawberry_type(description="Information to aid in pagination.") class PageInfo: """Information to aid in pagination. Attributes: has_next_page: When paginating forwards, are there more items? has_previous_page: When paginating backwards, are there more items? start_cursor: When paginating backwards, the cursor to continue end_cursor: When paginating forwards, the cursor to continue """ has_next_page: bool = field( description="When paginating forwards, are there more items?", ) has_previous_page: bool = field( description="When paginating backwards, are there more items?", ) start_cursor: str | None = field( description="When paginating backwards, the cursor to continue.", ) end_cursor: str | None = field( description="When paginating forwards, the cursor to continue.", ) @strawberry_type(description="An edge in a connection.") class Edge(Generic[NodeType]): """An edge in a connection. Attributes: cursor: A cursor for use in pagination node: The item at the end of the edge """ cursor: str = field(description="A cursor for use in pagination") node: NodeType = field(description="The item at the end of the edge") CURSOR_PREFIX: ClassVar[str] = PREFIX @classmethod def resolve_edge(cls, node: NodeType, *, cursor: Any = None, **kwargs: Any) -> Self: return cls(cursor=to_base64(cls.CURSOR_PREFIX, cursor), node=node, **kwargs) @strawberry_type(description="A connection to a list of items.") class Connection(Generic[NodeType]): """A connection to a list of items. Attributes: page_info: Pagination data for this connection edges: Contains the nodes in this connection """ page_info: PageInfo = field(description="Pagination data for this connection") edges: list[Edge[NodeType]] = field( description="Contains the nodes in this connection" ) @classmethod def resolve_node(cls, node: Any, *, info: Info, **kwargs: Any) -> NodeType: """The identity function for the node. This method is used to resolve a node of a different type to the connection's `NodeType`. By default it returns the node itself, but subclasses can override this to provide a custom implementation. Args: node: The resolved node which should return an instance of this connection's `NodeType`. info: The strawberry execution info resolve the type name from. **kwargs: Additional arguments passed to the resolver. """ return node @classmethod def resolve_connection( cls, nodes: NodeIterableType[NodeType], *, info: Info, before: str | None = None, after: str | None = None, first: int | None = None, last: int | None = None, max_results: int | None = None, **kwargs: Any, ) -> AwaitableOrValue[Self]: """Resolve a connection from nodes. Subclasses must define this method to paginate nodes based on `first`/`last`/`before`/`after` arguments. Args: info: The strawberry execution info resolve the type name from. nodes: An iterable/iteretor of nodes to paginate. before: Returns the items in the list that come before the specified cursor. after: Returns the items in the list that come after the specified cursor. first: Returns the first n items from the list. last: Returns the items in the list that come after the specified cursor. max_results: The maximum number of results to resolve. kwargs: Additional arguments passed to the resolver. Returns: The resolved `Connection` """ raise NotImplementedError @strawberry_type(name="Connection", description="A connection to a list of items.") class ListConnection(Connection[NodeType]): """A connection to a list of items. Attributes: page_info: Pagination data for this connection edges: Contains the nodes in this connection """ page_info: PageInfo = field(description="Pagination data for this connection") edges: list[Edge[NodeType]] = field( description="Contains the nodes in this connection" ) @classmethod def resolve_connection( cls, nodes: NodeIterableType[NodeType], *, info: Info, before: str | None = None, after: str | None = None, first: int | None = None, last: int | None = None, max_results: int | None = None, **kwargs: Any, ) -> AwaitableOrValue[Self]: """Resolve a connection from the list of nodes. This uses the described Relay Pagination algorithm_ Args: info: The strawberry execution info resolve the type name from. nodes: An iterable/iteretor of nodes to paginate. before: Returns the items in the list that come before the specified cursor. after: Returns the items in the list that come after the specified cursor. first: Returns the first n items from the list. last: Returns the items in the list that come after the specified cursor. max_results: The maximum number of results to resolve. kwargs: Additional arguments passed to the resolver. Returns: The resolved `Connection` .. _Relay Pagination algorithm: https://relay.dev/graphql/connections.htm#sec-Pagination-algorithm """ type_def = get_object_definition(cls) assert type_def field_def = type_def.get_field("edges") assert field_def field = field_def.resolve_type(type_definition=type_def) while isinstance(field, StrawberryContainer): field = field.of_type edge_class = cast("Edge[NodeType]", field) slice_metadata = SliceMetadata.from_arguments( info, before=before, after=after, first=first, last=last, max_results=max_results, prefix=edge_class.CURSOR_PREFIX, ) if isinstance(nodes, (AsyncIterator, AsyncIterable)) and in_async_context(): async def resolver() -> Self: try: iterator = cast( "AsyncIterator[NodeType] | AsyncIterable[NodeType]", cast("Sequence", nodes)[ slice_metadata.start : slice_metadata.overfetch ], ) except TypeError: # TODO: Why mypy isn't narrowing this based on the if above? assert isinstance(nodes, (AsyncIterator, AsyncIterable)) iterator = aislice( nodes, slice_metadata.start, slice_metadata.overfetch, ) async with aclosing(iterator): # The slice above might return an object that now is not async # iterable anymore (e.g. an already cached django queryset) if isinstance(iterator, (AsyncIterator, AsyncIterable)): edges: list[Edge] = [ edge_class.resolve_edge( cls.resolve_node(v, info=info, **kwargs), cursor=slice_metadata.start + i, ) async for i, v in aenumerate(iterator) ] else: edges: list[Edge] = [ # type: ignore[no-redef] edge_class.resolve_edge( cls.resolve_node(v, info=info, **kwargs), cursor=slice_metadata.start + i, ) for i, v in enumerate(iterator) ] has_previous_page = slice_metadata.start > 0 if ( slice_metadata.expected is not None and len(edges) == slice_metadata.expected + 1 ): # Remove the overfetched result edges = edges[:-1] has_next_page = True elif slice_metadata.end == sys.maxsize: # Last was asked without any after/before assert last is not None original_len = len(edges) edges = edges[-last:] has_next_page = False has_previous_page = len(edges) != original_len else: has_next_page = False return cls( edges=edges, page_info=PageInfo( start_cursor=edges[0].cursor if edges else None, end_cursor=edges[-1].cursor if edges else None, has_previous_page=has_previous_page, has_next_page=has_next_page, ), ) return resolver() try: iterator = cast( "Iterator[NodeType] | Iterable[NodeType]", cast("Sequence", nodes)[ slice_metadata.start : slice_metadata.overfetch ], ) except TypeError: assert isinstance(nodes, (Iterable, Iterator)) iterator = itertools.islice( nodes, slice_metadata.start, slice_metadata.overfetch, ) if not should_resolve_list_connection_edges(info): return cls( edges=[], page_info=PageInfo( start_cursor=None, end_cursor=None, has_previous_page=False, has_next_page=False, ), ) edges = [ edge_class.resolve_edge( cls.resolve_node(v, info=info, **kwargs), cursor=slice_metadata.start + i, ) for i, v in enumerate(iterator) ] has_previous_page = slice_metadata.start > 0 if ( slice_metadata.expected is not None and len(edges) == slice_metadata.expected + 1 ): # Remove the overfetched result edges = edges[:-1] has_next_page = True elif slice_metadata.end == sys.maxsize: # Last was asked without any after/before assert last is not None original_len = len(edges) edges = edges[-last:] has_next_page = False has_previous_page = len(edges) != original_len else: has_next_page = False return cls( edges=edges, page_info=PageInfo( start_cursor=edges[0].cursor if edges else None, end_cursor=edges[-1].cursor if edges else None, has_previous_page=has_previous_page, has_next_page=has_next_page, ), ) __all__ = [ "PREFIX", "Connection", "Edge", "GlobalID", "GlobalIDValueError", "ListConnection", "Node", "NodeID", "NodeIDAnnotationError", "NodeIDPrivate", "NodeIterableType", "NodeType", "PageInfo", ] strawberry-graphql-0.287.0/strawberry/relay/utils.py000066400000000000000000000126441511033167500225730ustar00rootroot00000000000000from __future__ import annotations import base64 import dataclasses import sys from typing import TYPE_CHECKING, Any from typing_extensions import Self, assert_never from strawberry.types.base import StrawberryObjectDefinition from strawberry.types.nodes import InlineFragment if TYPE_CHECKING: from strawberry.types.info import Info def from_base64(value: str) -> tuple[str, str]: """Parse the base64 encoded relay value. Args: value: The value to be parsed Returns: A tuple of (TypeName, NodeID). Raises: ValueError: If the value is not in the expected format """ try: res = base64.b64decode(value.encode()).decode().split(":", 1) except Exception as e: raise ValueError(str(e)) from e if len(res) != 2: raise ValueError(f"{res} expected to contain only 2 items") return res[0], res[1] def to_base64(type_: str | type | StrawberryObjectDefinition, node_id: Any) -> str: """Encode the type name and node id to a base64 string. Args: type_: The GraphQL type, type definition or type name. node_id: The node id itself Returns: A GlobalID, which is a string resulting from base64 encoding :. Raises: ValueError: If the value is not a valid GraphQL type or name """ try: if isinstance(type_, str): type_name = type_ elif isinstance(type_, StrawberryObjectDefinition): type_name = type_.name elif isinstance(type_, type): type_name = type_.__strawberry_definition__.name # type:ignore else: # pragma: no cover assert_never(type_) except Exception as e: raise ValueError(f"{type_} is not a valid GraphQL type or name") from e return base64.b64encode(f"{type_name}:{node_id}".encode()).decode() def should_resolve_list_connection_edges(info: Info) -> bool: """Check if the user requested to resolve the `edges` field of a connection. Args: info: The strawberry execution info resolve the type name from Returns: True if the user requested to resolve the `edges` field of a connection, False otherwise. """ resolve_for_field_names = {"edges", "pageInfo"} # Recursively inspect the selection to check if the user requested to resolve the `edges` field. stack = [] for selection_field in info.selected_fields: stack.extend(selection_field.selections) while stack: selection = stack.pop() if ( not isinstance(selection, InlineFragment) and selection.name in resolve_for_field_names ): return True if nested_selections := getattr(selection, "selections", None): stack.extend(nested_selections) return False @dataclasses.dataclass class SliceMetadata: start: int end: int expected: int | None @property def overfetch(self) -> int: # Overfetch by 1 to check if we have a next result return self.end + 1 if self.end != sys.maxsize else self.end @classmethod def from_arguments( cls, info: Info, *, before: str | None = None, after: str | None = None, first: int | None = None, last: int | None = None, max_results: int | None = None, prefix: str | None = None, ) -> Self: """Get the slice metadata to use on ListConnection.""" from strawberry.relay.types import PREFIX if prefix is None: prefix = PREFIX max_results = ( max_results if max_results is not None else info.schema.config.relay_max_results ) start = 0 end: int | None = None if after: after_type, after_parsed = from_base64(after) if after_type != prefix: raise TypeError("Argument 'after' contains a non-existing value.") start = int(after_parsed) + 1 if before: before_type, before_parsed = from_base64(before) if before_type != prefix: raise TypeError("Argument 'before' contains a non-existing value.") end = int(before_parsed) if isinstance(first, int): if first < 0: raise ValueError("Argument 'first' must be a non-negative integer.") if first > max_results: raise ValueError( f"Argument 'first' cannot be higher than {max_results}." ) if end is not None: start = max(0, end - 1) end = start + first if isinstance(last, int): if last < 0: raise ValueError("Argument 'last' must be a non-negative integer.") if last > max_results: raise ValueError( f"Argument 'last' cannot be higher than {max_results}." ) if end is not None: start = max(start, end - last) else: end = sys.maxsize if end is None: end = start + max_results expected = end - start if end != sys.maxsize else None return cls( start=start, end=end, expected=expected, ) __all__ = [ "SliceMetadata", "from_base64", "should_resolve_list_connection_edges", "to_base64", ] strawberry-graphql-0.287.0/strawberry/resolvers.py000066400000000000000000000004371511033167500223400ustar00rootroot00000000000000from collections.abc import Callable from typing import Any def is_default_resolver(func: Callable[..., Any]) -> bool: """Check whether the function is a default resolver or a user provided one.""" return getattr(func, "_is_default", False) __all__ = ["is_default_resolver"] strawberry-graphql-0.287.0/strawberry/sanic/000077500000000000000000000000001511033167500210335ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/sanic/__init__.py000066400000000000000000000000001511033167500231320ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/sanic/context.py000066400000000000000000000015151511033167500230730ustar00rootroot00000000000000import warnings from typing_extensions import TypedDict from sanic.request import Request from strawberry.http.temporal_response import TemporalResponse class StrawberrySanicContext(TypedDict): request: Request response: TemporalResponse # see https://github.com/python/mypy/issues/13066 for the type ignore def __getattr__(self, key: str) -> object: # type: ignore # a warning will be raised because this is not supported anymore # but we need to keep it for backwards compatibility warnings.warn( "Accessing context attributes via the dot notation is deprecated, " "please use context.get('key') or context['key'] instead", DeprecationWarning, stacklevel=2, ) return super().__getitem__(key) __all__ = ["StrawberrySanicContext"] strawberry-graphql-0.287.0/strawberry/sanic/utils.py000066400000000000000000000017251511033167500225520ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any, cast if TYPE_CHECKING: from sanic.request import File, Request def convert_request_to_files_dict(request: Request) -> dict[str, Any]: """Converts the request.files dictionary to a dictionary of sanic Request objects. `request.files` has the following format, even if only a single file is uploaded: ```python { "textFile": [ sanic.request.File(type="text/plain", body=b"strawberry", name="textFile.txt") ] } ``` Note that the dictionary entries are lists. """ request_files = cast("dict[str, list[File]] | None", request.files) if not request_files: return {} files_dict: dict[str, File | list[File]] = {} for field_name, file_list in request_files.items(): assert len(file_list) == 1 files_dict[field_name] = file_list[0] return files_dict __all__ = ["convert_request_to_files_dict"] strawberry-graphql-0.287.0/strawberry/sanic/views.py000066400000000000000000000131231511033167500225420ustar00rootroot00000000000000from __future__ import annotations import json import warnings from typing import ( TYPE_CHECKING, Any, TypeGuard, ) from lia import HTTPException, SanicHTTPRequestAdapter from sanic.request import Request from sanic.response import HTTPResponse, html from sanic.views import HTTPMethodView from strawberry.http.async_base_view import AsyncBaseHTTPView from strawberry.http.temporal_response import TemporalResponse from strawberry.http.typevars import ( Context, RootValue, ) if TYPE_CHECKING: from collections.abc import AsyncGenerator, Callable from strawberry.http import GraphQLHTTPResponse from strawberry.http.ides import GraphQL_IDE from strawberry.schema import BaseSchema class GraphQLView( AsyncBaseHTTPView[ Request, HTTPResponse, TemporalResponse, Request, TemporalResponse, Context, RootValue, ], HTTPMethodView, ): """Class based view to handle GraphQL HTTP Requests. Args: schema: strawberry.Schema graphiql: bool, default is True allow_queries_via_get: bool, default is True Returns: None Example: app.add_route( GraphQLView.as_view(schema=schema, graphiql=True), "/graphql" ) """ allow_queries_via_get = True request_adapter_class = SanicHTTPRequestAdapter def __init__( self, schema: BaseSchema, graphiql: bool | None = None, graphql_ide: GraphQL_IDE | None = "graphiql", allow_queries_via_get: bool = True, json_encoder: type[json.JSONEncoder] | None = None, json_dumps_params: dict[str, Any] | None = None, multipart_uploads_enabled: bool = False, ) -> None: self.schema = schema self.allow_queries_via_get = allow_queries_via_get self.json_encoder = json_encoder self.json_dumps_params = json_dumps_params self.multipart_uploads_enabled = multipart_uploads_enabled if self.json_encoder is not None: # pragma: no cover warnings.warn( "json_encoder is deprecated, override encode_json instead", DeprecationWarning, stacklevel=2, ) if self.json_dumps_params is not None: # pragma: no cover warnings.warn( "json_dumps_params is deprecated, override encode_json instead", DeprecationWarning, stacklevel=2, ) self.json_encoder = json.JSONEncoder if graphiql is not None: warnings.warn( "The `graphiql` argument is deprecated in favor of `graphql_ide`", DeprecationWarning, stacklevel=2, ) self.graphql_ide = "graphiql" if graphiql else None else: self.graphql_ide = graphql_ide async def get_root_value(self, request: Request) -> RootValue | None: return None async def get_context( self, request: Request, response: TemporalResponse ) -> Context: return {"request": request, "response": response} # type: ignore async def render_graphql_ide(self, request: Request) -> HTTPResponse: return html(self.graphql_ide_html) async def get_sub_response(self, request: Request) -> TemporalResponse: return TemporalResponse() def create_response( self, response_data: GraphQLHTTPResponse | list[GraphQLHTTPResponse], sub_response: TemporalResponse, ) -> HTTPResponse: status_code = sub_response.status_code data = self.encode_json(response_data) return HTTPResponse( data, status=status_code, content_type="application/json", headers=sub_response.headers, ) async def post(self, request: Request) -> HTTPResponse: self.request = request try: return await self.run(request) except HTTPException as e: return HTTPResponse( e.reason, status=e.status_code, content_type="text/plain" ) async def get(self, request: Request) -> HTTPResponse: self.request = request try: return await self.run(request) except HTTPException as e: return HTTPResponse( e.reason, status=e.status_code, content_type="text/plain" ) async def create_streaming_response( self, request: Request, stream: Callable[[], AsyncGenerator[str, None]], sub_response: TemporalResponse, headers: dict[str, str], ) -> HTTPResponse: response = await self.request.respond( status=sub_response.status_code, headers={ **sub_response.headers, **headers, }, ) async for chunk in stream(): await response.send(chunk) await response.eof() # returning the response will basically tell sanic to send it again # to the client, so we return None to avoid that, and we ignore the type # error mostly so we don't have to update the types everywhere for this # corner case return None # type: ignore def is_websocket_request(self, request: Request) -> TypeGuard[Request]: return False async def pick_websocket_subprotocol(self, request: Request) -> str | None: raise NotImplementedError async def create_websocket_response( self, request: Request, subprotocol: str | None ) -> TemporalResponse: raise NotImplementedError __all__ = ["GraphQLView"] strawberry-graphql-0.287.0/strawberry/scalars.py000066400000000000000000000043311511033167500217410ustar00rootroot00000000000000from __future__ import annotations import base64 from typing import TYPE_CHECKING, Any, NewType from strawberry.types.scalar import scalar if TYPE_CHECKING: from collections.abc import Mapping from strawberry.types.scalar import ScalarDefinition, ScalarWrapper ID = NewType("ID", str) """Represent the GraphQL `ID` scalar type.""" JSON = scalar( NewType("JSON", object), # mypy doesn't like `NewType("name", Any)` description=( "The `JSON` scalar type represents JSON values as specified by " "[ECMA-404]" "(https://ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf)." ), specified_by_url=( "https://ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf" ), serialize=lambda v: v, parse_value=lambda v: v, ) Base16 = scalar( NewType("Base16", bytes), description="Represents binary data as Base16-encoded (hexadecimal) strings.", specified_by_url="https://datatracker.ietf.org/doc/html/rfc4648.html#section-8", serialize=lambda v: base64.b16encode(v).decode("utf-8"), parse_value=lambda v: base64.b16decode(v.encode("utf-8"), casefold=True), ) Base32 = scalar( NewType("Base32", bytes), description=( "Represents binary data as Base32-encoded strings, using the standard alphabet." ), specified_by_url=("https://datatracker.ietf.org/doc/html/rfc4648.html#section-6"), serialize=lambda v: base64.b32encode(v).decode("utf-8"), parse_value=lambda v: base64.b32decode(v.encode("utf-8"), casefold=True), ) Base64 = scalar( NewType("Base64", bytes), description=( "Represents binary data as Base64-encoded strings, using the standard alphabet." ), specified_by_url="https://datatracker.ietf.org/doc/html/rfc4648.html#section-4", serialize=lambda v: base64.b64encode(v).decode("utf-8"), parse_value=lambda v: base64.b64decode(v.encode("utf-8")), ) def is_scalar( annotation: Any, scalar_registry: Mapping[object, ScalarWrapper | ScalarDefinition], ) -> bool: if annotation in scalar_registry: return True return hasattr(annotation, "_scalar_definition") __all__ = ["ID", "JSON", "Base16", "Base32", "Base64", "is_scalar"] strawberry-graphql-0.287.0/strawberry/schema/000077500000000000000000000000001511033167500211765ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/schema/__init__.py000066400000000000000000000001341511033167500233050ustar00rootroot00000000000000from .base import BaseSchema from .schema import Schema __all__ = ["BaseSchema", "Schema"] strawberry-graphql-0.287.0/strawberry/schema/_graphql_core.py000066400000000000000000000032131511033167500243540ustar00rootroot00000000000000from typing import TypeAlias, Union from graphql.execution import ExecutionContext as GraphQLExecutionContext from graphql.execution import ExecutionResult as OriginalGraphQLExecutionResult from graphql.execution import execute, subscribe from strawberry.types import ExecutionResult try: from graphql import ( # type: ignore[attr-defined] ExperimentalIncrementalExecutionResults as GraphQLIncrementalExecutionResults, ) from graphql.execution import ( # type: ignore[attr-defined] InitialIncrementalExecutionResult, experimental_execute_incrementally, ) from graphql.type.directives import ( # type: ignore[attr-defined] GraphQLDeferDirective, GraphQLStreamDirective, ) incremental_execution_directives = ( GraphQLDeferDirective, GraphQLStreamDirective, ) GraphQLExecutionResult: TypeAlias = ( OriginalGraphQLExecutionResult | InitialIncrementalExecutionResult ) except ImportError: GraphQLIncrementalExecutionResults = type(None) GraphQLExecutionResult = OriginalGraphQLExecutionResult # type: ignore incremental_execution_directives = () # type: ignore experimental_execute_incrementally = None # TODO: give this a better name, maybe also a better place ResultType = Union[ # noqa: UP007 OriginalGraphQLExecutionResult, GraphQLIncrementalExecutionResults, ExecutionResult, ] __all__ = [ "GraphQLExecutionContext", "GraphQLExecutionResult", "GraphQLIncrementalExecutionResults", "ResultType", "execute", "experimental_execute_incrementally", "incremental_execution_directives", "subscribe", ] strawberry-graphql-0.287.0/strawberry/schema/base.py000066400000000000000000000075601511033167500224720ustar00rootroot00000000000000from __future__ import annotations from abc import abstractmethod from typing import TYPE_CHECKING, Any from typing_extensions import Protocol from strawberry.utils.logging import StrawberryLogger if TYPE_CHECKING: from collections.abc import Iterable from graphql import GraphQLError from strawberry.directive import StrawberryDirective from strawberry.schema.schema import SubscriptionResult from strawberry.schema.schema_converter import GraphQLCoreConverter from strawberry.types import ( ExecutionContext, ExecutionResult, ) from strawberry.types.base import ( StrawberryObjectDefinition, WithStrawberryObjectDefinition, ) from strawberry.types.enum import StrawberryEnumDefinition from strawberry.types.graphql import OperationType from strawberry.types.scalar import ScalarDefinition from strawberry.types.union import StrawberryUnion from .config import StrawberryConfig class BaseSchema(Protocol): config: StrawberryConfig schema_converter: GraphQLCoreConverter query: type[WithStrawberryObjectDefinition] mutation: type[WithStrawberryObjectDefinition] | None subscription: type[WithStrawberryObjectDefinition] | None schema_directives: list[object] @abstractmethod async def execute( self, query: str | None, variable_values: dict[str, Any] | None = None, context_value: Any | None = None, root_value: Any | None = None, operation_name: str | None = None, allowed_operation_types: Iterable[OperationType] | None = None, operation_extensions: dict[str, Any] | None = None, ) -> ExecutionResult: raise NotImplementedError @abstractmethod def execute_sync( self, query: str | None, variable_values: dict[str, Any] | None = None, context_value: Any | None = None, root_value: Any | None = None, operation_name: str | None = None, allowed_operation_types: Iterable[OperationType] | None = None, operation_extensions: dict[str, Any] | None = None, ) -> ExecutionResult: raise NotImplementedError @abstractmethod async def subscribe( self, query: str, variable_values: dict[str, Any] | None = None, context_value: Any | None = None, root_value: Any | None = None, operation_name: str | None = None, operation_extensions: dict[str, Any] | None = None, ) -> SubscriptionResult: raise NotImplementedError @abstractmethod def get_type_by_name( self, name: str ) -> ( StrawberryObjectDefinition | ScalarDefinition | StrawberryEnumDefinition | StrawberryUnion | None ): raise NotImplementedError @abstractmethod def get_directive_by_name(self, graphql_name: str) -> StrawberryDirective | None: raise NotImplementedError @abstractmethod def as_str(self) -> str: raise NotImplementedError @staticmethod def remove_field_suggestion(error: GraphQLError) -> None: if ( error.message.startswith("Cannot query field") and "Did you mean" in error.message ): error.message = error.message.split("Did you mean")[0].strip() def _process_errors( self, errors: list[GraphQLError], execution_context: ExecutionContext | None = None, ) -> None: if self.config.disable_field_suggestions: for error in errors: self.remove_field_suggestion(error) self.process_errors(errors, execution_context) def process_errors( self, errors: list[GraphQLError], execution_context: ExecutionContext | None = None, ) -> None: for error in errors: StrawberryLogger.error(error, execution_context) __all__ = ["BaseSchema"] strawberry-graphql-0.287.0/strawberry/schema/compat.py000066400000000000000000000032451511033167500230370ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from strawberry.scalars import is_scalar as is_strawberry_scalar from strawberry.types.base import ( StrawberryType, has_object_definition, ) # TypeGuard is only available in typing_extensions => 3.10, we don't want # to force updates to the typing_extensions package so we only use it when # TYPE_CHECKING is enabled. if TYPE_CHECKING: from collections.abc import Mapping from typing import TypeGuard from strawberry.types.scalar import ScalarDefinition, ScalarWrapper def is_input_type(type_: StrawberryType | type) -> TypeGuard[type]: if not has_object_definition(type_): return False return type_.__strawberry_definition__.is_input def is_interface_type(type_: StrawberryType | type) -> TypeGuard[type]: if not has_object_definition(type_): return False return type_.__strawberry_definition__.is_interface def is_scalar( type_: StrawberryType | type, scalar_registry: Mapping[object, ScalarWrapper | ScalarDefinition], ) -> TypeGuard[type]: return is_strawberry_scalar(type_, scalar_registry) def is_schema_directive(type_: StrawberryType | type) -> TypeGuard[type]: return hasattr(type_, "__strawberry_directive__") # TODO: do we still need this? def is_graphql_generic(type_: StrawberryType | type) -> bool: if has_object_definition(type_): return type_.__strawberry_definition__.is_graphql_generic if isinstance(type_, StrawberryType): return type_.is_graphql_generic return False __all__ = [ "is_graphql_generic", "is_input_type", "is_interface_type", "is_scalar", "is_schema_directive", ] strawberry-graphql-0.287.0/strawberry/schema/config.py000066400000000000000000000023651511033167500230230ustar00rootroot00000000000000from __future__ import annotations from dataclasses import InitVar, dataclass, field from typing import TYPE_CHECKING, Any, TypedDict from strawberry.types.info import Info from .name_converter import NameConverter if TYPE_CHECKING: from collections.abc import Callable class BatchingConfig(TypedDict): max_operations: int @dataclass class StrawberryConfig: auto_camel_case: InitVar[bool] = None # pyright: reportGeneralTypeIssues=false name_converter: NameConverter = field(default_factory=NameConverter) default_resolver: Callable[[Any, str], object] = getattr relay_max_results: int = 100 relay_use_legacy_global_id: bool = False disable_field_suggestions: bool = False info_class: type[Info] = Info enable_experimental_incremental_execution: bool = False _unsafe_disable_same_type_validation: bool = False batching_config: BatchingConfig | None = None def __post_init__( self, auto_camel_case: bool, ) -> None: if auto_camel_case is not None: self.name_converter.auto_camel_case = auto_camel_case if not issubclass(self.info_class, Info): raise TypeError("`info_class` must be a subclass of strawberry.Info") __all__ = ["StrawberryConfig"] strawberry-graphql-0.287.0/strawberry/schema/exceptions.py000066400000000000000000000021211511033167500237250ustar00rootroot00000000000000from strawberry.types.graphql import OperationType class CannotGetOperationTypeError(Exception): """Internal error raised when we cannot get the operation type from a GraphQL document.""" def __init__(self, operation_name: str | None) -> None: self.operation_name = operation_name def as_http_error_reason(self) -> str: return ( "Can't get GraphQL operation type" if self.operation_name is None else f'Unknown operation named "{self.operation_name}".' ) class InvalidOperationTypeError(Exception): def __init__(self, operation_type: OperationType) -> None: self.operation_type = operation_type def as_http_error_reason(self, method: str) -> str: operation_type = { OperationType.QUERY: "queries", OperationType.MUTATION: "mutations", OperationType.SUBSCRIPTION: "subscriptions", }[self.operation_type] return f"{operation_type} are not allowed when using {method}" __all__ = [ "CannotGetOperationTypeError", "InvalidOperationTypeError", ] strawberry-graphql-0.287.0/strawberry/schema/name_converter.py000066400000000000000000000154251511033167500245660ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, cast from typing_extensions import Protocol from strawberry.directive import StrawberryDirective from strawberry.schema_directive import StrawberrySchemaDirective from strawberry.types.base import ( StrawberryList, StrawberryObjectDefinition, StrawberryOptional, has_object_definition, ) from strawberry.types.enum import EnumValue, StrawberryEnumDefinition from strawberry.types.lazy_type import LazyType from strawberry.types.scalar import ScalarDefinition from strawberry.types.union import StrawberryUnion from strawberry.utils.str_converters import capitalize_first, to_camel_case from strawberry.utils.typing import eval_type if TYPE_CHECKING: from strawberry.types.arguments import StrawberryArgument from strawberry.types.base import StrawberryType from strawberry.types.field import StrawberryField class HasGraphQLName(Protocol): python_name: str graphql_name: str | None class NameConverter: def __init__(self, auto_camel_case: bool = True) -> None: self.auto_camel_case = auto_camel_case def apply_naming_config(self, name: str) -> str: if self.auto_camel_case: name = to_camel_case(name) return name def from_type( self, type_: StrawberryType | StrawberryDirective, ) -> str: if isinstance(type_, (StrawberryDirective, StrawberrySchemaDirective)): return self.from_directive(type_) if isinstance(type_, StrawberryEnumDefinition): return self.from_enum(type_) if isinstance(type_, StrawberryObjectDefinition): if type_.is_input: return self.from_input_object(type_) if type_.is_interface: return self.from_interface(type_) return self.from_object(type_) if isinstance(type_, StrawberryUnion): return self.from_union(type_) if isinstance(type_, ScalarDefinition): # TODO: Replace with StrawberryScalar return self.from_scalar(type_) return str(type_) def from_argument(self, argument: StrawberryArgument) -> str: return self.get_graphql_name(argument) def from_object(self, object_type: StrawberryObjectDefinition) -> str: # if concrete_of is not generic, then this is a subclass of an already # specialized type. if object_type.concrete_of and object_type.concrete_of.is_graphql_generic: return self.from_generic( object_type, list(object_type.type_var_map.values()) ) return object_type.name def from_input_object(self, input_type: StrawberryObjectDefinition) -> str: return self.from_object(input_type) def from_interface(self, interface: StrawberryObjectDefinition) -> str: return self.from_object(interface) def from_enum(self, enum: StrawberryEnumDefinition) -> str: return enum.name def from_enum_value( self, enum: StrawberryEnumDefinition, enum_value: EnumValue ) -> str: return enum_value.name def from_directive( self, directive: StrawberryDirective | StrawberrySchemaDirective ) -> str: name = self.get_graphql_name(directive) if self.auto_camel_case: # we don't want the first letter to be uppercase for directives return name[0].lower() + name[1:] return name def from_scalar(self, scalar: ScalarDefinition) -> str: return scalar.name def from_field(self, field: StrawberryField) -> str: return self.get_graphql_name(field) def from_union(self, union: StrawberryUnion) -> str: if union.graphql_name is not None: return union.graphql_name name = "" types: tuple[StrawberryType, ...] = union.types if union.concrete_of and union.concrete_of.graphql_name: concrete_of_types = set(union.concrete_of.types) types = tuple(type_ for type_ in types if type_ not in concrete_of_types) for type_ in types: if isinstance(type_, LazyType): type_ = cast("StrawberryType", type_.resolve_type()) # noqa: PLW2901 if has_object_definition(type_): type_name = self.from_type(type_.__strawberry_definition__) else: # This should only be hit when generating names for type-related # exceptions type_name = self.from_type(type_) name += type_name if union.concrete_of and union.concrete_of.graphql_name: name += union.concrete_of.graphql_name return name def from_generic( self, generic_type: StrawberryObjectDefinition, types: list[StrawberryType | type], ) -> str: generic_type_name = generic_type.name names: list[str] = [] for type_ in types: name = self.get_name_from_type(type_) names.append(name) return "".join(names) + generic_type_name def get_name_from_type(self, type_: StrawberryType | type) -> str: type_ = eval_type(type_) if isinstance(type_, LazyType): type_ = type_.resolve_type() if isinstance(type_, StrawberryEnumDefinition): name = type_.name elif isinstance(type_, StrawberryUnion): name = type_.graphql_name if type_.graphql_name else self.from_union(type_) elif isinstance(type_, StrawberryList): name = self.get_name_from_type(type_.of_type) + "List" elif isinstance(type_, StrawberryOptional): name = self.get_name_from_type(type_.of_type) + "Optional" elif hasattr(type_, "_scalar_definition"): strawberry_type = type_._scalar_definition name = strawberry_type.name elif has_object_definition(type_): strawberry_type = type_.__strawberry_definition__ if ( strawberry_type.is_graphql_generic and not strawberry_type.is_specialized_generic ): types = type_.__args__ # type: ignore name = self.from_generic(strawberry_type, types) elif ( strawberry_type.concrete_of and not strawberry_type.is_specialized_generic ): types = list(strawberry_type.type_var_map.values()) name = self.from_generic(strawberry_type, types) else: name = strawberry_type.name else: name = type_.__name__ return capitalize_first(name) def get_graphql_name(self, obj: HasGraphQLName) -> str: if obj.graphql_name is not None: return obj.graphql_name assert obj.python_name return self.apply_naming_config(obj.python_name) __all__ = ["NameConverter"] strawberry-graphql-0.287.0/strawberry/schema/schema.py000066400000000000000000001147111511033167500230150ustar00rootroot00000000000000from __future__ import annotations import warnings from asyncio import ensure_future from collections.abc import AsyncGenerator, AsyncIterator, Awaitable, Callable, Iterable from functools import cached_property, lru_cache from inspect import isawaitable from typing import ( TYPE_CHECKING, Any, NamedTuple, cast, ) from graphql import ExecutionResult as GraphQLExecutionResult from graphql import ( ExecutionResult as OriginalExecutionResult, ) from graphql import ( FieldNode, FragmentDefinitionNode, GraphQLBoolean, GraphQLError, GraphQLField, GraphQLNamedType, GraphQLNonNull, GraphQLObjectType, GraphQLOutputType, GraphQLSchema, OperationDefinitionNode, get_introspection_query, parse, validate_schema, ) from graphql.execution.middleware import MiddlewareManager from graphql.type.directives import specified_directives from graphql.validation import validate from strawberry import relay from strawberry.annotation import StrawberryAnnotation from strawberry.exceptions import MissingQueryError from strawberry.extensions import SchemaExtension from strawberry.extensions.directives import ( DirectivesExtension, DirectivesExtensionSync, ) from strawberry.extensions.runner import SchemaExtensionsRunner from strawberry.printer import print_schema from strawberry.schema.schema_converter import GraphQLCoreConverter from strawberry.schema.validation_rules.maybe_null import MaybeNullValidationRule from strawberry.schema.validation_rules.one_of import OneOfInputValidationRule from strawberry.types.base import ( StrawberryObjectDefinition, WithStrawberryObjectDefinition, has_object_definition, ) from strawberry.types.execution import ( ExecutionContext, ExecutionResult, PreExecutionError, ) from strawberry.types.graphql import OperationType from strawberry.utils import IS_GQL_32, IS_GQL_33 from strawberry.utils.aio import aclosing from strawberry.utils.await_maybe import await_maybe from . import compat from ._graphql_core import ( GraphQLExecutionContext, GraphQLIncrementalExecutionResults, ResultType, execute, experimental_execute_incrementally, incremental_execution_directives, subscribe, ) from .base import BaseSchema from .config import StrawberryConfig from .exceptions import CannotGetOperationTypeError, InvalidOperationTypeError if TYPE_CHECKING: from collections.abc import Iterable, Mapping from typing import TypeAlias from graphql.language import DocumentNode from graphql.pyutils import Path from graphql.type import GraphQLResolveInfo from graphql.validation import ASTValidationRule from strawberry.directive import StrawberryDirective from strawberry.types.base import StrawberryType from strawberry.types.enum import StrawberryEnumDefinition from strawberry.types.field import StrawberryField from strawberry.types.scalar import ScalarDefinition, ScalarWrapper from strawberry.types.union import StrawberryUnion SubscriptionResult: TypeAlias = AsyncGenerator[ PreExecutionError | ExecutionResult, None ] OriginSubscriptionResult: TypeAlias = ( OriginalExecutionResult | AsyncIterator[OriginalExecutionResult] ) DEFAULT_ALLOWED_OPERATION_TYPES = { OperationType.QUERY, OperationType.MUTATION, OperationType.SUBSCRIPTION, } ProcessErrors: TypeAlias = ( "Callable[[list[GraphQLError], ExecutionContext | None], None]" ) # TODO: merge with below def validate_document( schema: GraphQLSchema, document: DocumentNode, validation_rules: tuple[type[ASTValidationRule], ...], ) -> list[GraphQLError]: validation_rules = ( *validation_rules, MaybeNullValidationRule, OneOfInputValidationRule, ) return validate( schema, document, validation_rules, ) def _run_validation(execution_context: ExecutionContext) -> None: # Check if there are any validation rules or if validation has # already been run by an extension if ( len(execution_context.validation_rules) > 0 and execution_context.pre_execution_errors is None ): assert execution_context.graphql_document execution_context.pre_execution_errors = validate_document( execution_context.schema._schema, execution_context.graphql_document, execution_context.validation_rules, ) def _coerce_error(error: GraphQLError | Exception) -> GraphQLError: if isinstance(error, GraphQLError): return error return GraphQLError(str(error), original_error=error) class _OperationContextAwareGraphQLResolveInfo(NamedTuple): # pyright: ignore field_name: str field_nodes: list[FieldNode] return_type: GraphQLOutputType parent_type: GraphQLObjectType path: Path schema: GraphQLSchema fragments: dict[str, FragmentDefinitionNode] root_value: Any operation: OperationDefinitionNode variable_values: dict[str, Any] context: Any is_awaitable: Callable[[Any], bool] operation_extensions: dict[str, Any] class StrawberryGraphQLCoreExecutionContext(GraphQLExecutionContext): def __init__(self, *args: Any, **kwargs: Any) -> None: operation_extensions = kwargs.pop("operation_extensions", None) super().__init__(*args, **kwargs) self.operation_extensions = operation_extensions if IS_GQL_33: def build_resolve_info( self, field_def: GraphQLField, field_nodes: list[FieldNode], parent_type: GraphQLObjectType, path: Path, ) -> GraphQLResolveInfo: return _OperationContextAwareGraphQLResolveInfo( # type: ignore field_nodes[0].name.value, field_nodes, field_def.type, parent_type, path, self.schema, self.fragments, self.root_value, self.operation, self.variable_values, self.context_value, self.is_awaitable, self.operation_extensions, ) class Schema(BaseSchema): def __init__( self, # TODO: can we make sure we only allow to pass # something that has been decorated? query: type, mutation: type | None = None, subscription: type | None = None, directives: Iterable[StrawberryDirective] = (), types: Iterable[type | StrawberryType] = (), extensions: Iterable[type[SchemaExtension] | SchemaExtension] = (), execution_context_class: type[GraphQLExecutionContext] | None = None, config: StrawberryConfig | None = None, scalar_overrides: ( Mapping[object, type | ScalarWrapper | ScalarDefinition] | None ) = None, schema_directives: Iterable[object] = (), ) -> None: """Default Schema to be used in a Strawberry application. A GraphQL Schema class used to define the structure and configuration of GraphQL queries, mutations, and subscriptions. This class allows the creation of a GraphQL schema by specifying the types for queries, mutations, and subscriptions, along with various configuration options such as directives, extensions, and scalar overrides. Args: query: The entry point for queries. mutation: The entry point for mutations. subscription: The entry point for subscriptions. directives: A list of operation directives that clients can use. The bult-in `@include` and `@skip` are included by default. types: A list of additional types that will be included in the schema. extensions: A list of Strawberry extensions. execution_context_class: The execution context class. config: The configuration for the schema. scalar_overrides: A dictionary of overrides for scalars. schema_directives: A list of schema directives for the schema. Example: ```python import strawberry @strawberry.type class Query: name: str = "Patrick" schema = strawberry.Schema(query=Query) ``` """ self.query = query self.mutation = mutation self.subscription = subscription self.extensions = extensions self._cached_middleware_manager: MiddlewareManager | None = None self.execution_context_class = ( execution_context_class or StrawberryGraphQLCoreExecutionContext ) self.config = config or StrawberryConfig() self.schema_converter = GraphQLCoreConverter( self.config, scalar_overrides=scalar_overrides or {}, # type: ignore get_fields=self.get_fields, ) self.directives = directives self.schema_directives = list(schema_directives) query_type = self.schema_converter.from_object( cast( "type[WithStrawberryObjectDefinition]", query ).__strawberry_definition__ ) mutation_type = ( self.schema_converter.from_object( cast( "type[WithStrawberryObjectDefinition]", mutation ).__strawberry_definition__ ) if mutation else None ) subscription_type = ( self.schema_converter.from_object( cast( "type[WithStrawberryObjectDefinition]", subscription ).__strawberry_definition__ ) if subscription else None ) graphql_directives = [ self.schema_converter.from_directive(directive) for directive in directives ] graphql_types = [] for type_ in types: if compat.is_schema_directive(type_): graphql_directives.append( self.schema_converter.from_schema_directive(type_) ) else: if ( has_object_definition(type_) and type_.__strawberry_definition__.is_graphql_generic ): type_ = StrawberryAnnotation(type_).resolve() # noqa: PLW2901 graphql_type = self.schema_converter.from_maybe_optional(type_) if isinstance(graphql_type, GraphQLNonNull): graphql_type = graphql_type.of_type if not isinstance(graphql_type, GraphQLNamedType): raise TypeError(f"{graphql_type} is not a named GraphQL Type") graphql_types.append(graphql_type) try: directives = specified_directives + tuple(graphql_directives) # type: ignore if self.config.enable_experimental_incremental_execution: directives = tuple(directives) + tuple(incremental_execution_directives) self._schema = GraphQLSchema( query=query_type, mutation=mutation_type, subscription=subscription_type if subscription else None, directives=directives, # type: ignore types=graphql_types, extensions={ GraphQLCoreConverter.DEFINITION_BACKREF: self, }, ) except TypeError as error: # GraphQL core throws a TypeError if there's any exception raised # during the schema creation, so we check if the cause was a # StrawberryError and raise it instead if that's the case. from strawberry.exceptions import StrawberryException if isinstance(error.__cause__, StrawberryException): raise error.__cause__ from None raise # attach our schema to the GraphQL schema instance self._schema._strawberry_schema = self # type: ignore self._warn_for_federation_directives() self._resolve_node_ids() self._extend_introspection() # Validate schema early because we want developers to know about # possible issues as soon as possible errors = validate_schema(self._schema) if errors: formatted_errors = "\n\n".join(f"❌ {error.message}" for error in errors) raise ValueError(f"Invalid Schema. Errors:\n\n{formatted_errors}") def get_extensions(self, sync: bool = False) -> list[SchemaExtension]: extensions: list[type[SchemaExtension] | SchemaExtension] = [] extensions.extend(self.extensions) if self.directives: extensions.extend( [DirectivesExtensionSync if sync else DirectivesExtension] ) return [ ext if isinstance(ext, SchemaExtension) else ext(execution_context=None) for ext in extensions ] @cached_property def _sync_extensions(self) -> list[SchemaExtension]: return self.get_extensions(sync=True) @cached_property def _async_extensions(self) -> list[SchemaExtension]: return self.get_extensions(sync=False) def create_extensions_runner( self, execution_context: ExecutionContext, extensions: list[SchemaExtension] ) -> SchemaExtensionsRunner: return SchemaExtensionsRunner( execution_context=execution_context, extensions=extensions, ) def _get_custom_context_kwargs( self, operation_extensions: dict[str, Any] | None = None ) -> dict[str, Any]: if not IS_GQL_33: return {} return {"operation_extensions": operation_extensions} def _get_middleware_manager( self, extensions: list[SchemaExtension] ) -> MiddlewareManager: # create a middleware manager with all the extensions that implement resolve if not self._cached_middleware_manager: self._cached_middleware_manager = MiddlewareManager( *(ext for ext in extensions if ext._implements_resolve()) ) return self._cached_middleware_manager def _create_execution_context( self, query: str | None, allowed_operation_types: Iterable[OperationType], variable_values: dict[str, Any] | None = None, context_value: Any | None = None, root_value: Any | None = None, operation_name: str | None = None, operation_extensions: dict[str, Any] | None = None, ) -> ExecutionContext: return ExecutionContext( query=query, schema=self, allowed_operations=allowed_operation_types, context=context_value, root_value=root_value, variables=variable_values, provided_operation_name=operation_name, operation_extensions=operation_extensions, ) @lru_cache def get_type_by_name( self, name: str ) -> ( StrawberryObjectDefinition | ScalarDefinition | StrawberryEnumDefinition | StrawberryUnion | None ): # TODO: respect auto_camel_case if name in self.schema_converter.type_map: return self.schema_converter.type_map[name].definition return None def get_field_for_type( self, field_name: str, type_name: str ) -> StrawberryField | None: type_ = self.get_type_by_name(type_name) if not type_: return None # pragma: no cover assert isinstance(type_, StrawberryObjectDefinition) return next( ( field for field in type_.fields if self.config.name_converter.get_graphql_name(field) == field_name ), None, ) @lru_cache def get_directive_by_name(self, graphql_name: str) -> StrawberryDirective | None: return next( ( directive for directive in self.directives if self.config.name_converter.from_directive(directive) == graphql_name ), None, ) def get_fields( self, type_definition: StrawberryObjectDefinition ) -> list[StrawberryField]: return type_definition.fields async def _parse_and_validate_async( self, context: ExecutionContext, extensions_runner: SchemaExtensionsRunner ) -> PreExecutionError | None: if not context.query: raise MissingQueryError async with extensions_runner.parsing(): try: if not context.graphql_document: context.graphql_document = parse(context.query) except GraphQLError as error: context.pre_execution_errors = [error] return PreExecutionError(data=None, errors=[error]) except Exception as error: # noqa: BLE001 error = GraphQLError(str(error), original_error=error) context.pre_execution_errors = [error] return PreExecutionError(data=None, errors=[error]) try: operation_type = context.operation_type except RuntimeError as error: raise CannotGetOperationTypeError(context.operation_name) from error if operation_type not in context.allowed_operations: raise InvalidOperationTypeError(operation_type) async with extensions_runner.validation(): _run_validation(context) if context.pre_execution_errors: return PreExecutionError( data=None, errors=context.pre_execution_errors, ) return None async def _handle_execution_result( self, context: ExecutionContext, result: ResultType, extensions_runner: SchemaExtensionsRunner, *, # TODO: can we remove this somehow, see comment in execute skip_process_errors: bool = False, ) -> ExecutionResult: # TODO: handle this, also, why do we have both GraphQLExecutionResult and ExecutionResult? if isinstance(result, GraphQLIncrementalExecutionResults): return result # Set errors on the context so that it's easier # to access in extensions if result.errors: context.pre_execution_errors = result.errors if not skip_process_errors: self._process_errors(result.errors, context) if isinstance(result, GraphQLExecutionResult): result = ExecutionResult(data=result.data, errors=result.errors) result.extensions = await extensions_runner.get_extensions_results(context) context.result = result return result async def execute( self, query: str | None, variable_values: dict[str, Any] | None = None, context_value: Any | None = None, root_value: Any | None = None, operation_name: str | None = None, allowed_operation_types: Iterable[OperationType] | None = None, operation_extensions: dict[str, Any] | None = None, ) -> ExecutionResult: if allowed_operation_types is None: allowed_operation_types = DEFAULT_ALLOWED_OPERATION_TYPES execution_context = self._create_execution_context( query=query, allowed_operation_types=allowed_operation_types, variable_values=variable_values, context_value=context_value, root_value=root_value, operation_name=operation_name, operation_extensions=operation_extensions, ) extensions = self.get_extensions() # TODO (#3571): remove this when we implement execution context as parameter. for extension in extensions: extension.execution_context = execution_context extensions_runner = self.create_extensions_runner(execution_context, extensions) middleware_manager = self._get_middleware_manager(extensions) execute_function = execute if self.config.enable_experimental_incremental_execution: execute_function = experimental_execute_incrementally if execute_function is None: raise RuntimeError( "Incremental execution is enabled but experimental_execute_incrementally is not available, " "please install graphql-core>=3.3.0" ) custom_context_kwargs = self._get_custom_context_kwargs(operation_extensions) try: async with extensions_runner.operation(): # Note: In graphql-core the schema would be validated here but in # Strawberry we are validating it at initialisation time instead if errors := await self._parse_and_validate_async( execution_context, extensions_runner ): return await self._handle_execution_result( execution_context, errors, extensions_runner, ) assert execution_context.graphql_document async with extensions_runner.executing(): if not execution_context.result: result = await await_maybe( execute_function( self._schema, execution_context.graphql_document, root_value=execution_context.root_value, middleware=middleware_manager, variable_values=execution_context.variables, operation_name=execution_context.operation_name, context_value=execution_context.context, execution_context_class=self.execution_context_class, **custom_context_kwargs, ) ) execution_context.result = result else: result = execution_context.result # Also set errors on the execution_context so that it's easier # to access in extensions # TODO: maybe here use the first result from incremental execution if it exists if isinstance(result, GraphQLExecutionResult) and result.errors: execution_context.pre_execution_errors = result.errors # Run the `Schema.process_errors` function here before # extensions have a chance to modify them (see the MaskErrors # extension). That way we can log the original errors but # only return a sanitised version to the client. self._process_errors(result.errors, execution_context) except ( MissingQueryError, CannotGetOperationTypeError, InvalidOperationTypeError, ): raise except Exception as exc: # noqa: BLE001 return await self._handle_execution_result( execution_context, PreExecutionError(data=None, errors=[_coerce_error(exc)]), extensions_runner, ) # return results after all the operation completed. return await self._handle_execution_result( execution_context, result, extensions_runner, skip_process_errors=True ) def execute_sync( self, query: str | None, variable_values: dict[str, Any] | None = None, context_value: Any | None = None, root_value: Any | None = None, operation_name: str | None = None, allowed_operation_types: Iterable[OperationType] | None = None, operation_extensions: dict[str, Any] | None = None, ) -> ExecutionResult: if allowed_operation_types is None: allowed_operation_types = DEFAULT_ALLOWED_OPERATION_TYPES execution_context = self._create_execution_context( query=query, allowed_operation_types=allowed_operation_types, variable_values=variable_values, context_value=context_value, root_value=root_value, operation_name=operation_name, operation_extensions=operation_extensions, ) extensions = self._sync_extensions # TODO (#3571): remove this when we implement execution context as parameter. for extension in extensions: extension.execution_context = execution_context extensions_runner = self.create_extensions_runner(execution_context, extensions) middleware_manager = self._get_middleware_manager(extensions) execute_function = execute if self.config.enable_experimental_incremental_execution: execute_function = experimental_execute_incrementally if execute_function is None: raise RuntimeError( "Incremental execution is enabled but experimental_execute_incrementally is not available, " "please install graphql-core>=3.3.0" ) custom_context_kwargs = self._get_custom_context_kwargs(operation_extensions) try: with extensions_runner.operation(): # Note: In graphql-core the schema would be validated here but in # Strawberry we are validating it at initialisation time instead if not execution_context.query: raise MissingQueryError # noqa: TRY301 with extensions_runner.parsing(): try: if not execution_context.graphql_document: execution_context.graphql_document = parse( execution_context.query, **execution_context.parse_options, ) except GraphQLError as error: execution_context.pre_execution_errors = [error] self._process_errors([error], execution_context) return ExecutionResult( data=None, errors=[error], extensions=extensions_runner.get_extensions_results_sync(), ) try: operation_type = execution_context.operation_type except RuntimeError as error: raise CannotGetOperationTypeError( execution_context.operation_name ) from error if operation_type not in execution_context.allowed_operations: raise InvalidOperationTypeError(operation_type) # noqa: TRY301 with extensions_runner.validation(): _run_validation(execution_context) if execution_context.pre_execution_errors: self._process_errors( execution_context.pre_execution_errors, execution_context ) return ExecutionResult( data=None, errors=execution_context.pre_execution_errors, extensions=extensions_runner.get_extensions_results_sync(), ) with extensions_runner.executing(): if not execution_context.result: result = execute_function( self._schema, execution_context.graphql_document, root_value=execution_context.root_value, middleware=middleware_manager, variable_values=execution_context.variables, operation_name=execution_context.operation_name, context_value=execution_context.context, execution_context_class=self.execution_context_class, **custom_context_kwargs, ) if isawaitable(result): result = cast("Awaitable[GraphQLExecutionResult]", result) # type: ignore[redundant-cast] ensure_future(result).cancel() raise RuntimeError( # noqa: TRY301 "GraphQL execution failed to complete synchronously." ) result = cast("GraphQLExecutionResult", result) # type: ignore[redundant-cast] execution_context.result = result # Also set errors on the context so that it's easier # to access in extensions if result.errors: execution_context.pre_execution_errors = result.errors # Run the `Schema.process_errors` function here before # extensions have a chance to modify them (see the MaskErrors # extension). That way we can log the original errors but # only return a sanitised version to the client. self._process_errors(result.errors, execution_context) except ( MissingQueryError, CannotGetOperationTypeError, InvalidOperationTypeError, ): raise except Exception as exc: # noqa: BLE001 errors = [_coerce_error(exc)] execution_context.pre_execution_errors = errors self._process_errors(errors, execution_context) return ExecutionResult( data=None, errors=errors, extensions=extensions_runner.get_extensions_results_sync(), ) return ExecutionResult( data=execution_context.result.data, errors=execution_context.result.errors, extensions=extensions_runner.get_extensions_results_sync(), ) async def _subscribe( self, execution_context: ExecutionContext, extensions_runner: SchemaExtensionsRunner, middleware_manager: MiddlewareManager, execution_context_class: type[GraphQLExecutionContext] | None = None, operation_extensions: dict[str, Any] | None = None, ) -> AsyncGenerator[ExecutionResult, None]: async with extensions_runner.operation(): if initial_error := await self._parse_and_validate_async( context=execution_context, extensions_runner=extensions_runner, ): initial_error.extensions = ( await extensions_runner.get_extensions_results(execution_context) ) yield await self._handle_execution_result( execution_context, initial_error, extensions_runner ) try: async with extensions_runner.executing(): assert execution_context.graphql_document is not None gql_33_kwargs = { "middleware": middleware_manager, "execution_context_class": execution_context_class, "operation_extensions": operation_extensions, } try: # Might not be awaitable for pre-execution errors. aiter_or_result: OriginSubscriptionResult = await await_maybe( subscribe( self._schema, execution_context.graphql_document, root_value=execution_context.root_value, variable_values=execution_context.variables, operation_name=execution_context.operation_name, context_value=execution_context.context, **{} if IS_GQL_32 else gql_33_kwargs, # type: ignore[arg-type] ) ) # graphql-core 3.2 doesn't handle some of the pre-execution errors. # see `test_subscription_immediate_error` except Exception as exc: # noqa: BLE001 aiter_or_result = OriginalExecutionResult( data=None, errors=[_coerce_error(exc)] ) # Handle pre-execution errors. if isinstance(aiter_or_result, OriginalExecutionResult): yield await self._handle_execution_result( execution_context, PreExecutionError(data=None, errors=aiter_or_result.errors), extensions_runner, ) else: try: async with aclosing(aiter_or_result): async for result in aiter_or_result: yield await self._handle_execution_result( execution_context, result, extensions_runner, ) # graphql-core doesn't handle exceptions raised while executing. except Exception as exc: # noqa: BLE001 yield await self._handle_execution_result( execution_context, OriginalExecutionResult( data=None, errors=[_coerce_error(exc)] ), extensions_runner, ) # catch exceptions raised in `on_execute` hook. except Exception as exc: # noqa: BLE001 origin_result = OriginalExecutionResult( data=None, errors=[_coerce_error(exc)] ) yield await self._handle_execution_result( execution_context, origin_result, extensions_runner, ) async def subscribe( self, query: str | None, variable_values: dict[str, Any] | None = None, context_value: Any | None = None, root_value: Any | None = None, operation_name: str | None = None, operation_extensions: dict[str, Any] | None = None, ) -> SubscriptionResult: execution_context = self._create_execution_context( query=query, allowed_operation_types=(OperationType.SUBSCRIPTION,), variable_values=variable_values, context_value=context_value, root_value=root_value, operation_name=operation_name, ) extensions = self._async_extensions # TODO (#3571): remove this when we implement execution context as parameter. for extension in extensions: extension.execution_context = execution_context return self._subscribe( execution_context, extensions_runner=self.create_extensions_runner( execution_context, extensions ), middleware_manager=self._get_middleware_manager(extensions), execution_context_class=self.execution_context_class, operation_extensions=operation_extensions, ) def _resolve_node_ids(self) -> None: for concrete_type in self.schema_converter.type_map.values(): type_def = concrete_type.definition # This can be a TypeDefinition, StrawberryEnumDefinition, ScalarDefinition # or UnionDefinition if not isinstance(type_def, StrawberryObjectDefinition): continue # Do not validate id_attr for interfaces. relay.Node itself and # any other interfdace that implements it are not required to # provide a NodeID annotation, only the concrete type implementing # them needs to do that. if type_def.is_interface: continue # Call resolve_id_attr in here to make sure we raise provide # early feedback for missing NodeID annotations origin = type_def.origin if issubclass(origin, relay.Node): has_custom_resolve_id = False for base in origin.__mro__: if base is relay.Node: break if "resolve_id" in base.__dict__: has_custom_resolve_id = True break if not has_custom_resolve_id: origin.resolve_id_attr() def _warn_for_federation_directives(self) -> None: """Raises a warning if the schema has any federation directives.""" from strawberry.federation.schema_directives import FederationDirective all_types = self.schema_converter.type_map.values() all_type_defs = (type_.definition for type_ in all_types) all_directives = ( directive for type_def in all_type_defs for directive in (type_def.directives or []) ) if any( isinstance(directive, FederationDirective) for directive in all_directives ): warnings.warn( "Federation directive found in schema. " "Use `strawberry.federation.Schema` instead of `strawberry.Schema`.", UserWarning, stacklevel=3, ) def _extend_introspection(self) -> None: def _resolve_is_one_of(obj: Any, info: Any) -> bool: if "strawberry-definition" not in obj.extensions: return False return obj.extensions["strawberry-definition"].is_one_of instrospection_type = self._schema.type_map["__Type"] instrospection_type.fields["isOneOf"] = GraphQLField(GraphQLBoolean) # type: ignore[attr-defined] instrospection_type.fields["isOneOf"].resolve = _resolve_is_one_of # type: ignore[attr-defined] def as_str(self) -> str: return print_schema(self) __str__ = as_str def introspect(self) -> dict[str, Any]: """Return the introspection query result for the current schema. Raises: ValueError: If the introspection query fails due to an invalid schema """ introspection = self.execute_sync(get_introspection_query()) if introspection.errors or not introspection.data: raise ValueError(f"Invalid Schema. Errors {introspection.errors!r}") return introspection.data __all__ = ["Schema"] strawberry-graphql-0.287.0/strawberry/schema/schema_converter.py000066400000000000000000001174251511033167500251110ustar00rootroot00000000000000from __future__ import annotations import dataclasses import sys import typing from functools import partial, reduce from typing import ( TYPE_CHECKING, Annotated, Any, Generic, TypeVar, cast, ) from typing_extensions import Protocol from graphql import ( GraphQLAbstractType, GraphQLArgument, GraphQLDirective, GraphQLEnumType, GraphQLEnumValue, GraphQLError, GraphQLField, GraphQLID, GraphQLInputField, GraphQLInputObjectType, GraphQLInterfaceType, GraphQLList, GraphQLNamedType, GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, GraphQLType, GraphQLUnionType, Undefined, ValueNode, default_type_resolver, ) from graphql.language.directive_locations import DirectiveLocation from strawberry.annotation import StrawberryAnnotation from strawberry.exceptions import ( DuplicatedTypeName, InvalidTypeInputForUnion, InvalidUnionTypeError, MissingTypesForGenericError, ScalarAlreadyRegisteredError, UnresolvedFieldTypeError, ) from strawberry.extensions.field_extension import build_field_extension_resolvers from strawberry.relay.types import GlobalID from strawberry.schema.types.scalar import ( DEFAULT_SCALAR_REGISTRY, _get_scalar_definition, _make_scalar_type, ) from strawberry.types.arguments import StrawberryArgument, convert_arguments from strawberry.types.base import ( StrawberryList, StrawberryMaybe, StrawberryObjectDefinition, StrawberryOptional, StrawberryType, get_object_definition, has_object_definition, ) from strawberry.types.cast import get_strawberry_type_cast from strawberry.types.enum import StrawberryEnumDefinition, has_enum_definition from strawberry.types.field import UNRESOLVED from strawberry.types.lazy_type import LazyType from strawberry.types.private import is_private from strawberry.types.scalar import ScalarWrapper, scalar from strawberry.types.union import StrawberryUnion from strawberry.types.unset import UNSET from strawberry.utils.await_maybe import await_maybe from . import compat from .types.concrete_type import ConcreteType if TYPE_CHECKING: from collections.abc import Awaitable, Callable, Mapping from graphql import ( GraphQLInputType, GraphQLNullableType, GraphQLOutputType, GraphQLResolveInfo, ) from strawberry.directive import StrawberryDirective from strawberry.schema.config import StrawberryConfig from strawberry.schema_directive import StrawberrySchemaDirective from strawberry.types.enum import EnumValue from strawberry.types.field import StrawberryField from strawberry.types.info import Info from strawberry.types.scalar import ScalarDefinition FieldType = TypeVar( "FieldType", bound=GraphQLField | GraphQLInputField, covariant=True, ) class FieldConverterProtocol(Protocol, Generic[FieldType]): def __call__( # pragma: no cover self, field: StrawberryField, *, type_definition: StrawberryObjectDefinition | None = None, ) -> FieldType: ... def _get_thunk_mapping( type_definition: StrawberryObjectDefinition, name_converter: Callable[[StrawberryField], str], field_converter: FieldConverterProtocol[FieldType], get_fields: Callable[[StrawberryObjectDefinition], list[StrawberryField]], ) -> dict[str, FieldType]: """Create a GraphQL core `ThunkMapping` mapping of field names to field types. This method filters out remaining `strawberry.Private` annotated fields that could not be filtered during the initialization of a `TypeDefinition` due to postponed type-hint evaluation (PEP-563). Performing this filtering now (at schema conversion time) ensures that all types to be included in the schema should have already been resolved. Raises: TypeError: If the type of a field in ``fields`` is `UNRESOLVED` """ thunk_mapping: dict[str, FieldType] = {} fields = get_fields(type_definition) for field in fields: field_type = field.type if field_type is UNRESOLVED: raise UnresolvedFieldTypeError(type_definition, field) if not is_private(field_type): thunk_mapping[name_converter(field)] = field_converter( field, type_definition=type_definition, ) return thunk_mapping # graphql-core expects a resolver for an Enum type to return # the enum's *value* (not its name or an instance of the enum). We have to # subclass the GraphQLEnumType class to enable returning Enum members from # resolvers. class CustomGraphQLEnumType(GraphQLEnumType): def __init__( self, enum: StrawberryEnumDefinition, *args: Any, **kwargs: Any, ) -> None: super().__init__(*args, **kwargs) self.wrapped_cls = enum.wrapped_cls def serialize(self, output_value: Any) -> str: if isinstance(output_value, self.wrapped_cls): for name, value in self.values.items(): if output_value.value == value.value: return name raise ValueError( f"Invalid value for enum {self.name}: {output_value}" ) # pragma: no cover return super().serialize(output_value) def parse_value(self, input_value: str) -> Any: return self.wrapped_cls(super().parse_value(input_value)) def parse_literal( self, value_node: ValueNode, _variables: dict[str, Any] | None = None ) -> Any: return self.wrapped_cls(super().parse_literal(value_node, _variables)) def get_arguments( *, field: StrawberryField, source: Any, info: Info, kwargs: Any, config: StrawberryConfig, scalar_registry: Mapping[object, ScalarWrapper | ScalarDefinition], ) -> tuple[list[Any], dict[str, Any]]: # TODO: An extension might have changed the resolver arguments, # but we need them here since we are calling it. # This is a bit of a hack, but it's the easiest way to get the arguments # This happens in mutation.InputMutationExtension field_arguments = field.arguments[:] if field.base_resolver: existing = {arg.python_name for arg in field_arguments} field_arguments.extend( [ arg for arg in field.base_resolver.arguments if arg.python_name not in existing ] ) kwargs = convert_arguments( kwargs, field_arguments, scalar_registry=scalar_registry, config=config, ) # the following code allows to omit info and root arguments # by inspecting the original resolver arguments, # if it asks for self, the source will be passed as first argument # if it asks for root or parent, the source will be passed as kwarg # if it asks for info, the info will be passed as kwarg args = [] if field.base_resolver: if field.base_resolver.self_parameter: args.append(source) if parent_parameter := field.base_resolver.parent_parameter: kwargs[parent_parameter.name] = source if root_parameter := field.base_resolver.root_parameter: kwargs[root_parameter.name] = source if info_parameter := field.base_resolver.info_parameter: kwargs[info_parameter.name] = info return args, kwargs class GraphQLCoreConverter: # TODO: Make abstract # Extension key used to link a GraphQLType back into the Strawberry definition DEFINITION_BACKREF = "strawberry-definition" def __init__( self, config: StrawberryConfig, scalar_overrides: Mapping[object, ScalarWrapper | ScalarDefinition], get_fields: Callable[[StrawberryObjectDefinition], list[StrawberryField]], ) -> None: self.type_map: dict[str, ConcreteType] = {} self.config = config self.scalar_registry = self._get_scalar_registry(scalar_overrides) self.get_fields = get_fields def _get_scalar_registry( self, scalar_overrides: Mapping[object, ScalarWrapper | ScalarDefinition], ) -> Mapping[object, ScalarWrapper | ScalarDefinition]: scalar_registry = {**DEFAULT_SCALAR_REGISTRY} global_id_name = "GlobalID" if self.config.relay_use_legacy_global_id else "ID" scalar_registry[GlobalID] = _get_scalar_definition( scalar( GlobalID, name=global_id_name, description=GraphQLID.description, parse_value=lambda v: v, serialize=str, specified_by_url=("https://relay.dev/graphql/objectidentification.htm"), ) ) if scalar_overrides: # TODO: check that the overrides are valid scalar_registry.update(scalar_overrides) # type: ignore return scalar_registry def from_argument(self, argument: StrawberryArgument) -> GraphQLArgument: argument_type = cast( "GraphQLInputType", self.from_maybe_optional(argument.type) ) if argument.is_maybe: default_value: Any = Undefined else: default_value = Undefined if argument.default is UNSET else argument.default return GraphQLArgument( type_=argument_type, default_value=default_value, description=argument.description, deprecation_reason=argument.deprecation_reason, extensions={ GraphQLCoreConverter.DEFINITION_BACKREF: argument, }, ) def from_enum(self, enum: StrawberryEnumDefinition) -> CustomGraphQLEnumType: enum_name = self.config.name_converter.from_type(enum) assert enum_name is not None # Don't reevaluate known types cached_type = self.type_map.get(enum_name, None) if cached_type: self.validate_same_type_definition(enum_name, enum, cached_type) graphql_enum = cached_type.implementation assert isinstance(graphql_enum, CustomGraphQLEnumType) # For mypy return graphql_enum graphql_enum = CustomGraphQLEnumType( enum=enum, name=enum_name, values={ self.config.name_converter.from_enum_value( enum, item ): self.from_enum_value(item) for item in enum.values }, description=enum.description, extensions={ GraphQLCoreConverter.DEFINITION_BACKREF: enum, }, ) self.type_map[enum_name] = ConcreteType( definition=enum, implementation=graphql_enum ) return graphql_enum def from_enum_value(self, enum_value: EnumValue) -> GraphQLEnumValue: return GraphQLEnumValue( enum_value.value, deprecation_reason=enum_value.deprecation_reason, description=enum_value.description, extensions={ GraphQLCoreConverter.DEFINITION_BACKREF: enum_value, }, ) def from_directive(self, directive: StrawberryDirective) -> GraphQLDirective: graphql_arguments = {} for argument in directive.arguments: argument_name = self.config.name_converter.from_argument(argument) graphql_arguments[argument_name] = self.from_argument(argument) directive_name = self.config.name_converter.from_type(directive) return GraphQLDirective( name=directive_name, locations=directive.locations, args=graphql_arguments, description=directive.description, extensions={ GraphQLCoreConverter.DEFINITION_BACKREF: directive, }, ) def from_schema_directive(self, cls: type) -> GraphQLDirective: strawberry_directive = cast( "StrawberrySchemaDirective", cls.__strawberry_directive__, # type: ignore[attr-defined] ) module = sys.modules[cls.__module__] args: dict[str, GraphQLArgument] = {} for field in strawberry_directive.fields: default = field.default if default == dataclasses.MISSING: default = UNSET name = self.config.name_converter.get_graphql_name(field) args[name] = self.from_argument( StrawberryArgument( python_name=field.python_name or field.name, graphql_name=None, type_annotation=StrawberryAnnotation( annotation=field.type, namespace=module.__dict__, ), default=default, ) ) return GraphQLDirective( name=self.config.name_converter.from_directive(strawberry_directive), locations=[ DirectiveLocation(loc.value) for loc in strawberry_directive.locations ], args=args, is_repeatable=strawberry_directive.repeatable, description=strawberry_directive.description, extensions={ GraphQLCoreConverter.DEFINITION_BACKREF: strawberry_directive, }, ) def from_field( self, field: StrawberryField, *, type_definition: StrawberryObjectDefinition | None = None, ) -> GraphQLField: # self.from_resolver needs to be called before accessing field.type because # in there a field extension might want to change the type during its apply resolver = self.from_resolver(field) field_type = cast( "GraphQLOutputType", self.from_maybe_optional( field.resolve_type(type_definition=type_definition) ), ) subscribe = None if field.is_subscription: subscribe = resolver resolver = lambda event, *_, **__: event # noqa: E731 graphql_arguments = {} for argument in field.arguments: argument_name = self.config.name_converter.from_argument(argument) graphql_arguments[argument_name] = self.from_argument(argument) return GraphQLField( type_=field_type, args=graphql_arguments, resolve=resolver, subscribe=subscribe, description=field.description, deprecation_reason=field.deprecation_reason, extensions={ GraphQLCoreConverter.DEFINITION_BACKREF: field, }, ) def from_input_field( self, field: StrawberryField, *, type_definition: StrawberryObjectDefinition | None = None, ) -> GraphQLInputField: field_type = cast( "GraphQLInputType", self.from_maybe_optional( field.resolve_type(type_definition=type_definition) ), ) default_value: object if isinstance(field.type, StrawberryMaybe): default_value = Undefined elif field.default_value is UNSET or field.default_value is dataclasses.MISSING: default_value = Undefined else: default_value = field.default_value return GraphQLInputField( type_=field_type, default_value=default_value, description=field.description, deprecation_reason=field.deprecation_reason, extensions={ GraphQLCoreConverter.DEFINITION_BACKREF: field, }, ) def get_graphql_fields( self, type_definition: StrawberryObjectDefinition ) -> dict[str, GraphQLField]: return _get_thunk_mapping( type_definition=type_definition, name_converter=self.config.name_converter.from_field, field_converter=self.from_field, get_fields=self.get_fields, ) def get_graphql_input_fields( self, type_definition: StrawberryObjectDefinition ) -> dict[str, GraphQLInputField]: return _get_thunk_mapping( type_definition=type_definition, name_converter=self.config.name_converter.from_field, field_converter=self.from_input_field, get_fields=self.get_fields, ) def from_input_object(self, object_type: type) -> GraphQLInputObjectType: type_definition = object_type.__strawberry_definition__ # type: ignore type_name = self.config.name_converter.from_type(type_definition) # Don't reevaluate known types cached_type = self.type_map.get(type_name, None) if cached_type: self.validate_same_type_definition(type_name, type_definition, cached_type) graphql_object_type = self.type_map[type_name].implementation assert isinstance(graphql_object_type, GraphQLInputObjectType) # For mypy return graphql_object_type def check_one_of(value: dict[str, Any]) -> dict[str, Any]: if len(value) != 1: raise GraphQLError( f"OneOf Input Object '{type_name}' must specify exactly one key." ) first_key, first_value = next(iter(value.items())) if first_value is None or first_value is UNSET: raise GraphQLError( f"Value for member field '{first_key}' must be non-null" ) return value out_type = ( check_one_of if type_definition.is_input and type_definition.is_one_of else None ) graphql_object_type = GraphQLInputObjectType( name=type_name, fields=lambda: self.get_graphql_input_fields(type_definition), description=type_definition.description, extensions={ GraphQLCoreConverter.DEFINITION_BACKREF: type_definition, }, out_type=out_type, ) self.type_map[type_name] = ConcreteType( definition=type_definition, implementation=graphql_object_type ) return graphql_object_type def from_interface( self, interface: StrawberryObjectDefinition ) -> GraphQLInterfaceType: interface_name = self.config.name_converter.from_type(interface) # Don't re-evaluate known types cached_type = self.type_map.get(interface_name, None) if cached_type: self.validate_same_type_definition(interface_name, interface, cached_type) graphql_interface = cached_type.implementation assert isinstance(graphql_interface, GraphQLInterfaceType) # For mypy return graphql_interface def _get_resolve_type() -> Callable[ [Any, GraphQLResolveInfo, GraphQLAbstractType], Awaitable[str | None] | str | None, ]: if interface.resolve_type: return interface.resolve_type def resolve_type( obj: Any, info: GraphQLResolveInfo, abstract_type: GraphQLAbstractType ) -> Awaitable[str | None] | str | None: if isinstance(obj, interface.origin): type_definition = get_object_definition(obj, strict=True) # TODO: we should find the correct type here from the # generic if not type_definition.is_graphql_generic: return type_definition.name # here we don't all the implementations of the generic # we need to find a way to find them, for now maybe # we can follow the union's approach and iterate over # all the types in the schema, but we should probably # optimize this return_type: GraphQLType | None = None for possible_concrete_type in self.type_map.values(): possible_type = possible_concrete_type.definition if not isinstance( possible_type, StrawberryObjectDefinition ): # pragma: no cover continue if possible_type.is_implemented_by(obj): return_type = possible_concrete_type.implementation break if return_type: assert isinstance(return_type, GraphQLNamedType) return return_type.name # Revert to calling is_type_of for cases where a direct subclass # of the interface is not returned (i.e. an ORM object) return default_type_resolver(obj, info, abstract_type) return resolve_type graphql_interface = GraphQLInterfaceType( name=interface_name, fields=lambda: self.get_graphql_fields(interface), interfaces=list(map(self.from_interface, interface.interfaces)), description=interface.description, extensions={ GraphQLCoreConverter.DEFINITION_BACKREF: interface, }, resolve_type=_get_resolve_type(), ) self.type_map[interface_name] = ConcreteType( definition=interface, implementation=graphql_interface ) return graphql_interface def from_list(self, type_: StrawberryList) -> GraphQLList: of_type = self.from_maybe_optional(type_.of_type) return GraphQLList(of_type) def from_object(self, object_type: StrawberryObjectDefinition) -> GraphQLObjectType: # TODO: Use StrawberryObjectType when it's implemented in another PR object_type_name = self.config.name_converter.from_type(object_type) # Don't reevaluate known types cached_type = self.type_map.get(object_type_name, None) if cached_type: self.validate_same_type_definition( object_type_name, object_type, cached_type ) graphql_object_type = cached_type.implementation assert isinstance(graphql_object_type, GraphQLObjectType) # For mypy return graphql_object_type def _get_is_type_of() -> Callable[[Any, GraphQLResolveInfo], bool] | None: if object_type.is_type_of: return object_type.is_type_of if not object_type.interfaces: return None # this allows returning interfaces types as well as the actual object type # this is useful in combination with `resolve_type` in interfaces possible_types = ( *tuple(interface.origin for interface in object_type.interfaces), object_type.origin, ) def is_type_of(obj: Any, _info: GraphQLResolveInfo) -> bool: if (type_cast := get_strawberry_type_cast(obj)) is not None: return type_cast in possible_types if object_type.concrete_of and ( has_object_definition(obj) and obj.__strawberry_definition__.origin is object_type.concrete_of.origin ): return True return isinstance(obj, possible_types) return is_type_of graphql_object_type = GraphQLObjectType( name=object_type_name, fields=lambda: self.get_graphql_fields(object_type), interfaces=list(map(self.from_interface, object_type.interfaces)), description=object_type.description, is_type_of=_get_is_type_of(), extensions={ GraphQLCoreConverter.DEFINITION_BACKREF: object_type, }, ) self.type_map[object_type_name] = ConcreteType( definition=object_type, implementation=graphql_object_type ) return graphql_object_type def from_resolver( self, field: StrawberryField ) -> Callable: # TODO: Take StrawberryResolver field.default_resolver = self.config.default_resolver if field.is_basic_field: def _get_basic_result(_source: Any, *args: str, **kwargs: Any) -> Any: # Call `get_result` without an info object or any args or # kwargs because this is a basic field with no resolver. return field.get_result(_source, info=None, args=[], kwargs={}) _get_basic_result._is_default = True # type: ignore return _get_basic_result def _strawberry_info_from_graphql(info: GraphQLResolveInfo) -> Info: return self.config.info_class( _raw_info=info, _field=field, ) def _get_result( _source: Any, info: Info, field_args: list[Any], field_kwargs: dict[str, Any], ) -> Any: return field.get_result( _source, info=info, args=field_args, kwargs=field_kwargs ) def wrap_field_extensions() -> Callable[..., Any]: """Wrap the provided field resolver with the middleware.""" for extension in field.extensions: extension.apply(field) extension_functions = build_field_extension_resolvers(field) def extension_resolver( _source: Any, info: Info, **kwargs: Any, ) -> Any: # parse field arguments into Strawberry input types and convert # field names to Python equivalents field_args, field_kwargs = get_arguments( field=field, source=_source, info=info, kwargs=kwargs, config=self.config, scalar_registry=self.scalar_registry, ) resolver_requested_info = False if "info" in field_kwargs: resolver_requested_info = True # remove info from field_kwargs because we're passing it # explicitly to the extensions field_kwargs.pop("info") # `_get_result` expects `field_args` and `field_kwargs` as # separate arguments so we have to wrap the function so that we # can pass them in def wrapped_get_result(_source: Any, info: Info, **kwargs: Any) -> Any: # if the resolver function requested the info object info # then put it back in the kwargs dictionary if resolver_requested_info: kwargs["info"] = info return _get_result( _source, info, field_args=field_args, field_kwargs=kwargs ) # combine all the extension resolvers return reduce( lambda chained_fn, next_fn: partial(next_fn, chained_fn), extension_functions, wrapped_get_result, )(_source, info, **field_kwargs) return extension_resolver _get_result_with_extensions = wrap_field_extensions() def _resolver(_source: Any, info: GraphQLResolveInfo, **kwargs: Any) -> Any: strawberry_info = _strawberry_info_from_graphql(info) return _get_result_with_extensions( _source, strawberry_info, **kwargs, ) async def _async_resolver( _source: Any, info: GraphQLResolveInfo, **kwargs: Any ) -> Any: strawberry_info = _strawberry_info_from_graphql(info) return await await_maybe( _get_result_with_extensions( _source, strawberry_info, **kwargs, ) ) if field.is_async: _async_resolver._is_default = not field.base_resolver # type: ignore return _async_resolver _resolver._is_default = not field.base_resolver # type: ignore return _resolver def from_scalar(self, scalar: type) -> GraphQLScalarType: from strawberry.relay.types import GlobalID if not self.config.relay_use_legacy_global_id and scalar is GlobalID: from strawberry import ID return self.from_scalar(ID) scalar_definition: ScalarDefinition if scalar in self.scalar_registry: _scalar_definition = self.scalar_registry[scalar] # TODO: check why we need the cast and we are not trying with getattr first if isinstance(_scalar_definition, ScalarWrapper): scalar_definition = _scalar_definition._scalar_definition else: scalar_definition = _scalar_definition else: scalar_definition = scalar._scalar_definition # type: ignore[attr-defined] scalar_name = self.config.name_converter.from_type(scalar_definition) if scalar_name not in self.type_map: implementation = ( scalar_definition.implementation if scalar_definition.implementation is not None else _make_scalar_type(scalar_definition) ) self.type_map[scalar_name] = ConcreteType( definition=scalar_definition, implementation=implementation ) else: other_definition = self.type_map[scalar_name].definition # TODO: the other definition might not be a scalar, we should # handle this case better, since right now we assume it is a scalar if other_definition != scalar_definition: other_definition = cast("ScalarDefinition", other_definition) raise ScalarAlreadyRegisteredError(scalar_definition, other_definition) implementation = cast( "GraphQLScalarType", self.type_map[scalar_name].implementation ) return implementation def from_maybe_optional( self, type_: StrawberryType | type ) -> GraphQLNullableType | GraphQLNonNull: NoneType = type(None) if type_ is None or type_ is NoneType: return self.from_type(type_) if isinstance(type_, StrawberryMaybe): # StrawberryMaybe should always generate optional types # because Maybe[T] = Union[Some[T], None] (field can be absent) # But we need to handle the case where of_type is itself optional if isinstance(type_.of_type, StrawberryOptional): return self.from_type(type_.of_type.of_type) return self.from_type(type_.of_type) if isinstance(type_, StrawberryOptional): return self.from_type(type_.of_type) return GraphQLNonNull(self.from_type(type_)) def from_type(self, type_: StrawberryType | type) -> GraphQLNullableType: if compat.is_graphql_generic(type_): raise MissingTypesForGenericError(type_) # to handle lazy unions if typing.get_origin(type_) is Annotated: args = typing.get_args(type_) if len(args) >= 2 and isinstance(args[1], StrawberryUnion): type_ = args[1] if isinstance(type_, StrawberryEnumDefinition): return self.from_enum(type_) if compat.is_input_type(type_): # TODO: Replace with StrawberryInputObject return self.from_input_object(type_) if isinstance(type_, StrawberryList): return self.from_list(type_) if compat.is_interface_type(type_): # TODO: Replace with StrawberryInterface type_definition: StrawberryObjectDefinition = ( type_.__strawberry_definition__ # type: ignore ) return self.from_interface(type_definition) if has_object_definition(type_): return self.from_object(type_.__strawberry_definition__) if has_enum_definition(type_): enum_definition: StrawberryEnumDefinition = type_.__strawberry_definition__ return self.from_enum(enum_definition) if isinstance(type_, StrawberryObjectDefinition): return self.from_object(type_) if isinstance(type_, StrawberryUnion): return self.from_union(type_) if isinstance(type_, LazyType): return self.from_type(type_.resolve_type()) if compat.is_scalar( type_, self.scalar_registry ): # TODO: Replace with StrawberryScalar return self.from_scalar(type_) raise TypeError(f"Unexpected type '{type_}'") def from_union(self, union: StrawberryUnion) -> GraphQLUnionType: union_name = self.config.name_converter.from_type(union) for type_ in union.types: # This check also occurs in the Annotation resolving, but because of # TypeVars, Annotations, LazyTypes, etc it can't perfectly detect issues at # that stage if not StrawberryUnion.is_valid_union_type(type_): raise InvalidUnionTypeError(union_name, type_, union_definition=union) # Don't re-evaluate known types if union_name in self.type_map: graphql_union = self.type_map[union_name].implementation assert isinstance(graphql_union, GraphQLUnionType) # For mypy return graphql_union graphql_types: list[GraphQLObjectType] = [] for type_ in union.types: graphql_type = self.from_type(type_) if isinstance(graphql_type, GraphQLInputObjectType): raise InvalidTypeInputForUnion(graphql_type) assert isinstance(graphql_type, (GraphQLObjectType, GraphQLUnionType)) # If the graphql_type is a GraphQLUnionType, merge its child types if isinstance(graphql_type, GraphQLUnionType): # Add the child types of the GraphQLUnionType to the list of graphql_types, # filter out any duplicates for child_type in graphql_type.types: if child_type not in graphql_types: graphql_types.append(child_type) else: graphql_types.append(graphql_type) graphql_union = GraphQLUnionType( name=union_name, types=graphql_types, description=union.description, resolve_type=union.get_type_resolver(self.type_map), extensions={ GraphQLCoreConverter.DEFINITION_BACKREF: union, }, ) self.type_map[union_name] = ConcreteType( definition=union, implementation=graphql_union ) return graphql_union def _get_is_type_of( self, object_type: StrawberryObjectDefinition, ) -> Callable[[Any, GraphQLResolveInfo], bool] | None: if object_type.is_type_of: return object_type.is_type_of if object_type.interfaces: def is_type_of(obj: Any, _info: GraphQLResolveInfo) -> bool: if (type_cast := get_strawberry_type_cast(obj)) is not None: return type_cast is object_type.origin if object_type.concrete_of and ( has_object_definition(obj) and obj.__strawberry_definition__.origin is object_type.concrete_of.origin ): return True return isinstance(obj, object_type.origin) return is_type_of return None def validate_same_type_definition( self, name: str, type_definition: StrawberryType, cached_type: ConcreteType ) -> None: # Skip validation if _unsafe_disable_same_type_validation is True if ( self.config._unsafe_disable_same_type_validation or cached_type.definition == type_definition ): return # otherwise we need to check if we are dealing with different instances # of the same type generic type. This happens when using the same generic # type in different places in the schema, like in the following example: # >>> @strawberry.type # >>> class A(Generic[T]): # >>> a: T # >>> @strawberry.type # >>> class Query: # >>> first: A[int] # >>> second: A[int] # in theory we won't ever have duplicated definitions for the same generic, # but we are doing the check in an exhaustive way just in case we missed # something. # we only do this check for TypeDefinitions, as they are the only ones # that can be generic. # of they are of the same generic type, we need to check if the type # var map is the same, in that case we can return first_type_definition = cached_type.definition second_type_definition = type_definition if self.is_same_type_definition( first_type_definition, second_type_definition, ): return if isinstance(second_type_definition, StrawberryObjectDefinition): first_origin = second_type_definition.origin elif isinstance(second_type_definition, StrawberryEnumDefinition): first_origin = second_type_definition.wrapped_cls else: first_origin = None if isinstance(first_type_definition, StrawberryObjectDefinition): second_origin = first_type_definition.origin elif isinstance(first_type_definition, StrawberryEnumDefinition): second_origin = first_type_definition.wrapped_cls else: second_origin = None raise DuplicatedTypeName(first_origin, second_origin, name) def is_same_type_definition( self, first_type_definition: StrawberryObjectDefinition | StrawberryType, second_type_definition: StrawberryObjectDefinition | StrawberryType, ) -> bool: # TODO: maybe move this on the StrawberryType class if ( not isinstance(first_type_definition, StrawberryObjectDefinition) or not isinstance(second_type_definition, StrawberryObjectDefinition) or first_type_definition.concrete_of is None or first_type_definition.concrete_of != second_type_definition.concrete_of or ( first_type_definition.type_var_map.keys() != second_type_definition.type_var_map.keys() ) ): return False # manually compare type_var_maps while resolving any lazy types # so that they're considered equal to the actual types they're referencing for type_var, type1 in first_type_definition.type_var_map.items(): type2 = second_type_definition.type_var_map[type_var] # both lazy types are always resolved because two different lazy types # may be referencing the same actual type if isinstance(type1, LazyType): type1 = type1.resolve_type() # noqa: PLW2901 elif isinstance(type1, StrawberryOptional) and isinstance( type1.of_type, LazyType ): type1.of_type = type1.of_type.resolve_type() if isinstance(type2, LazyType): type2 = type2.resolve_type() elif isinstance(type2, StrawberryOptional) and isinstance( type2.of_type, LazyType ): type2.of_type = type2.of_type.resolve_type() same_type = type1 == type2 # If both types have object definitions, we are handling a nested generic # type like `Foo[Foo[int]]`, meaning we need to compare their type definitions # as they will actually be different instances of the type if ( not same_type and has_object_definition(type1) and has_object_definition(type2) ): same_type = self.is_same_type_definition( type1.__strawberry_definition__, type2.__strawberry_definition__, ) if not same_type: return False return True __all__ = ["GraphQLCoreConverter"] strawberry-graphql-0.287.0/strawberry/schema/types/000077500000000000000000000000001511033167500223425ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/schema/types/__init__.py000066400000000000000000000001041511033167500244460ustar00rootroot00000000000000from .concrete_type import ConcreteType __all__ = ["ConcreteType"] strawberry-graphql-0.287.0/strawberry/schema/types/base_scalars.py000066400000000000000000000036741511033167500253500ustar00rootroot00000000000000import datetime import decimal import uuid from collections.abc import Callable from operator import methodcaller import dateutil.parser from graphql import GraphQLError from strawberry.types.scalar import scalar def wrap_parser(parser: Callable, type_: str) -> Callable: def inner(value: str) -> object: try: return parser(value) except ValueError as e: raise GraphQLError( # noqa: B904 f'Value cannot represent a {type_}: "{value}". {e}' ) return inner def parse_decimal(value: object) -> decimal.Decimal: try: return decimal.Decimal(str(value)) except decimal.DecimalException: raise GraphQLError(f'Value cannot represent a Decimal: "{value}".') # noqa: B904 isoformat = methodcaller("isoformat") Date = scalar( datetime.date, name="Date", description="Date (isoformat)", serialize=isoformat, parse_value=wrap_parser(datetime.date.fromisoformat, "Date"), ) DateTime = scalar( datetime.datetime, name="DateTime", description="Date with time (isoformat)", serialize=isoformat, parse_value=wrap_parser(dateutil.parser.isoparse, "DateTime"), ) Time = scalar( datetime.time, name="Time", description="Time (isoformat)", serialize=isoformat, parse_value=wrap_parser(datetime.time.fromisoformat, "Time"), ) Decimal = scalar( decimal.Decimal, name="Decimal", description="Decimal (fixed-point)", serialize=str, parse_value=parse_decimal, ) UUID = scalar( uuid.UUID, name="UUID", serialize=str, parse_value=wrap_parser(uuid.UUID, "UUID"), ) def _verify_void(x: None) -> None: if x is not None: raise ValueError(f"Expected 'None', got '{x}'") Void = scalar( type(None), name="Void", serialize=_verify_void, parse_value=_verify_void, description="Represents NULL values", ) __all__ = ["UUID", "Date", "DateTime", "Decimal", "Time", "Void"] strawberry-graphql-0.287.0/strawberry/schema/types/concrete_type.py000066400000000000000000000014411511033167500255570ustar00rootroot00000000000000from __future__ import annotations import dataclasses from typing import TYPE_CHECKING, TypeAlias from graphql import GraphQLField, GraphQLInputField, GraphQLType if TYPE_CHECKING: from strawberry.types.base import StrawberryObjectDefinition from strawberry.types.enum import StrawberryEnumDefinition from strawberry.types.scalar import ScalarDefinition from strawberry.types.union import StrawberryUnion Field: TypeAlias = GraphQLInputField | GraphQLField @dataclasses.dataclass class ConcreteType: definition: ( StrawberryObjectDefinition | StrawberryEnumDefinition | ScalarDefinition | StrawberryUnion ) implementation: GraphQLType TypeMap = dict[str, ConcreteType] __all__ = ["ConcreteType", "Field", "GraphQLType", "TypeMap"] strawberry-graphql-0.287.0/strawberry/schema/types/scalar.py000066400000000000000000000045071511033167500241670ustar00rootroot00000000000000import datetime import decimal from uuid import UUID from graphql import ( GraphQLBoolean, GraphQLFloat, GraphQLID, GraphQLInt, GraphQLScalarType, GraphQLString, ) from strawberry.file_uploads.scalars import Upload from strawberry.scalars import ID from strawberry.schema.types import base_scalars from strawberry.types.scalar import ScalarDefinition def _make_scalar_type(definition: ScalarDefinition) -> GraphQLScalarType: from strawberry.schema.schema_converter import GraphQLCoreConverter return GraphQLScalarType( name=definition.name, description=definition.description, specified_by_url=definition.specified_by_url, serialize=definition.serialize, parse_value=definition.parse_value, parse_literal=definition.parse_literal, extensions={GraphQLCoreConverter.DEFINITION_BACKREF: definition}, ) def _make_scalar_definition(scalar_type: GraphQLScalarType) -> ScalarDefinition: return ScalarDefinition( name=scalar_type.name, description=scalar_type.name, specified_by_url=scalar_type.specified_by_url, serialize=scalar_type.serialize, parse_literal=scalar_type.parse_literal, parse_value=scalar_type.parse_value, implementation=scalar_type, origin=scalar_type, ) def _get_scalar_definition(scalar: type) -> ScalarDefinition: return scalar._scalar_definition # type: ignore[attr-defined] DEFAULT_SCALAR_REGISTRY: dict[object, ScalarDefinition] = { type(None): _get_scalar_definition(base_scalars.Void), None: _get_scalar_definition(base_scalars.Void), str: _make_scalar_definition(GraphQLString), int: _make_scalar_definition(GraphQLInt), float: _make_scalar_definition(GraphQLFloat), bool: _make_scalar_definition(GraphQLBoolean), ID: _make_scalar_definition(GraphQLID), UUID: _get_scalar_definition(base_scalars.UUID), Upload: _get_scalar_definition(Upload), datetime.date: _get_scalar_definition(base_scalars.Date), datetime.datetime: _get_scalar_definition(base_scalars.DateTime), datetime.time: _get_scalar_definition(base_scalars.Time), decimal.Decimal: _get_scalar_definition(base_scalars.Decimal), } __all__ = [ "DEFAULT_SCALAR_REGISTRY", "_get_scalar_definition", "_make_scalar_definition", "_make_scalar_type", ] strawberry-graphql-0.287.0/strawberry/schema/validation_rules/000077500000000000000000000000001511033167500245425ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/schema/validation_rules/__init__.py000066400000000000000000000000001511033167500266410ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/schema/validation_rules/maybe_null.py000066400000000000000000000127701511033167500272520ustar00rootroot00000000000000from typing import Any from graphql import ( ArgumentNode, GraphQLError, GraphQLNamedType, ObjectValueNode, ValidationContext, ValidationRule, get_named_type, ) from strawberry.types.base import StrawberryMaybe, StrawberryOptional from strawberry.utils.str_converters import to_camel_case class MaybeNullValidationRule(ValidationRule): """Validates that Maybe[T] fields do not receive explicit null values. This rule ensures that: - Maybe[T] fields can only be omitted or have non-null values - Maybe[T | None] fields can be omitted, null, or have non-null values This provides clear semantics where Maybe[T] means "either present with value or absent" and Maybe[T | None] means "present with value, present but null, or absent". """ def __init__(self, validation_context: ValidationContext) -> None: super().__init__(validation_context) def enter_argument(self, node: ArgumentNode, *_args: Any) -> None: # Check if this is a null value if node.value.kind != "null_value": return # Get the argument definition from the schema argument_def = self.context.get_argument() if not argument_def: return # Check if this argument corresponds to a Maybe[T] (not Maybe[T | None]) # The argument type extensions should contain the Strawberry type info strawberry_arg_info = argument_def.extensions.get("strawberry-definition") if not strawberry_arg_info: return # Get the Strawberry type from the argument info field_type = getattr(strawberry_arg_info, "type", None) if not field_type: return if isinstance(field_type, StrawberryMaybe) and not isinstance( field_type.of_type, StrawberryOptional ): # This is Maybe[T] - should not accept null values type_name = self._get_type_name(field_type.of_type) self.report_error( GraphQLError( f"Expected value of type '{type_name}', found null. " f"Argument '{node.name.value}' of type 'Maybe[{type_name}]' cannot be explicitly set to null. " f"Use 'Maybe[{type_name} | None]' if you need to allow null values.", nodes=[node], ) ) def enter_object_value(self, node: ObjectValueNode, *_args: Any) -> None: # Get the input type for this object input_type = get_named_type(self.context.get_input_type()) if not input_type: return # Get the Strawberry type definition from extensions strawberry_type = input_type.extensions.get("strawberry-definition") if not strawberry_type: return # Check each field in the object for null Maybe[T] violations self.validate_maybe_fields(node, input_type, strawberry_type) def validate_maybe_fields( self, node: ObjectValueNode, input_type: GraphQLNamedType, strawberry_type: Any ) -> None: # Create a map of field names to field nodes for easy lookup field_node_map = {field.name.value: field for field in node.fields} # Check each field in the Strawberry type definition if not hasattr(strawberry_type, "fields"): return for field_def in strawberry_type.fields: # Resolve the actual GraphQL field name using the same logic as NameConverter if field_def.graphql_name is not None: field_name = field_def.graphql_name else: # Apply auto_camel_case conversion if enabled (default behavior) field_name = to_camel_case(field_def.python_name) # Check if this field is present in the input and has a null value if field_name in field_node_map: field_node = field_node_map[field_name] # Check if this field has a null value if field_node.value.kind == "null_value": # Check if this is a Maybe[T] (not Maybe[T | None]) field_type = field_def.type if isinstance(field_type, StrawberryMaybe) and not isinstance( field_type.of_type, StrawberryOptional ): # This is Maybe[T] - should not accept null values type_name = self._get_type_name(field_type.of_type) self.report_error( GraphQLError( f"Expected value of type '{type_name}', found null. " f"Field '{field_name}' of type 'Maybe[{type_name}]' cannot be explicitly set to null. " f"Use 'Maybe[{type_name} | None]' if you need to allow null values.", nodes=[field_node], ) ) def _get_type_name(self, type_: Any) -> str: """Get a readable type name for error messages.""" if hasattr(type_, "__name__"): return type_.__name__ # Handle Strawberry types that don't have __name__ if hasattr(type_, "of_type") and hasattr(type_.of_type, "__name__"): # For StrawberryList, StrawberryOptional, etc. return ( f"list[{type_.of_type.__name__}]" if "List" in str(type_.__class__) else type_.of_type.__name__ ) return str(type_) __all__ = ["MaybeNullValidationRule"] strawberry-graphql-0.287.0/strawberry/schema/validation_rules/one_of.py000066400000000000000000000051051511033167500263620ustar00rootroot00000000000000from typing import Any from graphql import ( ExecutableDefinitionNode, GraphQLError, GraphQLNamedType, ObjectValueNode, ValidationContext, ValidationRule, VariableDefinitionNode, get_named_type, ) class OneOfInputValidationRule(ValidationRule): def __init__(self, validation_context: ValidationContext) -> None: super().__init__(validation_context) def enter_operation_definition( self, node: ExecutableDefinitionNode, *_args: Any ) -> None: self.variable_definitions: dict[str, VariableDefinitionNode] = {} def enter_variable_definition( self, node: VariableDefinitionNode, *_args: Any ) -> None: self.variable_definitions[node.variable.name.value] = node def enter_object_value(self, node: ObjectValueNode, *_args: Any) -> None: type_ = get_named_type(self.context.get_input_type()) if not type_: return strawberry_type = type_.extensions.get("strawberry-definition") if strawberry_type and strawberry_type.is_one_of: self.validate_one_of(node, type_) def validate_one_of(self, node: ObjectValueNode, type: GraphQLNamedType) -> None: field_node_map = {field.name.value: field for field in node.fields} keys = list(field_node_map.keys()) is_not_exactly_one_field = len(keys) != 1 if is_not_exactly_one_field: self.report_error( GraphQLError( f"OneOf Input Object '{type.name}' must specify exactly one key.", nodes=[node], ) ) return value = field_node_map[keys[0]].value is_null_literal = not value or value.kind == "null_value" is_variable = value.kind == "variable" if is_null_literal: self.report_error( GraphQLError( f"Field '{type.name}.{keys[0]}' must be non-null.", nodes=[node], ) ) return if is_variable: variable_name = value.name.value # type: ignore definition = self.variable_definitions[variable_name] is_nullable_variable = definition.type.kind != "non_null_type" if is_nullable_variable: self.report_error( GraphQLError( f"Variable '{variable_name}' must be non-nullable to be used for OneOf Input Object '{type.name}'.", nodes=[node], ) ) __all__ = ["OneOfInputValidationRule"] strawberry-graphql-0.287.0/strawberry/schema_codegen/000077500000000000000000000000001511033167500226625ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/schema_codegen/__init__.py000066400000000000000000000571411511033167500250030ustar00rootroot00000000000000from __future__ import annotations import dataclasses import keyword from collections import defaultdict from graphlib import TopologicalSorter from typing import TYPE_CHECKING, TypeAlias from typing_extensions import Protocol import libcst as cst from graphql import ( EnumTypeDefinitionNode, EnumValueDefinitionNode, FieldDefinitionNode, InputObjectTypeDefinitionNode, InputValueDefinitionNode, InterfaceTypeDefinitionNode, ListTypeNode, NamedTypeNode, NonNullTypeNode, ObjectTypeDefinitionNode, ObjectTypeExtensionNode, OperationType, ScalarTypeDefinitionNode, SchemaDefinitionNode, SchemaExtensionNode, StringValueNode, TypeNode, UnionTypeDefinitionNode, parse, ) from graphql.language.ast import ( BooleanValueNode, ConstValueNode, ListValueNode, ) from strawberry.utils.str_converters import to_snake_case if TYPE_CHECKING: from graphql.language.ast import ConstDirectiveNode class HasDirectives(Protocol): directives: tuple[ConstDirectiveNode, ...] _SCALAR_MAP = { "Int": cst.Name("int"), "Float": cst.Name("float"), "Boolean": cst.Name("bool"), "String": cst.Name("str"), "ID": cst.Attribute( value=cst.Name("strawberry"), attr=cst.Name("ID"), ), "JSON": cst.Attribute( value=cst.Name("strawberry"), attr=cst.Name("JSON"), ), "UUID": cst.Name("UUID"), "Decimal": cst.Name("Decimal"), "Date": cst.Name("date"), "Time": cst.Name("time"), "DateTime": cst.Name("datetime"), } @dataclasses.dataclass(frozen=True) class Import: module: str | None imports: tuple[str] def module_path_to_cst(self, module_path: str) -> cst.Name | cst.Attribute: parts = module_path.split(".") module_name: cst.Name | cst.Attribute = cst.Name(parts[0]) for part in parts[1:]: module_name = cst.Attribute(value=module_name, attr=cst.Name(part)) return module_name def to_cst(self) -> cst.Import | cst.ImportFrom: if self.module is None: return cst.Import( names=[cst.ImportAlias(name=cst.Name(name)) for name in self.imports] ) return cst.ImportFrom( module=self.module_path_to_cst(self.module), names=[cst.ImportAlias(name=cst.Name(name)) for name in self.imports], ) def _is_federation_link_directive(directive: ConstDirectiveNode) -> bool: if directive.name.value != "link": return False return next( ( argument.value.value for argument in directive.arguments if argument.name.value == "url" if isinstance(argument.value, StringValueNode) ), "", ).startswith("https://specs.apollo.dev/federation") def _get_field_type( field_type: TypeNode, was_non_nullable: bool = False ) -> cst.BaseExpression: expr: cst.BaseExpression | None if isinstance(field_type, NonNullTypeNode): return _get_field_type(field_type.type, was_non_nullable=True) if isinstance(field_type, ListTypeNode): expr = cst.Subscript( value=cst.Name("list"), slice=[ cst.SubscriptElement( cst.Index( value=_get_field_type(field_type.type), ), ) ], ) elif isinstance(field_type, NamedTypeNode): expr = _SCALAR_MAP.get(field_type.name.value) if expr is None: expr = cst.Name(field_type.name.value) else: raise NotImplementedError(f"Unknown type {field_type}") if was_non_nullable: return expr return cst.BinaryOperation( left=expr, operator=cst.BitOr(), right=cst.Name("None"), ) def _sanitize_argument(value: ArgumentValue) -> cst.SimpleString | cst.Name | cst.List: if isinstance(value, bool): return cst.Name(value=str(value)) if isinstance(value, list): return cst.List( elements=[ cst.Element(value=_sanitize_argument(item)) for item in value if item is not None ], ) if "\n" in value: argument_value = cst.SimpleString(f'"""\n{value}\n"""') elif '"' in value: argument_value = cst.SimpleString(f"'{value}'") else: argument_value = cst.SimpleString(f'"{value}"') return argument_value def _get_argument(name: str, value: ArgumentValue) -> cst.Arg: argument_value = _sanitize_argument(value) return cst.Arg( value=argument_value, keyword=cst.Name(name), equal=cst.AssignEqual(cst.SimpleWhitespace(""), cst.SimpleWhitespace("")), ) def _get_field_value( field: FieldDefinitionNode | InputValueDefinitionNode, alias: str | None, is_apollo_federation: bool, imports: set[Import], ) -> cst.Call | None: description = field.description.value if field.description else None args = list( filter( None, [ _get_argument("description", description) if description else None, _get_argument("name", alias) if alias else None, ], ) ) directives = _get_directives(field) apollo_federation_args = _get_federation_arguments(directives, imports) if is_apollo_federation and apollo_federation_args: args.extend(apollo_federation_args) return cst.Call( func=cst.Attribute( value=cst.Attribute( value=cst.Name("strawberry"), attr=cst.Name("federation"), ), attr=cst.Name("field"), ), args=args, ) if args: return cst.Call( func=cst.Attribute( value=cst.Name("strawberry"), attr=cst.Name("field"), ), args=args, ) return None def _get_field( field: FieldDefinitionNode | InputValueDefinitionNode, is_apollo_federation: bool, imports: set[Import], ) -> cst.SimpleStatementLine: name = to_snake_case(field.name.value) alias: str | None = None if keyword.iskeyword(name): name = f"{name}_" alias = field.name.value return cst.SimpleStatementLine( body=[ cst.AnnAssign( target=cst.Name(name), annotation=cst.Annotation( _get_field_type(field.type), ), value=_get_field_value( field, alias=alias, is_apollo_federation=is_apollo_federation, imports=imports, ), ) ] ) ArgumentValue: TypeAlias = str | bool | list["ArgumentValue"] def _get_argument_value(argument_value: ConstValueNode) -> ArgumentValue: if isinstance(argument_value, StringValueNode): return argument_value.value if isinstance(argument_value, EnumValueDefinitionNode): return argument_value.name.value if isinstance(argument_value, ListValueNode): return [_get_argument_value(arg) for arg in argument_value.values] if isinstance(argument_value, BooleanValueNode): return argument_value.value raise NotImplementedError(f"Unknown argument value {argument_value}") def _get_directives( definition: HasDirectives, ) -> dict[str, list[dict[str, ArgumentValue]]]: directives: dict[str, list[dict[str, ArgumentValue]]] = defaultdict(list) for directive in definition.directives: directive_name = directive.name.value directives[directive_name].append( { argument.name.value: _get_argument_value(argument.value) for argument in directive.arguments } ) return directives def _get_federation_arguments( directives: dict[str, list[dict[str, ArgumentValue]]], imports: set[Import], ) -> list[cst.Arg]: def append_arg_from_directive( directive: str, argument_name: str, keyword_name: str | None = None, flatten: bool = True, ) -> None: keyword_name = keyword_name or directive if directive in directives: values = [item[argument_name] for item in directives[directive]] if flatten: arguments.append(_get_argument(keyword_name, values)) else: arguments.extend(_get_argument(keyword_name, value) for value in values) arguments: list[cst.Arg] = [] append_arg_from_directive("key", "fields", "keys") append_arg_from_directive("requires", "fields") append_arg_from_directive("provides", "fields") append_arg_from_directive( "requiresScopes", "scopes", "requires_scopes", flatten=False ) append_arg_from_directive("policy", "policies", "policy", flatten=False) append_arg_from_directive("tag", "name", "tags") boolean_keys = ( "shareable", "inaccessible", "external", "authenticated", ) arguments.extend( _get_argument(key, True) for key in boolean_keys if directives.get(key, False) ) if overrides := directives.get("override"): override = overrides[0] if "label" not in override: arguments.append(_get_argument("override", override["from"])) else: imports.add( Import( module="strawberry.federation.schema_directives", imports=("Override",), ) ) arguments.append( cst.Arg( keyword=cst.Name("override"), value=cst.Call( func=cst.Name("Override"), args=[ _get_argument("override_from", override["from"]), _get_argument("label", override["label"]), ], ), equal=cst.AssignEqual( cst.SimpleWhitespace(""), cst.SimpleWhitespace("") ), ) ) return arguments def _get_strawberry_decorator( definition: ObjectTypeDefinitionNode | ObjectTypeExtensionNode | InterfaceTypeDefinitionNode | InputObjectTypeDefinitionNode, is_apollo_federation: bool, imports: set[Import], ) -> cst.Decorator: type_ = { ObjectTypeDefinitionNode: "type", InterfaceTypeDefinitionNode: "interface", InputObjectTypeDefinitionNode: "input", ObjectTypeExtensionNode: "type", }[type(definition)] description = ( definition.description if not isinstance(definition, ObjectTypeExtensionNode) else None ) directives = _get_directives(definition) decorator: cst.BaseExpression = cst.Attribute( value=cst.Name("strawberry"), attr=cst.Name(type_), ) arguments: list[cst.Arg] = [] if description is not None: arguments.append(_get_argument("description", description.value)) federation_arguments = _get_federation_arguments(directives, imports) # and has any directive that is a federation directive if is_apollo_federation and federation_arguments: decorator = cst.Attribute( value=cst.Attribute( value=cst.Name("strawberry"), attr=cst.Name("federation"), ), attr=cst.Name(type_), ) arguments.extend(federation_arguments) if arguments: decorator = cst.Call( func=decorator, args=arguments, ) return cst.Decorator( decorator=decorator, ) def _get_class_definition( definition: ObjectTypeDefinitionNode | ObjectTypeExtensionNode | InterfaceTypeDefinitionNode | InputObjectTypeDefinitionNode, is_apollo_federation: bool, imports: set[Import], ) -> Definition: decorator = _get_strawberry_decorator(definition, is_apollo_federation, imports) interfaces = ( [interface.name.value for interface in definition.interfaces] if isinstance( definition, (ObjectTypeDefinitionNode, InterfaceTypeDefinitionNode) ) and definition.interfaces else [] ) class_definition = cst.ClassDef( name=cst.Name(definition.name.value), body=cst.IndentedBlock( body=[ _get_field(field, is_apollo_federation, imports) for field in definition.fields ] ), bases=[cst.Arg(cst.Name(interface)) for interface in interfaces], decorators=[decorator], ) return Definition(class_definition, interfaces, definition.name.value) def _get_enum_value(enum_value: EnumValueDefinitionNode) -> cst.SimpleStatementLine: name = enum_value.name.value return cst.SimpleStatementLine( body=[ cst.Assign( targets=[cst.AssignTarget(cst.Name(name))], value=cst.SimpleString(f'"{name}"'), ) ] ) def _get_enum_definition(definition: EnumTypeDefinitionNode) -> Definition: decorator = cst.Decorator( decorator=cst.Attribute( value=cst.Name("strawberry"), attr=cst.Name("enum"), ), ) class_definition = cst.ClassDef( name=cst.Name(definition.name.value), bases=[cst.Arg(cst.Name("Enum"))], body=cst.IndentedBlock( body=[_get_enum_value(value) for value in definition.values] ), decorators=[decorator], ) return Definition( class_definition, [], definition.name.value, ) def _get_schema_definition( root_query_name: str | None, root_mutation_name: str | None, root_subscription_name: str | None, is_apollo_federation: bool, ) -> cst.SimpleStatementLine | None: if not any([root_query_name, root_mutation_name, root_subscription_name]): return None args: list[cst.Arg] = [] def _get_arg(name: str, value: str) -> cst.Arg: return cst.Arg( keyword=cst.Name(name), value=cst.Name(value), equal=cst.AssignEqual(cst.SimpleWhitespace(""), cst.SimpleWhitespace("")), ) if root_query_name: args.append(_get_arg("query", root_query_name)) if root_mutation_name: args.append(_get_arg("mutation", root_mutation_name)) if root_subscription_name: args.append(_get_arg("subscription", root_subscription_name)) # Federation 2 is now always enabled for federation schemas if is_apollo_federation: schema_call = cst.Call( func=cst.Attribute( value=cst.Attribute( value=cst.Name(value="strawberry"), attr=cst.Name(value="federation"), ), attr=cst.Name(value="Schema"), ), args=args, ) else: schema_call = cst.Call( func=cst.Attribute( value=cst.Name("strawberry"), attr=cst.Name("Schema"), ), args=args, ) return cst.SimpleStatementLine( body=[ cst.Assign( targets=[cst.AssignTarget(cst.Name("schema"))], value=schema_call, ) ] ) @dataclasses.dataclass(frozen=True) class Definition: code: cst.CSTNode dependencies: list[str] name: str def _get_union_definition(definition: UnionTypeDefinitionNode) -> Definition: name = definition.name.value types = cst.parse_expression( " | ".join([type_.name.value for type_ in definition.types]) ) simple_statement = cst.SimpleStatementLine( body=[ cst.Assign( targets=[cst.AssignTarget(cst.Name(name))], value=cst.Subscript( value=cst.Name("Annotated"), slice=[ cst.SubscriptElement(slice=cst.Index(types)), cst.SubscriptElement( slice=cst.Index( cst.Call( cst.Attribute( value=cst.Name("strawberry"), attr=cst.Name("union"), ), args=[_get_argument("name", name)], ) ) ), ], ), ) ] ) return Definition( simple_statement, [], definition.name.value, ) def _get_scalar_definition( definition: ScalarTypeDefinitionNode, imports: set[Import] ) -> Definition | None: name = definition.name.value if name == "Date": imports.add(Import(module="datetime", imports=("date",))) return None if name == "Time": imports.add(Import(module="datetime", imports=("time",))) return None if name == "DateTime": imports.add(Import(module="datetime", imports=("datetime",))) return None if name == "Decimal": imports.add(Import(module="decimal", imports=("Decimal",))) return None if name == "UUID": imports.add(Import(module="uuid", imports=("UUID",))) return None if name == "JSON": return None description = definition.description.value if definition.description else None specified_by_url = None for directive in definition.directives: if directive.name.value == "specifiedBy": arg = directive.arguments[0] assert isinstance(arg.value, StringValueNode) specified_by_url = arg.value.value imports.add(Import(module="typing", imports=("NewType",))) identity_lambda = cst.Lambda( body=cst.Name("v"), params=cst.Parameters( params=[cst.Param(cst.Name("v"))], ), ) additional_args: list[cst.Arg | None] = [ _get_argument("description", description) if description else None, _get_argument("specified_by_url", specified_by_url) if specified_by_url else None, cst.Arg( keyword=cst.Name("serialize"), value=identity_lambda, equal=cst.AssignEqual(cst.SimpleWhitespace(""), cst.SimpleWhitespace("")), ), cst.Arg( keyword=cst.Name("parse_value"), value=identity_lambda, equal=cst.AssignEqual(cst.SimpleWhitespace(""), cst.SimpleWhitespace("")), ), ] statement_definition = cst.SimpleStatementLine( body=[ cst.Assign( targets=[cst.AssignTarget(cst.Name(name))], value=cst.Call( func=cst.Attribute( value=cst.Name("strawberry"), attr=cst.Name("scalar"), ), args=[ cst.Arg( cst.Call( func=cst.Name("NewType"), args=[ cst.Arg(cst.SimpleString(f'"{name}"')), cst.Arg(cst.Name("object")), ], ) ), *filter(None, additional_args), ], ), ) ] ) return Definition(statement_definition, [], name=definition.name.value) def codegen(schema: str) -> str: document = parse(schema) definitions: dict[str, Definition] = {} root_query_name: str | None = None root_mutation_name: str | None = None root_subscription_name: str | None = None imports: set[Import] = { Import(module=None, imports=("strawberry",)), } # when we encounter a extend schema @link ..., we check if is an apollo federation schema # and we use this variable to keep track of it, but at the moment the assumption is that # the schema extension is always done at the top, this might not be the case all the # time is_apollo_federation = False for graphql_definition in document.definitions: definition: Definition | None = None if isinstance( graphql_definition, ( ObjectTypeDefinitionNode, InterfaceTypeDefinitionNode, InputObjectTypeDefinitionNode, ObjectTypeExtensionNode, ), ): definition = _get_class_definition( graphql_definition, is_apollo_federation, imports ) elif isinstance(graphql_definition, EnumTypeDefinitionNode): imports.add(Import(module="enum", imports=("Enum",))) definition = _get_enum_definition(graphql_definition) elif isinstance(graphql_definition, SchemaDefinitionNode): for operation_type_definition in graphql_definition.operation_types: if operation_type_definition.operation == OperationType.QUERY: root_query_name = operation_type_definition.type.name.value elif operation_type_definition.operation == OperationType.MUTATION: root_mutation_name = operation_type_definition.type.name.value elif operation_type_definition.operation == OperationType.SUBSCRIPTION: root_subscription_name = operation_type_definition.type.name.value else: raise NotImplementedError( f"Unknown operation {operation_type_definition.operation}" ) elif isinstance(graphql_definition, UnionTypeDefinitionNode): imports.add(Import(module="typing", imports=("Annotated",))) definition = _get_union_definition(graphql_definition) elif isinstance(graphql_definition, ScalarTypeDefinitionNode): definition = _get_scalar_definition(graphql_definition, imports) elif isinstance(graphql_definition, SchemaExtensionNode): is_apollo_federation = any( _is_federation_link_directive(directive) for directive in graphql_definition.directives ) else: raise NotImplementedError(f"Unknown definition {definition}") if definition is not None: definitions[definition.name] = definition if root_query_name is None: root_query_name = "Query" if "Query" in definitions else None if root_mutation_name is None: root_mutation_name = "Mutation" if "Mutation" in definitions else None if root_subscription_name is None: root_subscription_name = ( "Subscription" if "Subscription" in definitions else None ) schema_definition = _get_schema_definition( root_query_name=root_query_name, root_mutation_name=root_mutation_name, root_subscription_name=root_subscription_name, is_apollo_federation=is_apollo_federation, ) if schema_definition: definitions["Schema"] = Definition(schema_definition, [], "schema") body: list[cst.CSTNode] = [ cst.SimpleStatementLine(body=[import_.to_cst()]) for import_ in sorted(imports, key=lambda i: (i.module or "", i.imports)) ] # DAG to sort definitions based on dependencies graph = {name: definition.dependencies for name, definition in definitions.items()} ts = TopologicalSorter(graph) for definition_name in tuple(ts.static_order()): definition = definitions[definition_name] body.append(cst.EmptyLine()) body.append(definition.code) module = cst.Module(body=body) # type: ignore return module.code __all__ = ["codegen"] strawberry-graphql-0.287.0/strawberry/schema_directive.py000066400000000000000000000037511511033167500236140ustar00rootroot00000000000000import dataclasses from collections.abc import Callable from enum import Enum from typing import TypeVar from typing_extensions import dataclass_transform from strawberry.types.field import StrawberryField, field from strawberry.types.object_type import _wrap_dataclass from strawberry.types.type_resolver import _get_fields from .directive import directive_field class Location(Enum): SCHEMA = "schema" SCALAR = "scalar" OBJECT = "object" FIELD_DEFINITION = "field definition" ARGUMENT_DEFINITION = "argument definition" INTERFACE = "interface" UNION = "union" ENUM = "enum" ENUM_VALUE = "enum value" INPUT_OBJECT = "input object" INPUT_FIELD_DEFINITION = "input field definition" @dataclasses.dataclass class StrawberrySchemaDirective: python_name: str graphql_name: str | None locations: list[Location] fields: list["StrawberryField"] description: str | None = None repeatable: bool = False print_definition: bool = True origin: type | None = None T = TypeVar("T", bound=type) @dataclass_transform( order_default=True, kw_only_default=True, field_specifiers=(directive_field, field, StrawberryField), ) def schema_directive( *, locations: list[Location], description: str | None = None, name: str | None = None, repeatable: bool = False, print_definition: bool = True, ) -> Callable[[T], T]: def _wrap(cls: T) -> T: cls = _wrap_dataclass(cls) # type: ignore fields = _get_fields(cls, {}) cls.__strawberry_directive__ = StrawberrySchemaDirective( # type: ignore[attr-defined] python_name=cls.__name__, graphql_name=name, locations=locations, description=description, repeatable=repeatable, fields=fields, print_definition=print_definition, origin=cls, ) return cls return _wrap __all__ = ["Location", "StrawberrySchemaDirective", "schema_directive"] strawberry-graphql-0.287.0/strawberry/schema_directives.py000066400000000000000000000002571511033167500237750ustar00rootroot00000000000000from strawberry.schema_directive import Location, schema_directive @schema_directive(locations=[Location.INPUT_OBJECT], name="oneOf") class OneOf: ... __all__ = ["OneOf"] strawberry-graphql-0.287.0/strawberry/static/000077500000000000000000000000001511033167500212255ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/static/apollo-sandbox.html000066400000000000000000000017151511033167500250410ustar00rootroot00000000000000 Strawberry Apollo Sandbox
strawberry-graphql-0.287.0/strawberry/static/graphiql.html000066400000000000000000000102411511033167500237200ustar00rootroot00000000000000 Strawberry GraphiQL
Loading...
strawberry-graphql-0.287.0/strawberry/static/pathfinder.html000066400000000000000000000030131511033167500242340ustar00rootroot00000000000000 Strawberry Pathfinder
strawberry-graphql-0.287.0/strawberry/streamable.py000066400000000000000000000011431511033167500224260ustar00rootroot00000000000000from collections.abc import AsyncGenerator from typing import Annotated, TypeVar class StrawberryStreamable: ... T = TypeVar("T") Streamable = Annotated[AsyncGenerator[T, None], StrawberryStreamable()] """Represents a list that can be streamed using @stream. Example: ```python import strawberry @strawberry.type class Comment: id: strawberry.ID content: str @strawberry.type class Article: @strawberry.field @staticmethod async def comments() -> strawberry.Streamable[Comment]: for comment in fetch_comments(): yield comment ``` """ __all__ = ["Streamable"] strawberry-graphql-0.287.0/strawberry/subscriptions/000077500000000000000000000000001511033167500226455ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/subscriptions/__init__.py000066400000000000000000000002521511033167500247550ustar00rootroot00000000000000GRAPHQL_TRANSPORT_WS_PROTOCOL = "graphql-transport-ws" GRAPHQL_WS_PROTOCOL = "graphql-ws" __all__ = [ "GRAPHQL_TRANSPORT_WS_PROTOCOL", "GRAPHQL_WS_PROTOCOL", ] strawberry-graphql-0.287.0/strawberry/subscriptions/protocols/000077500000000000000000000000001511033167500246715ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/subscriptions/protocols/__init__.py000066400000000000000000000000001511033167500267700ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/subscriptions/protocols/graphql_transport_ws/000077500000000000000000000000001511033167500311545ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/subscriptions/protocols/graphql_transport_ws/__init__.py000066400000000000000000000002121511033167500332600ustar00rootroot00000000000000# Code 4406 is "Subprotocol not acceptable" WS_4406_PROTOCOL_NOT_ACCEPTABLE = 4406 __all__ = [ "WS_4406_PROTOCOL_NOT_ACCEPTABLE", ] strawberry-graphql-0.287.0/strawberry/subscriptions/protocols/graphql_transport_ws/handlers.py000066400000000000000000000353551511033167500333410ustar00rootroot00000000000000from __future__ import annotations import asyncio import logging from contextlib import suppress from typing import ( TYPE_CHECKING, Any, Generic, cast, ) from graphql import GraphQLError, GraphQLSyntaxError, parse from strawberry.exceptions import ConnectionRejectionError from strawberry.http.exceptions import ( NonJsonMessageReceived, NonTextMessageReceived, WebSocketDisconnected, ) from strawberry.http.typevars import Context, RootValue from strawberry.schema.exceptions import CannotGetOperationTypeError from strawberry.subscriptions.protocols.graphql_transport_ws.types import ( CompleteMessage, ConnectionInitMessage, Message, NextMessagePayload, PingMessage, PongMessage, SubscribeMessage, ) from strawberry.types import ExecutionResult from strawberry.types.execution import PreExecutionError from strawberry.types.graphql import OperationType from strawberry.types.unset import UnsetType from strawberry.utils.operation import get_operation_type if TYPE_CHECKING: from datetime import timedelta from strawberry.http.async_base_view import AsyncBaseHTTPView, AsyncWebSocketAdapter from strawberry.schema import BaseSchema from strawberry.schema.schema import SubscriptionResult class BaseGraphQLTransportWSHandler(Generic[Context, RootValue]): task_logger: logging.Logger = logging.getLogger("strawberry.ws.task") def __init__( self, view: AsyncBaseHTTPView[Any, Any, Any, Any, Any, Context, RootValue], websocket: AsyncWebSocketAdapter, context: Context, root_value: RootValue | None, schema: BaseSchema, connection_init_wait_timeout: timedelta, ) -> None: self.view = view self.websocket = websocket self.context = context self.root_value = root_value self.schema = schema self.connection_init_wait_timeout = connection_init_wait_timeout self.connection_init_timeout_task: asyncio.Task | None = None self.connection_init_received = False self.connection_acknowledged = False self.connection_timed_out = False self.operations: dict[str, Operation[Context, RootValue]] = {} self.completed_tasks: list[asyncio.Task] = [] async def handle(self) -> None: self.on_request_accepted() try: try: async for message in self.websocket.iter_json(): await self.handle_message(cast("Message", message)) except NonTextMessageReceived: await self.handle_invalid_message("WebSocket message type must be text") except NonJsonMessageReceived: await self.handle_invalid_message( "WebSocket message must be valid JSON" ) except WebSocketDisconnected: pass finally: await self.shutdown() async def shutdown(self) -> None: if self.connection_init_timeout_task: self.connection_init_timeout_task.cancel() with suppress(asyncio.CancelledError): await self.connection_init_timeout_task for operation_id in list(self.operations.keys()): await self.cleanup_operation(operation_id) await self.reap_completed_tasks() def on_request_accepted(self) -> None: # handle_request should call this once it has sent the # websocket.accept() response to start the timeout. assert not self.connection_init_timeout_task self.connection_init_timeout_task = asyncio.create_task( self.handle_connection_init_timeout() ) async def handle_connection_init_timeout(self) -> None: task = asyncio.current_task() assert task try: delay = self.connection_init_wait_timeout.total_seconds() await asyncio.sleep(delay=delay) if self.connection_init_received: return # pragma: no cover self.connection_timed_out = True reason = "Connection initialisation timeout" await self.websocket.close(code=4408, reason=reason) except Exception as error: # noqa: BLE001 await self.handle_task_exception(error) # pragma: no cover finally: # do not clear self.connection_init_timeout_task # so that unittests can inspect it. self.completed_tasks.append(task) async def handle_task_exception(self, error: Exception) -> None: # pragma: no cover self.task_logger.exception("Exception in worker task", exc_info=error) async def handle_message(self, message: Message) -> None: try: if message["type"] == "connection_init": await self.handle_connection_init(message) elif message["type"] == "ping": await self.handle_ping(message) elif message["type"] == "pong": await self.handle_pong(message) elif message["type"] == "subscribe": await self.handle_subscribe(message) elif message["type"] == "complete": await self.handle_complete(message) else: error_message = f"Unknown message type: {message['type']}" await self.handle_invalid_message(error_message) except KeyError: await self.handle_invalid_message("Failed to parse message") finally: await self.reap_completed_tasks() async def handle_connection_init(self, message: ConnectionInitMessage) -> None: if self.connection_timed_out: # No way to reliably excercise this case during testing return # pragma: no cover if self.connection_init_timeout_task: self.connection_init_timeout_task.cancel() payload = message.get("payload", {}) if not isinstance(payload, dict): await self.websocket.close( code=4400, reason="Invalid connection init payload" ) return if self.connection_init_received: reason = "Too many initialisation requests" await self.websocket.close(code=4429, reason=reason) return self.connection_init_received = True if isinstance(self.context, dict): self.context["connection_params"] = payload elif hasattr(self.context, "connection_params"): self.context.connection_params = payload try: connection_ack_payload = await self.view.on_ws_connect(self.context) except ConnectionRejectionError: await self.websocket.close(code=4403, reason="Forbidden") return if isinstance(connection_ack_payload, UnsetType): await self.send_message({"type": "connection_ack"}) else: await self.send_message( {"type": "connection_ack", "payload": connection_ack_payload} ) self.connection_acknowledged = True async def handle_ping(self, message: PingMessage) -> None: await self.send_message({"type": "pong"}) async def handle_pong(self, message: PongMessage) -> None: pass async def handle_subscribe(self, message: SubscribeMessage) -> None: if not self.connection_acknowledged: await self.websocket.close(code=4401, reason="Unauthorized") return try: graphql_document = parse(message["payload"]["query"]) except GraphQLSyntaxError as exc: await self.websocket.close(code=4400, reason=exc.message) return operation_name = message["payload"].get("operationName") try: operation_type = get_operation_type(graphql_document, operation_name) except RuntimeError: # Unlike in the other protocol implementations, we access the operation type # before executing the operation. Therefore, we don't get a nice # CannotGetOperationTypeError, but rather the underlying RuntimeError. e = CannotGetOperationTypeError(operation_name) await self.websocket.close( code=4400, reason=e.as_http_error_reason(), ) return if message["id"] in self.operations: reason = f"Subscriber for {message['id']} already exists" await self.websocket.close(code=4409, reason=reason) return operation = Operation( self, message["id"], operation_type, message["payload"]["query"], message["payload"].get("variables"), message["payload"].get("operationName"), ) operation.task = asyncio.create_task(self.run_operation(operation)) self.operations[message["id"]] = operation async def run_operation(self, operation: Operation[Context, RootValue]) -> None: """The operation task's top level method. Cleans-up and de-registers the operation once it is done.""" result_source: ExecutionResult | SubscriptionResult try: # Get an AsyncGenerator yielding the results if operation.operation_type == OperationType.SUBSCRIPTION: result_source = await self.schema.subscribe( query=operation.query, variable_values=operation.variables, operation_name=operation.operation_name, context_value=self.context, root_value=self.root_value, ) else: result_source = await self.schema.execute( query=operation.query, variable_values=operation.variables, context_value=self.context, root_value=self.root_value, operation_name=operation.operation_name, ) # TODO: maybe change PreExecutionError to an exception that can be caught if isinstance(result_source, ExecutionResult): if isinstance(result_source, PreExecutionError): assert result_source.errors await operation.send_initial_errors(result_source.errors) else: await operation.send_next(result_source) else: is_first_result = True async for result in result_source: if is_first_result and isinstance(result, PreExecutionError): assert result.errors await operation.send_initial_errors(result.errors) break await operation.send_next(result) is_first_result = False await operation.send_operation_message( CompleteMessage(id=operation.id, type="complete") ) except Exception as error: # pragma: no cover await self.handle_task_exception(error) with suppress(Exception): await operation.send_operation_message( {"id": operation.id, "type": "complete"} ) self.operations.pop(operation.id, None) raise finally: # add this task to a list to be reaped later task = asyncio.current_task() assert task is not None self.completed_tasks.append(task) def forget_id(self, id: str) -> None: # de-register the operation id making it immediately available # for re-use del self.operations[id] async def handle_complete(self, message: CompleteMessage) -> None: await self.cleanup_operation(operation_id=message["id"]) async def handle_invalid_message(self, error_message: str) -> None: await self.websocket.close(code=4400, reason=error_message) async def send_message(self, message: Message) -> None: await self.websocket.send_json(message) async def cleanup_operation(self, operation_id: str) -> None: if operation_id not in self.operations: return operation = self.operations.pop(operation_id) assert operation.task operation.task.cancel() # do not await the task here, lest we block the main # websocket handler Task. async def reap_completed_tasks(self) -> None: """Await tasks that have completed.""" tasks, self.completed_tasks = self.completed_tasks, [] for task in tasks: with suppress(BaseException): await task class Operation(Generic[Context, RootValue]): """A class encapsulating a single operation with its id. Helps enforce protocol state transition.""" __slots__ = [ "completed", "handler", "id", "operation_name", "operation_type", "query", "task", "variables", ] def __init__( self, handler: BaseGraphQLTransportWSHandler[Context, RootValue], id: str, operation_type: OperationType, query: str, variables: dict[str, object] | None, operation_name: str | None, ) -> None: self.handler = handler self.id = id self.operation_type = operation_type self.query = query self.variables = variables self.operation_name = operation_name self.completed = False self.task: asyncio.Task | None = None async def send_operation_message(self, message: Message) -> None: if self.completed: return if message["type"] == "complete" or message["type"] == "error": self.completed = True # de-register the operation _before_ sending the final message self.handler.forget_id(self.id) await self.handler.send_message(message) async def send_initial_errors(self, errors: list[GraphQLError]) -> None: # Initial errors see https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md#error # "This can occur before execution starts, # usually due to validation errors, or during the execution of the request" await self.send_operation_message( { "id": self.id, "type": "error", "payload": [err.formatted for err in errors], } ) async def send_next(self, execution_result: ExecutionResult) -> None: next_payload: NextMessagePayload = {"data": execution_result.data} if execution_result.errors: next_payload["errors"] = [err.formatted for err in execution_result.errors] if execution_result.extensions: next_payload["extensions"] = execution_result.extensions await self.send_operation_message( {"id": self.id, "type": "next", "payload": next_payload} ) __all__ = ["BaseGraphQLTransportWSHandler", "Operation"] strawberry-graphql-0.287.0/strawberry/subscriptions/protocols/graphql_transport_ws/types.py000066400000000000000000000041311511033167500326710ustar00rootroot00000000000000from typing import Literal, TypeAlias, TypedDict from typing_extensions import NotRequired from graphql import GraphQLFormattedError class ConnectionInitMessage(TypedDict): """Direction: Client -> Server.""" type: Literal["connection_init"] payload: NotRequired[dict[str, object] | None] class ConnectionAckMessage(TypedDict): """Direction: Server -> Client.""" type: Literal["connection_ack"] payload: NotRequired[dict[str, object] | None] class PingMessage(TypedDict): """Direction: bidirectional.""" type: Literal["ping"] payload: NotRequired[dict[str, object] | None] class PongMessage(TypedDict): """Direction: bidirectional.""" type: Literal["pong"] payload: NotRequired[dict[str, object] | None] class SubscribeMessagePayload(TypedDict): operationName: NotRequired[str | None] query: str variables: NotRequired[dict[str, object] | None] extensions: NotRequired[dict[str, object] | None] class SubscribeMessage(TypedDict): """Direction: Client -> Server.""" id: str type: Literal["subscribe"] payload: SubscribeMessagePayload class NextMessagePayload(TypedDict): errors: NotRequired[list[GraphQLFormattedError]] data: NotRequired[dict[str, object] | None] extensions: NotRequired[dict[str, object]] class NextMessage(TypedDict): """Direction: Server -> Client.""" id: str type: Literal["next"] payload: NextMessagePayload class ErrorMessage(TypedDict): """Direction: Server -> Client.""" id: str type: Literal["error"] payload: list[GraphQLFormattedError] class CompleteMessage(TypedDict): """Direction: bidirectional.""" id: str type: Literal["complete"] Message: TypeAlias = ( ConnectionInitMessage | ConnectionAckMessage | PingMessage | PongMessage | SubscribeMessage | NextMessage | ErrorMessage | CompleteMessage ) __all__ = [ "CompleteMessage", "ConnectionAckMessage", "ConnectionInitMessage", "ErrorMessage", "Message", "NextMessage", "PingMessage", "PongMessage", "SubscribeMessage", ] strawberry-graphql-0.287.0/strawberry/subscriptions/protocols/graphql_ws/000077500000000000000000000000001511033167500270405ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/subscriptions/protocols/graphql_ws/__init__.py000066400000000000000000000000001511033167500311370ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/subscriptions/protocols/graphql_ws/handlers.py000066400000000000000000000203501511033167500312120ustar00rootroot00000000000000from __future__ import annotations import asyncio from collections.abc import AsyncGenerator from contextlib import suppress from typing import ( TYPE_CHECKING, Any, Generic, cast, ) from strawberry.exceptions import ConnectionRejectionError from strawberry.http.exceptions import NonTextMessageReceived, WebSocketDisconnected from strawberry.http.typevars import Context, RootValue from strawberry.schema.exceptions import CannotGetOperationTypeError from strawberry.subscriptions.protocols.graphql_ws.types import ( CompleteMessage, ConnectionInitMessage, ConnectionTerminateMessage, DataMessage, ErrorMessage, OperationMessage, StartMessage, StopMessage, ) from strawberry.types.execution import ExecutionResult, PreExecutionError from strawberry.types.unset import UnsetType if TYPE_CHECKING: from collections.abc import AsyncGenerator from strawberry.http.async_base_view import AsyncBaseHTTPView, AsyncWebSocketAdapter from strawberry.schema import BaseSchema class BaseGraphQLWSHandler(Generic[Context, RootValue]): def __init__( self, view: AsyncBaseHTTPView[Any, Any, Any, Any, Any, Context, RootValue], websocket: AsyncWebSocketAdapter, context: Context, root_value: RootValue | None, schema: BaseSchema, keep_alive: bool, keep_alive_interval: float | None, ) -> None: self.view = view self.websocket = websocket self.context = context self.root_value = root_value self.schema = schema self.keep_alive = keep_alive self.keep_alive_interval = keep_alive_interval self.keep_alive_task: asyncio.Task | None = None self.subscriptions: dict[str, AsyncGenerator] = {} self.tasks: dict[str, asyncio.Task] = {} async def handle(self) -> None: try: try: async for message in self.websocket.iter_json( ignore_parsing_errors=True ): await self.handle_message(cast("OperationMessage", message)) except NonTextMessageReceived: await self.websocket.close( code=1002, reason="WebSocket message type must be text" ) except WebSocketDisconnected: pass finally: if self.keep_alive_task: self.keep_alive_task.cancel() with suppress(BaseException): await self.keep_alive_task await self.cleanup() async def handle_message( self, message: OperationMessage, ) -> None: if message["type"] == "connection_init": await self.handle_connection_init(message) elif message["type"] == "connection_terminate": await self.handle_connection_terminate(message) elif message["type"] == "start": await self.handle_start(message) elif message["type"] == "stop": await self.handle_stop(message) async def handle_connection_init(self, message: ConnectionInitMessage) -> None: payload = message.get("payload") if payload is not None and not isinstance(payload, dict): await self.send_message({"type": "connection_error"}) await self.websocket.close(code=1000, reason="") return if isinstance(self.context, dict): self.context["connection_params"] = payload elif hasattr(self.context, "connection_params"): self.context.connection_params = payload try: connection_ack_payload = await self.view.on_ws_connect(self.context) except ConnectionRejectionError as e: await self.send_message({"type": "connection_error", "payload": e.payload}) await self.websocket.close(code=1011, reason="") return if ( isinstance(connection_ack_payload, UnsetType) or connection_ack_payload is None ): await self.send_message({"type": "connection_ack"}) else: await self.send_message( {"type": "connection_ack", "payload": connection_ack_payload} ) if self.keep_alive: keep_alive_handler = self.handle_keep_alive() self.keep_alive_task = asyncio.create_task(keep_alive_handler) async def handle_connection_terminate( self, message: ConnectionTerminateMessage ) -> None: await self.websocket.close(code=1000, reason="") async def handle_start(self, message: StartMessage) -> None: operation_id = message["id"] payload = message["payload"] query = payload["query"] operation_name = payload.get("operationName") variables = payload.get("variables") result_handler = self.handle_async_results( operation_id, query, operation_name, variables ) self.tasks[operation_id] = asyncio.create_task(result_handler) async def handle_stop(self, message: StopMessage) -> None: operation_id = message["id"] await self.cleanup_operation(operation_id) async def handle_keep_alive(self) -> None: assert self.keep_alive_interval while True: await self.send_message({"type": "ka"}) await asyncio.sleep(self.keep_alive_interval) async def handle_async_results( self, operation_id: str, query: str, operation_name: str | None, variables: dict[str, object] | None, ) -> None: try: result_source = await self.schema.subscribe( query=query, variable_values=variables, operation_name=operation_name, context_value=self.context, root_value=self.root_value, ) self.subscriptions[operation_id] = result_source is_first_result = True async for result in result_source: if is_first_result and isinstance(result, PreExecutionError): assert result.errors await self.send_message( ErrorMessage( type="error", id=operation_id, payload=result.errors[0].formatted, ) ) return await self.send_data_message(result, operation_id) is_first_result = False await self.send_message(CompleteMessage(type="complete", id=operation_id)) except CannotGetOperationTypeError as e: await self.send_message( ErrorMessage( type="error", id=operation_id, payload={"message": e.as_http_error_reason()}, ) ) except asyncio.CancelledError: await self.send_message(CompleteMessage(type="complete", id=operation_id)) async def cleanup_operation(self, operation_id: str) -> None: if operation_id in self.subscriptions: with suppress(RuntimeError): await self.subscriptions[operation_id].aclose() del self.subscriptions[operation_id] self.tasks[operation_id].cancel() with suppress(BaseException): await self.tasks[operation_id] del self.tasks[operation_id] async def cleanup(self) -> None: for operation_id in list(self.tasks.keys()): await self.cleanup_operation(operation_id) async def send_data_message( self, execution_result: ExecutionResult, operation_id: str ) -> None: data_message: DataMessage = { "type": "data", "id": operation_id, "payload": {"data": execution_result.data}, } if execution_result.errors: data_message["payload"]["errors"] = [ err.formatted for err in execution_result.errors ] if execution_result.extensions: data_message["payload"]["extensions"] = execution_result.extensions await self.send_message(data_message) async def send_message(self, message: OperationMessage) -> None: await self.websocket.send_json(message) __all__ = ["BaseGraphQLWSHandler"] strawberry-graphql-0.287.0/strawberry/subscriptions/protocols/graphql_ws/types.py000066400000000000000000000037451511033167500305670ustar00rootroot00000000000000from typing import Literal, TypeAlias, TypedDict from typing_extensions import NotRequired from graphql import GraphQLFormattedError class ConnectionInitMessage(TypedDict): type: Literal["connection_init"] payload: NotRequired[dict[str, object]] class StartMessagePayload(TypedDict): query: str variables: NotRequired[dict[str, object]] operationName: NotRequired[str] class StartMessage(TypedDict): type: Literal["start"] id: str payload: StartMessagePayload class StopMessage(TypedDict): type: Literal["stop"] id: str class ConnectionTerminateMessage(TypedDict): type: Literal["connection_terminate"] class ConnectionErrorMessage(TypedDict): type: Literal["connection_error"] payload: NotRequired[dict[str, object]] class ConnectionAckMessage(TypedDict): type: Literal["connection_ack"] payload: NotRequired[dict[str, object]] class DataMessagePayload(TypedDict): data: object errors: NotRequired[list[GraphQLFormattedError]] # Non-standard field: extensions: NotRequired[dict[str, object]] class DataMessage(TypedDict): type: Literal["data"] id: str payload: DataMessagePayload class ErrorMessage(TypedDict): type: Literal["error"] id: str payload: GraphQLFormattedError class CompleteMessage(TypedDict): type: Literal["complete"] id: str class ConnectionKeepAliveMessage(TypedDict): type: Literal["ka"] OperationMessage: TypeAlias = ( ConnectionInitMessage | StartMessage | StopMessage | ConnectionTerminateMessage | ConnectionErrorMessage | ConnectionAckMessage | DataMessage | ErrorMessage | CompleteMessage | ConnectionKeepAliveMessage ) __all__ = [ "CompleteMessage", "ConnectionAckMessage", "ConnectionErrorMessage", "ConnectionInitMessage", "ConnectionKeepAliveMessage", "ConnectionTerminateMessage", "DataMessage", "ErrorMessage", "OperationMessage", "StartMessage", "StopMessage", ] strawberry-graphql-0.287.0/strawberry/test/000077500000000000000000000000001511033167500207155ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/test/__init__.py000066400000000000000000000001631511033167500230260ustar00rootroot00000000000000from .client import BaseGraphQLTestClient, Body, Response __all__ = ["BaseGraphQLTestClient", "Body", "Response"] strawberry-graphql-0.287.0/strawberry/test/client.py000066400000000000000000000143231511033167500225500ustar00rootroot00000000000000from __future__ import annotations import json import warnings from abc import ABC, abstractmethod from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Literal from typing_extensions import TypedDict if TYPE_CHECKING: from collections.abc import Coroutine from graphql import GraphQLFormattedError @dataclass class Response: errors: list[GraphQLFormattedError] | None data: dict[str, object] | None extensions: dict[str, object] | None class Body(TypedDict, total=False): query: str variables: dict[str, object] | None class BaseGraphQLTestClient(ABC): def __init__( self, client: Any, url: str = "/graphql/", ) -> None: self._client = client self.url = url def query( self, query: str, variables: dict[str, Any] | None = None, headers: dict[str, object] | None = None, asserts_errors: bool | None = None, files: dict[str, object] | None = None, assert_no_errors: bool | None = True, ) -> Coroutine[Any, Any, Response] | Response: body = self._build_body(query, variables, files) resp = self.request(body, headers, files) data = self._decode(resp, type="multipart" if files else "json") response = Response( errors=data.get("errors"), data=data.get("data"), extensions=data.get("extensions"), ) if asserts_errors is not None: warnings.warn( "The `asserts_errors` argument has been renamed to `assert_no_errors`", DeprecationWarning, stacklevel=2, ) assert_no_errors = ( assert_no_errors if asserts_errors is None else asserts_errors ) if assert_no_errors: assert response.errors is None return response @abstractmethod def request( self, body: dict[str, object], headers: dict[str, object] | None = None, files: dict[str, object] | None = None, ) -> Any: raise NotImplementedError def _build_body( self, query: str, variables: dict[str, Any] | None = None, files: dict[str, object] | None = None, ) -> dict[str, object]: body: dict[str, object] = {"query": query} if variables: body["variables"] = variables if files: assert variables is not None assert files is not None file_map = BaseGraphQLTestClient._build_multipart_file_map(variables, files) body = { "operations": json.dumps(body), "map": json.dumps(file_map), **files, } return body @staticmethod def _build_multipart_file_map( variables: dict[str, Any], files: dict[str, object] ) -> dict[str, list[str]]: """Creates the file mapping between the variables and the files objects passed as key arguments. Args: variables: A dictionary with the variables that are going to be passed to the query. files: A dictionary with the files that are going to be passed to the query. Example usages: ```python _build_multipart_file_map(variables={"textFile": None}, files={"textFile": f}) # {"textFile": ["variables.textFile"]} ``` If the variable is a list we have to enumerate files in the mapping ```python _build_multipart_file_map( variables={"files": [None, None]}, files={"file1": file1, "file2": file2}, ) # {"file1": ["variables.files.0"], "file2": ["variables.files.1"]} ``` If `variables` contains another keyword (a folder) we must include that keyword in the mapping ```python _build_multipart_file_map( variables={"folder": {"files": [None, None]}}, files={"file1": file1, "file2": file2}, ) # { # "file1": ["variables.files.folder.files.0"], # "file2": ["variables.files.folder.files.1"] # } ``` If `variables` includes both a list of files and other single values, we must map them accordingly ```python _build_multipart_file_map( variables={"files": [None, None], "textFile": None}, files={"file1": file1, "file2": file2, "textFile": file3}, ) # { # "file1": ["variables.files.0"], # "file2": ["variables.files.1"], # "textFile": ["variables.textFile"], # } ``` """ map: dict[str, list[str]] = {} for key, values in variables.items(): reference = key variable_values = values # In case of folders the variables will look like # `{"folder": {"files": ...]}}` if isinstance(values, dict): folder_key = next(iter(values.keys())) reference += f".{folder_key}" # the list of file is inside the folder keyword variable_values = variable_values[folder_key] # If the variable is an array of files we must number the keys if isinstance(variable_values, list): # copying `files` as when we map a file we must discard from the dict _kwargs = files.copy() for index, _ in enumerate(variable_values): k = next(iter(_kwargs.keys())) _kwargs.pop(k) map.setdefault(k, []) map[k].append(f"variables.{reference}.{index}") else: map[key] = [f"variables.{reference}"] # Variables can be mixed files and other data, we don't want to map non-files # vars so we need to remove them, we can't remove them before # because they can be part of a list of files or folder return {k: v for k, v in map.items() if k in files} def _decode(self, response: Any, type: Literal["multipart", "json"]) -> Any: if type == "multipart": return json.loads(response.content.decode()) return response.json() __all__ = ["BaseGraphQLTestClient", "Body", "Response"] strawberry-graphql-0.287.0/strawberry/tools/000077500000000000000000000000001511033167500210765ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/tools/__init__.py000066400000000000000000000001771511033167500232140ustar00rootroot00000000000000from .create_type import create_type from .merge_types import merge_types __all__ = [ "create_type", "merge_types", ] strawberry-graphql-0.287.0/strawberry/tools/create_type.py000066400000000000000000000043671511033167500237660ustar00rootroot00000000000000import types from collections.abc import Sequence import strawberry from strawberry.types.field import StrawberryField def create_type( name: str, fields: list[StrawberryField], is_input: bool = False, is_interface: bool = False, description: str | None = None, directives: Sequence[object] | None = (), extend: bool = False, ) -> type: """Create a Strawberry type from a list of StrawberryFields. Args: name: The GraphQL name of the type. fields: The fields of the type. is_input: Whether the type is an input type. is_interface: Whether the type is an interface. description: The GraphQL description of the type. directives: The directives to attach to the type. extend: Whether the type is an extension. Example usage: ```python import strawberry @strawberry.field def hello(info) -> str: return "World" Query = create_type(name="Query", fields=[hello]) ``` """ if not fields: raise ValueError(f'Can\'t create type "{name}" with no fields') namespace = {} annotations = {} for field in fields: if not isinstance(field, StrawberryField): raise TypeError("Field is not an instance of StrawberryField") # Fields created using `strawberry.field` without a resolver don't have a # `python_name`. In that case, we fall back to the field's `graphql_name` # set via the `name` argument passed to `strawberry.field`. field_name = field.python_name or field.graphql_name if field_name is None: raise ValueError( "Field doesn't have a name. Fields passed to " "`create_type` must define a name by passing the " "`name` argument to `strawberry.field`." ) namespace[field_name] = field annotations[field_name] = field.type namespace["__annotations__"] = annotations # type: ignore cls = types.new_class(name, (), {}, lambda ns: ns.update(namespace)) return strawberry.type( cls, is_input=is_input, is_interface=is_interface, description=description, directives=directives, extend=extend, ) __all__ = ["create_type"] strawberry-graphql-0.287.0/strawberry/tools/merge_types.py000066400000000000000000000017771511033167500240070ustar00rootroot00000000000000import warnings from collections import Counter from itertools import chain import strawberry from strawberry.types.base import has_object_definition def merge_types(name: str, types: tuple[type, ...]) -> type: """Merge multiple Strawberry types into one. For example, given two queries `A` and `B`, one can merge them into a super type as follows: merge_types("SuperQuery", (B, A)) This is essentially the same as: class SuperQuery(B, A): ... """ if not types: raise ValueError("Can't merge types if none are supplied") fields = chain( *(t.__strawberry_definition__.fields for t in types if has_object_definition(t)) ) counter = Counter(f.name for f in fields) dupes = [f for f, c in counter.most_common() if c > 1] if dupes: warnings.warn( "{} has overridden fields: {}".format(name, ", ".join(dupes)), stacklevel=2 ) return strawberry.type(type(name, types, {})) __all__ = ["merge_types"] strawberry-graphql-0.287.0/strawberry/types/000077500000000000000000000000001511033167500211025ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/types/__init__.py000066400000000000000000000005371511033167500232200ustar00rootroot00000000000000from .base import get_object_definition, has_object_definition from .execution import ExecutionContext, ExecutionResult, SubscriptionExecutionResult from .info import Info __all__ = [ "ExecutionContext", "ExecutionResult", "Info", "Info", "SubscriptionExecutionResult", "get_object_definition", "has_object_definition", ] strawberry-graphql-0.287.0/strawberry/types/arguments.py000066400000000000000000000264001511033167500234630ustar00rootroot00000000000000from __future__ import annotations import inspect import warnings from typing import ( TYPE_CHECKING, Annotated, Any, cast, get_args, get_origin, ) from strawberry.annotation import StrawberryAnnotation from strawberry.exceptions import MultipleStrawberryArgumentsError, UnsupportedTypeError from strawberry.scalars import is_scalar from strawberry.types.base import ( StrawberryList, StrawberryMaybe, StrawberryOptional, has_object_definition, ) from strawberry.types.enum import StrawberryEnumDefinition, has_enum_definition from strawberry.types.lazy_type import LazyType, StrawberryLazyReference from strawberry.types.maybe import Some from strawberry.types.unset import UNSET as _deprecated_UNSET # noqa: N811 from strawberry.types.unset import ( _deprecated_is_unset, # noqa: F401 ) if TYPE_CHECKING: from collections.abc import Iterable, Mapping from strawberry.schema.config import StrawberryConfig from strawberry.types.base import StrawberryType from strawberry.types.scalar import ScalarDefinition, ScalarWrapper DEPRECATED_NAMES: dict[str, str] = { "UNSET": ( "importing `UNSET` from `strawberry.arguments` is deprecated, " "import instead from `strawberry` or from `strawberry.types.unset`" ), "is_unset": "`is_unset` is deprecated use `value is UNSET` instead", } class StrawberryArgumentAnnotation: description: str | None name: str | None deprecation_reason: str | None directives: Iterable[object] metadata: Mapping[Any, Any] def __init__( self, description: str | None = None, name: str | None = None, deprecation_reason: str | None = None, directives: Iterable[object] = (), metadata: Mapping[Any, Any] | None = None, ) -> None: self.description = description self.name = name self.deprecation_reason = deprecation_reason self.directives = directives self.metadata = metadata or {} class StrawberryArgument: def __init__( self, python_name: str, graphql_name: str | None, type_annotation: StrawberryAnnotation, is_subscription: bool = False, description: str | None = None, default: object = _deprecated_UNSET, deprecation_reason: str | None = None, directives: Iterable[object] = (), metadata: Mapping[Any, Any] | None = None, ) -> None: self.python_name = python_name self.graphql_name = graphql_name self.is_subscription = is_subscription self.description = description self.type_annotation = type_annotation self.deprecation_reason = deprecation_reason self.directives = directives self.metadata = metadata or {} # TODO: Consider moving this logic to a function self.default = ( _deprecated_UNSET if default is inspect.Parameter.empty else default ) annotation = type_annotation.annotation if not isinstance(annotation, str): resolved_annotation = annotation if get_origin(resolved_annotation) is Annotated: first, *rest = get_args(resolved_annotation) # The first argument to Annotated is always the underlying type self.type_annotation = StrawberryAnnotation(first) # Find any instances of StrawberryArgumentAnnotation # in the other Annotated args, raising an exception if there # are multiple StrawberryArgumentAnnotations argument_annotation_seen = False for arg in rest: if isinstance(arg, StrawberryArgumentAnnotation): if argument_annotation_seen: raise MultipleStrawberryArgumentsError( argument_name=python_name ) argument_annotation_seen = True self.description = arg.description self.graphql_name = arg.name self.deprecation_reason = arg.deprecation_reason self.directives = arg.directives self.metadata = arg.metadata if isinstance(arg, StrawberryLazyReference): self.type_annotation = StrawberryAnnotation( arg.resolve_forward_ref(first) ) @property def type(self) -> StrawberryType | type: return self.type_annotation.resolve() @property def is_graphql_generic(self) -> bool: from strawberry.schema.compat import is_graphql_generic return is_graphql_generic(self.type) @property def is_maybe(self) -> bool: return isinstance(self.type, StrawberryMaybe) def _is_leaf_type( type_: StrawberryType | type, scalar_registry: Mapping[object, ScalarWrapper | ScalarDefinition], skip_classes: tuple[type, ...] = (), ) -> bool: if type_ in skip_classes: return False if is_scalar(type_, scalar_registry): return True if isinstance(type_, StrawberryEnumDefinition): return True if isinstance(type_, LazyType): return _is_leaf_type(type_.resolve_type(), scalar_registry) return False def _is_optional_leaf_type( type_: StrawberryType | type, scalar_registry: Mapping[object, ScalarWrapper | ScalarDefinition], skip_classes: tuple[type, ...] = (), ) -> bool: if type_ in skip_classes: return False if isinstance(type_, StrawberryOptional): return _is_leaf_type(type_.of_type, scalar_registry, skip_classes) return False def convert_argument( value: object, type_: StrawberryType | type, scalar_registry: Mapping[object, ScalarWrapper | ScalarDefinition], config: StrawberryConfig, ) -> object: from strawberry.relay.types import GlobalID # TODO: move this somewhere else and make it first class # Handle StrawberryMaybe first, since it extends StrawberryOptional if isinstance(type_, StrawberryMaybe): # Check if this is Maybe[T | None] (has StrawberryOptional as of_type) if isinstance(type_.of_type, StrawberryOptional): # This is Maybe[T | None] - allows null values res = convert_argument(value, type_.of_type, scalar_registry, config) return Some(res) # This is Maybe[T] - validation for null values is handled by MaybeNullValidationRule # Convert the value and wrap in Some() res = convert_argument(value, type_.of_type, scalar_registry, config) return Some(res) # Handle regular StrawberryOptional (not Maybe) if isinstance(type_, StrawberryOptional): return convert_argument(value, type_.of_type, scalar_registry, config) if value is None: return None if value is _deprecated_UNSET: return _deprecated_UNSET if isinstance(type_, StrawberryList): value_list = cast("Iterable", value) if _is_leaf_type( type_.of_type, scalar_registry, skip_classes=(GlobalID,) ) or _is_optional_leaf_type( type_.of_type, scalar_registry, skip_classes=(GlobalID,) ): return value_list value_list = cast("Iterable", value) return [ convert_argument(x, type_.of_type, scalar_registry, config) for x in value_list ] if _is_leaf_type(type_, scalar_registry): if type_ is GlobalID: return GlobalID.from_id(value) # type: ignore return value if isinstance(type_, LazyType): return convert_argument(value, type_.resolve_type(), scalar_registry, config) if has_enum_definition(type_): enum_definition: StrawberryEnumDefinition = type_.__strawberry_definition__ return convert_argument(value, enum_definition, scalar_registry, config) if has_object_definition(type_): kwargs = {} type_definition = type_.__strawberry_definition__ for field in type_definition.fields: value = cast("Mapping", value) graphql_name = config.name_converter.from_field(field) if graphql_name in value: kwargs[field.python_name] = convert_argument( value[graphql_name], field.resolve_type(type_definition=type_definition), scalar_registry, config, ) type_ = cast("type", type_) return type_(**kwargs) raise UnsupportedTypeError(type_) def convert_arguments( value: dict[str, Any], arguments: list[StrawberryArgument], scalar_registry: Mapping[object, ScalarWrapper | ScalarDefinition], config: StrawberryConfig, ) -> dict[str, Any]: """Converts a nested dictionary to a dictionary of actual types. It deals with conversion of input types to proper dataclasses and also uses a sentinel value for unset values. """ if not arguments: return {} kwargs = {} for argument in arguments: assert argument.python_name name = config.name_converter.from_argument(argument) if name in value: current_value = value[name] kwargs[argument.python_name] = convert_argument( value=current_value, type_=argument.type, config=config, scalar_registry=scalar_registry, ) return kwargs def argument( description: str | None = None, name: str | None = None, deprecation_reason: str | None = None, directives: Iterable[object] = (), metadata: Mapping[Any, Any] | None = None, ) -> StrawberryArgumentAnnotation: """Function to add metadata to an argument, like a description or deprecation reason. Args: description: The GraphQL description of the argument name: The GraphQL name of the argument deprecation_reason: The reason why this argument is deprecated, setting this will mark the argument as deprecated directives: The directives to attach to the argument metadata: Metadata to attach to the argument, this can be used to store custom data that can be used by custom logic or plugins Returns: A StrawberryArgumentAnnotation object that can be used to customise an argument Example: ```python import strawberry @strawberry.type class Query: @strawberry.field def example( self, info, value: int = strawberry.argument(description="The value") ) -> int: return value ``` """ return StrawberryArgumentAnnotation( description=description, name=name, deprecation_reason=deprecation_reason, directives=directives, metadata=metadata, ) def __getattr__(name: str) -> Any: if name in DEPRECATED_NAMES: warnings.warn(DEPRECATED_NAMES[name], DeprecationWarning, stacklevel=2) return globals()[f"_deprecated_{name}"] raise AttributeError(f"module {__name__} has no attribute {name}") # TODO: check exports __all__ = [ # noqa: F822 "UNSET", # for backwards compatibility # type: ignore "StrawberryArgument", "StrawberryArgumentAnnotation", "argument", "is_unset", # for backwards compatibility # type: ignore ] strawberry-graphql-0.287.0/strawberry/types/auto.py000066400000000000000000000056121511033167500224300ustar00rootroot00000000000000from __future__ import annotations from typing import Annotated, Any, cast, get_args, get_origin from strawberry.annotation import StrawberryAnnotation from strawberry.types.base import StrawberryType class StrawberryAutoMeta(type): """Metaclass for StrawberryAuto. This is used to make sure StrawberryAuto is a singleton and also to override the behavior of `isinstance` so that it consider the following cases: >> isinstance(StrawberryAuto(), StrawberryAuto) True >> isinstance(StrawberryAnnotation(StrawberryAuto()), StrawberryAuto) True >> isinstance(Annotated[StrawberryAuto(), object()), StrawberryAuto) True """ def __init__(cls, *args: str, **kwargs: Any) -> None: cls._instance: StrawberryAuto | None = None super().__init__(*args, **kwargs) def __call__(cls, *args: str, **kwargs: Any) -> Any: if cls._instance is None: cls._instance = super().__call__(*args, **kwargs) return cls._instance def __instancecheck__( cls, instance: StrawberryAuto | StrawberryAnnotation | StrawberryType | type, ) -> bool: if isinstance(instance, StrawberryAnnotation): resolved = instance.raw_annotation if isinstance(resolved, str): namespace = instance.namespace resolved = namespace and namespace.get(resolved) if resolved is not None: instance = cast("type", resolved) if instance is auto: return True # Support uses of Annotated[auto, something()] if get_origin(instance) is Annotated: args = get_args(instance) if args[0] is Any: return any(isinstance(arg, StrawberryAuto) for arg in args[1:]) # StrawberryType's `__eq__` tries to find the string passed in the global # namespace, which will fail with a `NameError` if "strawberry.auto" hasn't # been imported. So we can't use `instance == "strawberry.auto"` here. # Instead, we'll use `isinstance(instance, str)` to check if the instance # is a StrawberryType, in that case we can return False since we know it # won't be a StrawberryAuto. if isinstance(instance, StrawberryType): return False return instance == "strawberry.auto" class StrawberryAuto(metaclass=StrawberryAutoMeta): def __str__(self) -> str: return "auto" def __repr__(self) -> str: return "" auto = Annotated[Any, StrawberryAuto()] """Special marker for automatic annotation. A special value that can be used to automatically infer the type of a field when using integrations like Strawberry Django or Strawberry Pydantic. Example: ```python import strawberry from my_user_app import models @strawberry.django.type(models.User) class User: name: strawberry.auto ``` """ __all__ = ["auto"] strawberry-graphql-0.287.0/strawberry/types/base.py000066400000000000000000000365231511033167500223770ustar00rootroot00000000000000from __future__ import annotations import dataclasses from abc import ABC, abstractmethod from typing import ( TYPE_CHECKING, Any, ClassVar, Generic, Literal, TypeGuard, TypeVar, overload, ) from typing_extensions import Protocol, Self, deprecated from strawberry.utils.deprecations import DEPRECATION_MESSAGES, DeprecatedDescriptor from strawberry.utils.inspect import get_specialized_type_var_map from strawberry.utils.typing import is_concrete_generic from strawberry.utils.typing import is_generic as is_type_generic if TYPE_CHECKING: from collections.abc import Callable, Mapping, Sequence from graphql import GraphQLAbstractType, GraphQLResolveInfo from strawberry.types.field import StrawberryField class StrawberryType(ABC): """The base class for all types that Strawberry uses. Every type that is decorated by strawberry should have a dunder `__strawberry_definition__` with an instance of a StrawberryType that contains the parsed information that strawberry created. NOTE: ATM this is only true for @type @interface @input follow https://github.com/strawberry-graphql/strawberry/issues/2841 to see progress. """ @property def type_params(self) -> list[TypeVar]: return [] @property def is_one_of(self) -> bool: return False @abstractmethod def copy_with( self, type_var_map: Mapping[ str, StrawberryType | type[WithStrawberryObjectDefinition] ], ) -> StrawberryType | type[WithStrawberryObjectDefinition]: raise NotImplementedError @property @abstractmethod def is_graphql_generic(self) -> bool: raise NotImplementedError def has_generic(self, type_var: TypeVar) -> bool: return False def __eq__(self, other: object) -> bool: from strawberry.annotation import StrawberryAnnotation if isinstance(other, StrawberryType): return self is other if isinstance(other, StrawberryAnnotation): return self == other.resolve() # This could be simplified if StrawberryAnnotation.resolve() always returned # a StrawberryType resolved = StrawberryAnnotation(other).resolve() if isinstance(resolved, StrawberryType): return self == resolved return NotImplemented def __hash__(self) -> int: # TODO: Is this a bad idea? __eq__ objects are supposed to have the same hash return id(self) class StrawberryContainer(StrawberryType): def __init__( self, of_type: StrawberryType | type[WithStrawberryObjectDefinition] | type ) -> None: self.of_type = of_type def __hash__(self) -> int: return hash((self.__class__, self.of_type)) def __eq__(self, other: object) -> bool: if isinstance(other, StrawberryType): if isinstance(other, StrawberryContainer): return self.of_type == other.of_type return False return super().__eq__(other) @property def type_params(self) -> list[TypeVar]: if has_object_definition(self.of_type): parameters = getattr(self.of_type, "__parameters__", None) return list(parameters) if parameters else [] if isinstance(self.of_type, StrawberryType): return self.of_type.type_params return [] def copy_with( self, type_var_map: Mapping[ str, StrawberryType | type[WithStrawberryObjectDefinition] ], ) -> Self: of_type_copy = self.of_type if has_object_definition(self.of_type): type_definition = self.of_type.__strawberry_definition__ if type_definition.is_graphql_generic: of_type_copy = type_definition.copy_with(type_var_map) elif ( isinstance(self.of_type, StrawberryType) and self.of_type.is_graphql_generic ): of_type_copy = self.of_type.copy_with(type_var_map) return type(self)(of_type_copy) @property def is_graphql_generic(self) -> bool: from strawberry.schema.compat import is_graphql_generic type_ = self.of_type return is_graphql_generic(type_) def has_generic(self, type_var: TypeVar) -> bool: if isinstance(self.of_type, StrawberryType): return self.of_type.has_generic(type_var) return False class StrawberryList(StrawberryContainer): ... class StrawberryOptional(StrawberryContainer): def __init__( self, of_type: StrawberryType | type[WithStrawberryObjectDefinition] | type, ) -> None: super().__init__(of_type) class StrawberryMaybe(StrawberryOptional): pass class StrawberryTypeVar(StrawberryType): def __init__(self, type_var: TypeVar) -> None: self.type_var = type_var def copy_with( self, type_var_map: Mapping[str, StrawberryType | type] ) -> StrawberryType | type: return type_var_map[self.type_var.__name__] @property def is_graphql_generic(self) -> bool: return True def has_generic(self, type_var: TypeVar) -> bool: return self.type_var == type_var @property def type_params(self) -> list[TypeVar]: return [self.type_var] def __eq__(self, other: object) -> bool: if isinstance(other, StrawberryTypeVar): return self.type_var == other.type_var if isinstance(other, TypeVar): return self.type_var == other return super().__eq__(other) def __hash__(self) -> int: return hash(self.type_var) StrawberryDefinitionType = TypeVar("StrawberryDefinitionType") class WithStrawberryDefinition(Protocol, Generic[StrawberryDefinitionType]): __strawberry_definition__: ClassVar[StrawberryDefinitionType] WithStrawberryObjectDefinition = WithStrawberryDefinition["StrawberryObjectDefinition"] def has_strawberry_definition( obj: Any, ) -> TypeGuard[type[WithStrawberryDefinition]]: if hasattr(obj, "__strawberry_definition__"): return True # TODO: Generics remove dunder members here, so we inject it here. # Would be better to avoid it somehow. # https://github.com/python/cpython/blob/3a314f7c3df0dd7c37da7d12b827f169ee60e1ea/Lib/typing.py#L1152 if is_concrete_generic(obj): concrete = obj.__origin__ if hasattr(concrete, "__strawberry_definition__"): obj.__strawberry_definition__ = concrete.__strawberry_definition__ return True return False def has_object_definition(obj: Any) -> TypeGuard[type[WithStrawberryObjectDefinition]]: if has_strawberry_definition(obj): return isinstance(obj.__strawberry_definition__, StrawberryObjectDefinition) return False @overload def get_object_definition( obj: Any, *, strict: Literal[True], ) -> StrawberryObjectDefinition: ... @overload def get_object_definition( obj: Any, *, strict: bool = False, ) -> StrawberryObjectDefinition | None: ... def get_object_definition( obj: Any, *, strict: bool = False, ) -> StrawberryObjectDefinition | None: definition = obj.__strawberry_definition__ if has_object_definition(obj) else None if strict and definition is None: raise TypeError(f"{obj!r} does not have a StrawberryObjectDefinition") return definition @dataclasses.dataclass(eq=False) class StrawberryObjectDefinition(StrawberryType): """Encapsulates definitions for Input / Object / interface GraphQL Types. In order get the definition from a decorated object you can use `has_object_definition` or `get_object_definition` as a shortcut. """ name: str is_input: bool is_interface: bool origin: type[Any] description: str | None interfaces: list[StrawberryObjectDefinition] extend: bool directives: Sequence[object] | None is_type_of: Callable[[Any, GraphQLResolveInfo], bool] | None resolve_type: Callable[[Any, GraphQLResolveInfo, GraphQLAbstractType], str] | None fields: list[StrawberryField] concrete_of: StrawberryObjectDefinition | None = None """Concrete implementations of Generic TypeDefinitions fill this in""" type_var_map: Mapping[str, StrawberryType | type] = dataclasses.field( default_factory=dict ) def __post_init__(self) -> None: # resolve `Self` annotation with the origin type for index, field in enumerate(self.fields): if isinstance(field.type, StrawberryType) and field.type.has_generic(Self): # type: ignore self.fields[index] = field.copy_with({Self.__name__: self.origin}) # type: ignore def resolve_generic(self, wrapped_cls: type) -> type: from strawberry.annotation import StrawberryAnnotation passed_types = wrapped_cls.__args__ # type: ignore params = wrapped_cls.__origin__.__parameters__ # type: ignore # Make sure all passed_types are turned into StrawberryTypes resolved_types = [] for passed_type in passed_types: resolved_type = StrawberryAnnotation(passed_type).resolve() resolved_types.append(resolved_type) type_var_map = dict( zip((param.__name__ for param in params), resolved_types, strict=True) ) return self.copy_with(type_var_map) def copy_with( self, type_var_map: Mapping[str, StrawberryType | type] ) -> type[WithStrawberryObjectDefinition]: fields = [field.copy_with(type_var_map) for field in self.fields] new_type_definition = StrawberryObjectDefinition( name=self.name, is_input=self.is_input, origin=self.origin, is_interface=self.is_interface, directives=self.directives and self.directives[:], interfaces=self.interfaces and self.interfaces[:], description=self.description, extend=self.extend, is_type_of=self.is_type_of, resolve_type=self.resolve_type, fields=fields, concrete_of=self, type_var_map=type_var_map, ) new_type = type( new_type_definition.name, (self.origin,), {"__strawberry_definition__": new_type_definition}, ) # TODO: remove when deprecating _type_definition DeprecatedDescriptor( DEPRECATION_MESSAGES._TYPE_DEFINITION, new_type.__strawberry_definition__, # type: ignore "_type_definition", ).inject(new_type) new_type_definition.origin = new_type return new_type def get_field(self, python_name: str) -> StrawberryField | None: return next( (field for field in self.fields if field.python_name == python_name), None ) @property def is_graphql_generic(self) -> bool: if not is_type_generic(self.origin): return False # here we are checking if any exposed field is generic # a Strawberry class can be "generic", but not expose any # generic field to GraphQL return any(field.is_graphql_generic for field in self.fields) @property def is_specialized_generic(self) -> bool: return self.is_graphql_generic and not getattr( self.origin, "__parameters__", None ) @property def specialized_type_var_map(self) -> dict[str, type] | None: return get_specialized_type_var_map(self.origin) @property def is_object_type(self) -> bool: return not self.is_input and not self.is_interface @property def type_params(self) -> list[TypeVar]: type_params: list[TypeVar] = [] for field in self.fields: type_params.extend(field.type_params) return type_params def is_implemented_by(self, root: type[WithStrawberryObjectDefinition]) -> bool: # TODO: Support dicts if isinstance(root, dict): raise NotImplementedError type_definition = root.__strawberry_definition__ if type_definition is self: # No generics involved. Exact type match return True if type_definition is not self.concrete_of: # Either completely different type, or concrete type of a different generic return False # Check the mapping of all fields' TypeVars for field in type_definition.fields: if not field.is_graphql_generic: continue value = getattr(root, field.name) generic_field_type = field.type while isinstance(generic_field_type, StrawberryList): generic_field_type = generic_field_type.of_type assert isinstance(value, (list, tuple)) if len(value) == 0: # We can't infer the type of an empty list, so we just # return the first one we find return True value = value[0] if isinstance(generic_field_type, StrawberryTypeVar): type_var = generic_field_type.type_var # TODO: I don't think we support nested types properly # if there's a union that has two nested types that # are have the same field with different types, we might # not be able to differentiate them else: continue # For each TypeVar found, get the expected type from the copy's type map expected_concrete_type = self.type_var_map.get(type_var.__name__) # this shouldn't happen, but we do a defensive check just in case if expected_concrete_type is None: continue # Check if the expected type matches the type found on the type_map from strawberry.types.enum import ( StrawberryEnumDefinition, has_enum_definition, ) real_concrete_type: type | StrawberryEnumDefinition = type(value) # TODO: uniform type var map, at the moment we map object types # to their class (not to TypeDefinition) while we map enum to # the StrawberryEnumDefinition class. This is why we do this check here: if has_enum_definition(real_concrete_type): real_concrete_type = real_concrete_type.__strawberry_definition__ if ( isinstance(expected_concrete_type, type) and isinstance(real_concrete_type, type) and issubclass(real_concrete_type, expected_concrete_type) ): return True if real_concrete_type is not expected_concrete_type: return False # All field mappings succeeded. This is a match return True @property def is_one_of(self) -> bool: from strawberry.schema_directives import OneOf if not self.is_input or not self.directives: return False return any(isinstance(directive, OneOf) for directive in self.directives) # TODO: remove when deprecating _type_definition if TYPE_CHECKING: @deprecated("Use StrawberryObjectDefinition instead") class TypeDefinition(StrawberryObjectDefinition): ... else: TypeDefinition = StrawberryObjectDefinition __all__ = [ "StrawberryContainer", "StrawberryList", "StrawberryObjectDefinition", "StrawberryOptional", "StrawberryType", "StrawberryTypeVar", "TypeDefinition", "WithStrawberryObjectDefinition", "get_object_definition", "has_object_definition", ] strawberry-graphql-0.287.0/strawberry/types/cast.py000066400000000000000000000016241511033167500224110ustar00rootroot00000000000000from __future__ import annotations from typing import Any, TypeVar, overload _T = TypeVar("_T", bound=object) TYPE_CAST_ATTRIBUTE = "__as_strawberry_type__" @overload def cast(type_: type, obj: None) -> None: ... @overload def cast(type_: type, obj: _T) -> _T: ... def cast(type_: type, obj: _T | None) -> _T | None: """Cast an object to given type. This is used to mark an object as a cast object, so that the type can be picked up when resolving unions/interfaces in case of ambiguity, which can happen when returning an alike object instead of an instance of the type (e.g. returning a Django, Pydantic or SQLAlchemy object) """ if obj is None: return None setattr(obj, TYPE_CAST_ATTRIBUTE, type_) return obj def get_strawberry_type_cast(obj: Any) -> type | None: """Get the type of a cast object.""" return getattr(obj, TYPE_CAST_ATTRIBUTE, None) strawberry-graphql-0.287.0/strawberry/types/enum.py000066400000000000000000000161661511033167500224320ustar00rootroot00000000000000import dataclasses from collections.abc import Callable, Iterable, Mapping from enum import EnumMeta from typing import TYPE_CHECKING, Any, TypeGuard, TypeVar, overload from strawberry.exceptions import ObjectIsNotAnEnumError from strawberry.types.base import ( StrawberryType, WithStrawberryDefinition, has_strawberry_definition, ) from strawberry.utils.deprecations import DEPRECATION_MESSAGES, DeprecatedDescriptor @dataclasses.dataclass class EnumValue: name: str value: Any deprecation_reason: str | None = None directives: Iterable[object] = () description: str | None = None @dataclasses.dataclass class StrawberryEnumDefinition(StrawberryType): wrapped_cls: EnumMeta name: str values: list[EnumValue] description: str | None directives: Iterable[object] = () def __hash__(self) -> int: # TODO: Is this enough for unique-ness? return hash(self.name) def copy_with( self, type_var_map: Mapping[str, StrawberryType | type] ) -> StrawberryType | type: # enum don't support type parameters, so we can safely return self return self @property def is_graphql_generic(self) -> bool: return False @property def origin(self) -> type: return self.wrapped_cls # TODO: remove duplication of EnumValueDefinition and EnumValue @dataclasses.dataclass class EnumValueDefinition: value: Any graphql_name: str | None = None deprecation_reason: str | None = None directives: Iterable[object] = () description: str | None = None def __int__(self) -> int: return self.value def enum_value( value: Any, name: str | None = None, deprecation_reason: str | None = None, directives: Iterable[object] = (), description: str | None = None, ) -> EnumValueDefinition: """Function to customise an enum value, for example to add a description or deprecation reason. Args: value: The value of the enum member. name: The GraphQL name of the enum member. deprecation_reason: The deprecation reason of the enum member, setting this will mark the enum member as deprecated. directives: The directives to attach to the enum member. description: The GraphQL description of the enum member. Returns: An EnumValueDefinition object that can be used to customise an enum member. Example: ```python from enum import Enum import strawberry @strawberry.enum class MyEnum(Enum): FIRST_VALUE = strawberry.enum_value(description="The first value") SECOND_VALUE = strawberry.enum_value(description="The second value") ``` """ return EnumValueDefinition( value=value, graphql_name=name, deprecation_reason=deprecation_reason, directives=directives, description=description, ) EnumType = TypeVar("EnumType", bound=EnumMeta) def _process_enum( cls: EnumType, name: str | None = None, description: str | None = None, directives: Iterable[object] = (), ) -> EnumType: if not isinstance(cls, EnumMeta): raise ObjectIsNotAnEnumError(cls) if not name: name = cls.__name__ values = [] for item in cls: # type: ignore item_value = item.value item_name = item.name deprecation_reason = None item_directives: Iterable[object] = () enum_value_description = None if isinstance(item_value, EnumValueDefinition): item_directives = item_value.directives enum_value_description = item_value.description deprecation_reason = item_value.deprecation_reason # update _value2member_map_ so that doing `MyEnum.MY_VALUE` and # `MyEnum['MY_VALUE']` both work cls._value2member_map_[item_value.value] = item cls._member_map_[item_name]._value_ = item_value.value if item_value.graphql_name: item_name = item_value.graphql_name item_value = item_value.value value = EnumValue( item_name, item_value, deprecation_reason=deprecation_reason, directives=item_directives, description=enum_value_description, ) values.append(value) cls.__strawberry_definition__ = StrawberryEnumDefinition( # type: ignore wrapped_cls=cls, name=name, values=values, description=description, directives=directives, ) # TODO: remove when deprecating _enum_definition DeprecatedDescriptor( DEPRECATION_MESSAGES._ENUM_DEFINITION, cls.__strawberry_definition__, # type: ignore[attr-defined] "_enum_definition", ).inject(cls) return cls @overload def enum( cls: EnumType, *, name: str | None = None, description: str | None = None, directives: Iterable[object] = (), ) -> EnumType: ... @overload def enum( cls: None = None, *, name: str | None = None, description: str | None = None, directives: Iterable[object] = (), ) -> Callable[[EnumType], EnumType]: ... def enum( cls: EnumType | None = None, *, name: str | None = None, description: str | None = None, directives: Iterable[object] = (), ) -> EnumType | Callable[[EnumType], EnumType]: """Annotates an Enum class a GraphQL enum. GraphQL enums only have names, while Python enums have names and values, Strawberry will use the names of the Python enum as the names of the GraphQL enum values. Args: cls: The Enum class to be annotated. name: The name of the GraphQL enum. description: The description of the GraphQL enum. directives: The directives to attach to the GraphQL enum. Returns: The decorated Enum class. Example: ```python from enum import Enum import strawberry @strawberry.enum class MyEnum(Enum): FIRST_VALUE = "first_value" SECOND_VALUE = "second_value" ``` The above code will generate the following GraphQL schema: ```graphql enum MyEnum { FIRST_VALUE SECOND_VALUE } ``` If name is passed, the name of the GraphQL type will be the value passed of name instead of the Enum class name. """ def wrap(cls: EnumType) -> EnumType: return _process_enum(cls, name, description, directives=directives) if not cls: return wrap return wrap(cls) # TODO: remove when deprecating _enum_definition if TYPE_CHECKING: from typing_extensions import deprecated @deprecated("Use StrawberryEnumDefinition instead") class EnumDefinition(StrawberryEnumDefinition): ... else: EnumDefinition = StrawberryEnumDefinition WithStrawberryEnumDefinition = WithStrawberryDefinition["StrawberryEnumDefinition"] def has_enum_definition(obj: Any) -> TypeGuard[type[WithStrawberryEnumDefinition]]: if has_strawberry_definition(obj): return isinstance(obj.__strawberry_definition__, StrawberryEnumDefinition) return False __all__ = [ "EnumDefinition", "EnumValue", "EnumValueDefinition", "StrawberryEnumDefinition", "enum", "enum_value", ] strawberry-graphql-0.287.0/strawberry/types/execution.py000066400000000000000000000076631511033167500234730ustar00rootroot00000000000000from __future__ import annotations import dataclasses from typing import ( TYPE_CHECKING, Any, runtime_checkable, ) from typing_extensions import Protocol, TypedDict, deprecated from graphql import specified_rules from strawberry.utils.operation import get_first_operation, get_operation_type if TYPE_CHECKING: from collections.abc import Iterable from typing_extensions import NotRequired from graphql import ASTValidationRule from graphql.error.graphql_error import GraphQLError from graphql.language import DocumentNode, OperationDefinitionNode from strawberry.schema import Schema from strawberry.schema._graphql_core import GraphQLExecutionResult from .graphql import OperationType @dataclasses.dataclass class ExecutionContext: query: str | None schema: Schema allowed_operations: Iterable[OperationType] context: Any = None variables: dict[str, Any] | None = None parse_options: ParseOptions = dataclasses.field( default_factory=lambda: ParseOptions() ) root_value: Any | None = None validation_rules: tuple[type[ASTValidationRule], ...] = dataclasses.field( default_factory=lambda: tuple(specified_rules) ) # The operation name that is provided by the request provided_operation_name: dataclasses.InitVar[str | None] = None # Values that get populated during the GraphQL execution so that they can be # accessed by extensions graphql_document: DocumentNode | None = None pre_execution_errors: list[GraphQLError] | None = None result: GraphQLExecutionResult | None = None extensions_results: dict[str, Any] = dataclasses.field(default_factory=dict) operation_extensions: dict[str, Any] | None = None def __post_init__(self, provided_operation_name: str | None) -> None: self._provided_operation_name = provided_operation_name @property def operation_name(self) -> str | None: if self._provided_operation_name is not None: return self._provided_operation_name definition = self._get_first_operation() if not definition: return None if not definition.name: return None return definition.name.value @property def operation_type(self) -> OperationType: graphql_document = self.graphql_document if not graphql_document: raise RuntimeError("No GraphQL document available") return get_operation_type(graphql_document, self.operation_name) def _get_first_operation(self) -> OperationDefinitionNode | None: graphql_document = self.graphql_document if not graphql_document: return None return get_first_operation(graphql_document) @property @deprecated("Use 'pre_execution_errors' instead") def errors(self) -> list[GraphQLError] | None: """Deprecated: Use pre_execution_errors instead.""" return self.pre_execution_errors @dataclasses.dataclass class ExecutionResult: data: dict[str, Any] | None errors: list[GraphQLError] | None extensions: dict[str, Any] | None = None @dataclasses.dataclass class PreExecutionError(ExecutionResult): """Differentiate between a normal execution result and an immediate error. Immediate errors are errors that occur before the execution phase i.e validation errors, or any other error that occur before we interact with resolvers. These errors are required by `graphql-ws-transport` protocol in order to close the operation right away once the error is encountered. """ class ParseOptions(TypedDict): max_tokens: NotRequired[int] @runtime_checkable class SubscriptionExecutionResult(Protocol): def __aiter__(self) -> SubscriptionExecutionResult: # pragma: no cover ... async def __anext__(self) -> Any: # pragma: no cover ... __all__ = [ "ExecutionContext", "ExecutionResult", "ParseOptions", "SubscriptionExecutionResult", ] strawberry-graphql-0.287.0/strawberry/types/field.py000066400000000000000000000477061511033167500225550ustar00rootroot00000000000000from __future__ import annotations import contextlib import copy import dataclasses import sys from collections.abc import Awaitable, Callable, Coroutine, Mapping, Sequence from functools import cached_property from typing import ( TYPE_CHECKING, Any, NoReturn, TypeAlias, TypeVar, Union, overload, ) from strawberry.annotation import StrawberryAnnotation from strawberry.exceptions import InvalidArgumentTypeError, InvalidDefaultFactoryError from strawberry.types.base import ( StrawberryType, WithStrawberryObjectDefinition, has_object_definition, ) from strawberry.types.union import StrawberryUnion from .fields.resolver import StrawberryResolver if TYPE_CHECKING: import builtins from typing import Literal from typing_extensions import Self from strawberry.extensions.field_extension import FieldExtension from strawberry.permission import BasePermission from strawberry.types.arguments import StrawberryArgument from strawberry.types.base import StrawberryObjectDefinition from strawberry.types.info import Info T = TypeVar("T") _RESOLVER_TYPE_SYNC: TypeAlias = Union[ StrawberryResolver[T], Callable[..., T], "staticmethod[Any, T]", "classmethod[Any, Any, T]", ] _RESOLVER_TYPE_ASYNC: TypeAlias = ( Callable[..., Coroutine[Any, Any, T]] | Callable[..., Awaitable[T]] ) _RESOLVER_TYPE: TypeAlias = _RESOLVER_TYPE_SYNC[T] | _RESOLVER_TYPE_ASYNC[T] class UNRESOLVED: def __new__(cls) -> NoReturn: raise TypeError("UNRESOLVED is a sentinel and cannot be instantiated.") FieldType: TypeAlias = ( StrawberryType | type[WithStrawberryObjectDefinition | UNRESOLVED] ) def _is_generic(resolver_type: StrawberryType | type) -> bool: """Returns True if `resolver_type` is generic else False.""" if isinstance(resolver_type, StrawberryType): return resolver_type.is_graphql_generic # solves the Generic subclass case if has_object_definition(resolver_type): return resolver_type.__strawberry_definition__.is_graphql_generic return False class StrawberryField(dataclasses.Field): type_annotation: StrawberryAnnotation | None default_resolver: Callable[[Any, str], object] = getattr def __init__( self, python_name: str | None = None, graphql_name: str | None = None, type_annotation: StrawberryAnnotation | None = None, origin: type | Callable | staticmethod | classmethod | None = None, is_subscription: bool = False, description: str | None = None, base_resolver: StrawberryResolver | None = None, permission_classes: list[type[BasePermission]] = (), # type: ignore default: object = dataclasses.MISSING, default_factory: Callable[[], Any] | object = dataclasses.MISSING, metadata: Mapping[Any, Any] | None = None, deprecation_reason: str | None = None, directives: Sequence[object] = (), extensions: list[FieldExtension] = (), # type: ignore ) -> None: # basic fields are fields with no provided resolver is_basic_field = not base_resolver kwargs: Any = { "kw_only": True, } # doc was added to python 3.14 and it is required if sys.version_info >= (3, 14): kwargs["doc"] = None super().__init__( default=default, default_factory=default_factory, # type: ignore init=is_basic_field, repr=is_basic_field, compare=is_basic_field, hash=None, metadata=metadata or {}, **kwargs, ) self.graphql_name = graphql_name if python_name is not None: self.python_name = python_name self.type_annotation = type_annotation self.description: str | None = description self.origin = origin self._arguments: list[StrawberryArgument] | None = None self._base_resolver: StrawberryResolver | None = None if base_resolver is not None: self.base_resolver = base_resolver # Note: StrawberryField.default is the same as # StrawberryField.default_value except that `.default` uses # `dataclasses.MISSING` to represent an "undefined" value and # `.default_value` uses `UNSET` self.default_value = default if callable(default_factory): try: self.default_value = default_factory() except TypeError as exc: raise InvalidDefaultFactoryError from exc self.is_subscription = is_subscription self.permission_classes: list[type[BasePermission]] = list(permission_classes) self.directives = list(directives) self.extensions: list[FieldExtension] = list(extensions) # Automatically add the permissions extension if len(self.permission_classes): from strawberry.permission import PermissionExtension if not self.extensions: self.extensions = [] permission_instances = [ permission_class() for permission_class in permission_classes ] # Append to make it run first (last is outermost) self.extensions.append( PermissionExtension(permission_instances, use_directives=False) ) self.deprecation_reason = deprecation_reason def __copy__(self) -> Self: new_field = type(self)( python_name=self.python_name, graphql_name=self.graphql_name, type_annotation=self.type_annotation, origin=self.origin, is_subscription=self.is_subscription, description=self.description, base_resolver=self.base_resolver, permission_classes=( self.permission_classes[:] if self.permission_classes is not None else [] ), default=self.default_value, default_factory=self.default_factory, metadata=self.metadata.copy() if self.metadata is not None else None, deprecation_reason=self.deprecation_reason, directives=self.directives[:] if self.directives is not None else [], extensions=self.extensions[:] if self.extensions is not None else [], ) new_field._arguments = ( self._arguments[:] if self._arguments is not None else None ) return new_field def __call__(self, resolver: _RESOLVER_TYPE) -> Self: """Add a resolver to the field.""" # Allow for StrawberryResolvers or bare functions to be provided if not isinstance(resolver, StrawberryResolver): resolver = StrawberryResolver(resolver) for argument in resolver.arguments: if isinstance(argument.type_annotation.annotation, str): continue if isinstance(argument.type, StrawberryUnion): raise InvalidArgumentTypeError( resolver, argument, ) if ( has_object_definition(argument.type) and argument.type.__strawberry_definition__.is_interface ): raise InvalidArgumentTypeError( resolver, argument, ) self.base_resolver = resolver return self def get_result( self, source: Any, info: Info | None, args: list[Any], kwargs: Any ) -> Awaitable[Any] | Any: """Calls the resolver defined for the StrawberryField. If the field doesn't have a resolver defined we default to using the default resolver specified in StrawberryConfig. """ if self.base_resolver: return self.base_resolver(*args, **kwargs) return self.default_resolver(source, self.python_name) @property def is_basic_field(self) -> bool: """Returns a boolean indicating if the field is a basic field. A "basic" field us a field that has no resolver or permission classes, i.e. it just returns the relevant attribute from the source object. If it is a basic field we can avoid constructing an `Info` object and running any permission checks in the resolver which improves performance. """ return not self.base_resolver and not self.extensions @property def arguments(self) -> list[StrawberryArgument]: if self._arguments is None: self._arguments = self.base_resolver.arguments if self.base_resolver else [] return self._arguments @arguments.setter def arguments(self, value: list[StrawberryArgument]) -> None: self._arguments = value @property def is_graphql_generic(self) -> bool: return ( self.base_resolver.is_graphql_generic if self.base_resolver else _is_generic(self.type) ) def _python_name(self) -> str | None: if self.name: return self.name if self.base_resolver: return self.base_resolver.name return None def _set_python_name(self, name: str) -> None: self.name = name python_name: str = property(_python_name, _set_python_name) # type: ignore[assignment] @property def base_resolver(self) -> StrawberryResolver | None: return self._base_resolver @base_resolver.setter def base_resolver(self, resolver: StrawberryResolver) -> None: self._base_resolver = resolver # Don't add field to __init__, __repr__ and __eq__ once it has a resolver self.init = False self.compare = False self.repr = False # TODO: See test_resolvers.test_raises_error_when_argument_annotation_missing # (https://github.com/strawberry-graphql/strawberry/blob/8e102d3/tests/types/test_resolvers.py#L89-L98) # # Currently we expect the exception to be thrown when the StrawberryField # is constructed, but this only happens if we explicitly retrieve the # arguments. # # If we want to change when the exception is thrown, this line can be # removed. _ = resolver.arguments @property def type(self) -> FieldType: return self.resolve_type() @type.setter def type(self, type_: Any) -> None: # Note: we aren't setting a namespace here for the annotation. That # happens in the `_get_fields` function in `types/type_resolver` so # that we have access to the correct namespace for the object type # the field is attached to. self.type_annotation = StrawberryAnnotation.from_annotation( type_, namespace=None ) # TODO: add this to arguments (and/or move it to StrawberryType) @property def type_params(self) -> list[TypeVar]: if has_object_definition(self.type): parameters = getattr(self.type, "__parameters__", None) return list(parameters) if parameters else [] # TODO: Consider making leaf types always StrawberryTypes, maybe a # StrawberryBaseType or something if isinstance(self.type, StrawberryType): return self.type.type_params return [] def resolve_type( self, *, type_definition: StrawberryObjectDefinition | None = None, ) -> FieldType: # We return UNRESOLVED by default, which means this case will raise a # MissingReturnAnnotationError exception in _check_field_annotations resolved: FieldType = UNRESOLVED # type: ignore[assignment] # We are catching NameError because dataclasses tries to fetch the type # of the field from the class before the class is fully defined. # This triggers a NameError error when using forward references because # our `type` property tries to find the field type from the global namespace # but it is not yet defined. with contextlib.suppress(NameError): # Prioritise the field type over the resolver return type if self.type_annotation is not None: resolved = self.type_annotation.resolve(type_definition=type_definition) elif self.base_resolver is not None and self.base_resolver.type is not None: # Handle unannotated functions (such as lambdas) # Generics will raise MissingTypesForGenericError later # on if we let it be returned. So use `type_annotation` instead # which is the same behaviour as having no type information. resolved = self.base_resolver.type return resolved def copy_with( self, type_var_map: Mapping[str, StrawberryType | builtins.type] ) -> Self: new_field = copy.copy(self) override_type: StrawberryType | type[WithStrawberryObjectDefinition] | None = ( None ) type_ = self.resolve_type() if has_object_definition(type_): type_definition = type_.__strawberry_definition__ if type_definition.is_graphql_generic: type_ = type_definition override_type = type_.copy_with(type_var_map) elif isinstance(type_, StrawberryType): override_type = type_.copy_with(type_var_map) if override_type is not None: new_field.type_annotation = StrawberryAnnotation( override_type, namespace=( self.type_annotation.namespace if self.type_annotation else None ), ) if self.base_resolver is not None: new_field.base_resolver = self.base_resolver.copy_with(type_var_map) return new_field @property def _has_async_base_resolver(self) -> bool: return self.base_resolver is not None and self.base_resolver.is_async @cached_property def is_async(self) -> bool: return self._has_async_base_resolver # NOTE: we are separating the sync and async resolvers because using both # in the same function will cause mypy to raise an error. Not sure if it is a bug @overload def field( *, resolver: _RESOLVER_TYPE_ASYNC[T], name: str | None = None, is_subscription: bool = False, description: str | None = None, init: Literal[False] = False, permission_classes: list[type[BasePermission]] | None = None, deprecation_reason: str | None = None, default: Any = dataclasses.MISSING, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: Mapping[Any, Any] | None = None, directives: Sequence[object] | None = (), extensions: list[FieldExtension] | None = None, graphql_type: Any | None = None, ) -> T: ... @overload def field( *, resolver: _RESOLVER_TYPE_SYNC[T], name: str | None = None, is_subscription: bool = False, description: str | None = None, init: Literal[False] = False, permission_classes: list[type[BasePermission]] | None = None, deprecation_reason: str | None = None, default: Any = dataclasses.MISSING, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: Mapping[Any, Any] | None = None, directives: Sequence[object] | None = (), extensions: list[FieldExtension] | None = None, graphql_type: Any | None = None, ) -> T: ... @overload def field( *, name: str | None = None, is_subscription: bool = False, description: str | None = None, init: Literal[True] = True, permission_classes: list[type[BasePermission]] | None = None, deprecation_reason: str | None = None, default: Any = dataclasses.MISSING, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: Mapping[Any, Any] | None = None, directives: Sequence[object] | None = (), extensions: list[FieldExtension] | None = None, graphql_type: Any | None = None, ) -> Any: ... @overload def field( resolver: _RESOLVER_TYPE_ASYNC[T], *, name: str | None = None, is_subscription: bool = False, description: str | None = None, permission_classes: list[type[BasePermission]] | None = None, deprecation_reason: str | None = None, default: Any = dataclasses.MISSING, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: Mapping[Any, Any] | None = None, directives: Sequence[object] | None = (), extensions: list[FieldExtension] | None = None, graphql_type: Any | None = None, ) -> StrawberryField: ... @overload def field( resolver: _RESOLVER_TYPE_SYNC[T], *, name: str | None = None, is_subscription: bool = False, description: str | None = None, permission_classes: list[type[BasePermission]] | None = None, deprecation_reason: str | None = None, default: Any = dataclasses.MISSING, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: Mapping[Any, Any] | None = None, directives: Sequence[object] | None = (), extensions: list[FieldExtension] | None = None, graphql_type: Any | None = None, ) -> StrawberryField: ... def field( resolver: _RESOLVER_TYPE[Any] | None = None, *, name: str | None = None, is_subscription: bool = False, description: str | None = None, permission_classes: list[type[BasePermission]] | None = None, deprecation_reason: str | None = None, default: Any = dataclasses.MISSING, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: Mapping[Any, Any] | None = None, directives: Sequence[object] | None = (), extensions: list[FieldExtension] | None = None, graphql_type: Any | None = None, # This init parameter is used by PyRight to determine whether this field # is added in the constructor or not. It is not used to change # any behavior at the moment. init: Literal[True, False] | None = None, ) -> Any: """Annotates a method or property as a GraphQL field. Args: resolver: The resolver for the field. This can be a function or a `StrawberryResolver`. name: The GraphQL name of the field. is_subscription: Whether the field is a subscription field. description: The GraphQL description of the field. permission_classes: The permission classes required to access the field. deprecation_reason: The deprecation reason for the field. default: The default value for the field. default_factory: The default factory for the field. metadata: The metadata for the field. directives: The directives for the field. extensions: The extensions for the field. graphql_type: The GraphQL type for the field, useful when you want to use a different type in the resolver than the one in the schema. init: This parameter is used by PyRight to determine whether this field is added in the constructor or not. It is not used to change any behavior at the moment. Returns: The field. This is normally used inside a type declaration: ```python import strawberry @strawberry.type class X: field_abc: str = strawberry.field(description="ABC") @strawberry.field(description="ABC") def field_with_resolver(self) -> str: return "abc" ``` it can be used both as decorator and as a normal function. """ type_annotation = StrawberryAnnotation.from_annotation(graphql_type) field_ = StrawberryField( python_name=None, graphql_name=name, type_annotation=type_annotation, description=description, is_subscription=is_subscription, permission_classes=permission_classes or [], deprecation_reason=deprecation_reason, default=default, default_factory=default_factory, metadata=metadata, directives=directives or (), extensions=extensions or [], ) if resolver: assert init is not True, "Can't set init as True when passing a resolver." return field_(resolver) return field_ __all__ = ["StrawberryField", "field"] strawberry-graphql-0.287.0/strawberry/types/fields/000077500000000000000000000000001511033167500223505ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/types/fields/__init__.py000066400000000000000000000000001511033167500244470ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/types/fields/resolver.py000066400000000000000000000343721511033167500245740ustar00rootroot00000000000000from __future__ import annotations as _ import asyncio import inspect import sys import warnings from functools import cached_property from inspect import isasyncgenfunction from typing import ( TYPE_CHECKING, Annotated, Any, Generic, NamedTuple, TypeVar, cast, get_origin, ) from typing_extensions import Protocol from strawberry.annotation import StrawberryAnnotation from strawberry.exceptions import ( ConflictingArgumentsError, MissingArgumentsAnnotationsError, ) from strawberry.parent import StrawberryParent, resolve_parent_forward_arg from strawberry.types.arguments import StrawberryArgument from strawberry.types.base import StrawberryType, has_object_definition from strawberry.types.info import Info from strawberry.utils.typing import type_has_annotation if TYPE_CHECKING: import builtins from collections.abc import Callable, Mapping class Parameter(inspect.Parameter): def __hash__(self) -> int: """Override to exclude default value from hash. This adds compatibility for using unhashable default values in resolvers such as list and dict. The present use-case is limited to analyzing parameters from one resolver. Therefore, the name, kind, and annotation combination are guaranteed to be unique since two arguments cannot have the same name in a callable. Furthermore, even though it is not currently a use-case to collect parameters from different resolvers, the likelihood of collision from having the same hash value but different defaults is mitigated by Python invoking the :py:meth:`__eq__` method if two items have the same hash. See the verification of this behavior in the `test_parameter_hash_collision` test. """ return hash((self.name, self.kind, self.annotation)) class Signature(inspect.Signature): _parameter_cls = Parameter class ReservedParameterSpecification(Protocol): def find( self, parameters: tuple[inspect.Parameter, ...], resolver: StrawberryResolver[Any], ) -> inspect.Parameter | None: """Finds the reserved parameter from ``parameters``.""" class ReservedName(NamedTuple): name: str def find( self, parameters: tuple[inspect.Parameter, ...], resolver: StrawberryResolver[Any], ) -> inspect.Parameter | None: del resolver return next((p for p in parameters if p.name == self.name), None) class ReservedNameBoundParameter(NamedTuple): name: str def find( self, parameters: tuple[inspect.Parameter, ...], resolver: StrawberryResolver[Any], ) -> inspect.Parameter | None: del resolver if parameters: # Add compatibility for resolvers with no arguments first_parameter = parameters[0] return first_parameter if first_parameter.name == self.name else None return None class ReservedType(NamedTuple): """Define a reserved type by name or by type. To preserve backwards-comaptibility, if an annotation was defined but does not match :attr:`type`, then the name is used as a fallback if available. """ name: str | None type: type def find( self, parameters: tuple[inspect.Parameter, ...], resolver: StrawberryResolver[Any], ) -> inspect.Parameter | None: # Go through all the types even after we've found one so we can # give a helpful error message if someone uses the type more than once. type_parameters = [] for parameter in parameters: annotation = resolver.strawberry_annotations[parameter] if isinstance(annotation, StrawberryAnnotation): try: evaled_annotation = annotation.evaluate() except NameError: # If this is a strawberry.Parent using ForwardRef, we will fail to # evaluate at this moment, but at least knowing that it is a reserved # type is enough for now # We might want to revisit this in the future, maybe by postponing # this check to when the schema is actually being created evaled_annotation = resolve_parent_forward_arg( annotation.annotation ) if self.is_reserved_type(evaled_annotation): type_parameters.append(parameter) if len(type_parameters) > 1: raise ConflictingArgumentsError( resolver, [parameter.name for parameter in type_parameters] ) if type_parameters: return type_parameters[0] # Fallback to matching by name if not self.name: return None reserved_name = ReservedName(name=self.name).find(parameters, resolver) if reserved_name: warning = DeprecationWarning( f"Argument name-based matching of '{self.name}' is deprecated and will " "be removed in v1.0. Ensure that reserved arguments are annotated " "their respective types (i.e. use value: 'DirectiveValue[str]' instead " "of 'value: str' and 'info: Info' instead of a plain 'info')." ) warnings.warn(warning, stacklevel=3) return reserved_name return None def is_reserved_type(self, other: builtins.type) -> bool: origin = cast("type", get_origin(other)) or other if origin is Annotated: # Handle annotated arguments such as Private[str] and DirectiveValue[str] return type_has_annotation(other, self.type) # Handle both concrete and generic types (i.e Info, and Info) return ( issubclass(origin, self.type) if isinstance(origin, type) else origin is self.type ) SELF_PARAMSPEC = ReservedNameBoundParameter("self") CLS_PARAMSPEC = ReservedNameBoundParameter("cls") ROOT_PARAMSPEC = ReservedName("root") INFO_PARAMSPEC = ReservedType("info", Info) PARENT_PARAMSPEC = ReservedType(name=None, type=StrawberryParent) T = TypeVar("T") # in python >= 3.12 coroutine functions are market using inspect.markcoroutinefunction, # which should be checked with inspect.iscoroutinefunction instead of asyncio.iscoroutinefunction if hasattr(inspect, "markcoroutinefunction"): iscoroutinefunction = inspect.iscoroutinefunction else: iscoroutinefunction = asyncio.iscoroutinefunction # type: ignore[assignment] class StrawberryResolver(Generic[T]): RESERVED_PARAMSPEC: tuple[ReservedParameterSpecification, ...] = ( SELF_PARAMSPEC, CLS_PARAMSPEC, ROOT_PARAMSPEC, INFO_PARAMSPEC, PARENT_PARAMSPEC, ) def __init__( self, func: Callable[..., T] | staticmethod | classmethod, *, description: str | None = None, type_override: StrawberryType | type | None = None, ) -> None: self.wrapped_func = func self._description = description self._type_override = type_override """Specify the type manually instead of calculating from wrapped func This is used when creating copies of types w/ generics """ # TODO: Use this when doing the actual resolving? How to deal with async resolvers? def __call__(self, *args: str, **kwargs: Any) -> T: if not callable(self.wrapped_func): raise UncallableResolverError(self) return self.wrapped_func(*args, **kwargs) @cached_property def signature(self) -> inspect.Signature: return Signature.from_callable(self._unbound_wrapped_func, follow_wrapped=True) # TODO: find better name @cached_property def strawberry_annotations( self, ) -> dict[inspect.Parameter, StrawberryAnnotation | None]: return { p: ( StrawberryAnnotation(p.annotation, namespace=self._namespace) if p.annotation is not inspect.Signature.empty else None ) for p in self.signature.parameters.values() } @cached_property def reserved_parameters( self, ) -> dict[ReservedParameterSpecification, inspect.Parameter | None]: """Mapping of reserved parameter specification to parameter.""" parameters = tuple(self.signature.parameters.values()) return {spec: spec.find(parameters, self) for spec in self.RESERVED_PARAMSPEC} @cached_property def arguments(self) -> list[StrawberryArgument]: """Resolver arguments exposed in the GraphQL Schema.""" root_parameter = self.reserved_parameters.get(ROOT_PARAMSPEC) parent_parameter = self.reserved_parameters.get(PARENT_PARAMSPEC) # TODO: Maybe use SELF_PARAMSPEC in the future? Right now # it would prevent some common pattern for integrations # (e.g. django) of typing the `root` parameters as the # type of the real object being used if ( root_parameter is not None and parent_parameter is not None and root_parameter.name != parent_parameter.name ): raise ConflictingArgumentsError( self, [root_parameter.name, parent_parameter.name], ) parameters = self.signature.parameters.values() reserved_parameters = set(self.reserved_parameters.values()) missing_annotations: list[str] = [] arguments: list[StrawberryArgument] = [] user_parameters = (p for p in parameters if p not in reserved_parameters) for param in user_parameters: annotation = self.strawberry_annotations[param] if annotation is None: missing_annotations.append(param.name) else: argument = StrawberryArgument( python_name=param.name, graphql_name=None, type_annotation=annotation, default=param.default, ) arguments.append(argument) if missing_annotations: raise MissingArgumentsAnnotationsError(self, missing_annotations) return arguments @cached_property def info_parameter(self) -> inspect.Parameter | None: return self.reserved_parameters.get(INFO_PARAMSPEC) @cached_property def root_parameter(self) -> inspect.Parameter | None: return self.reserved_parameters.get(ROOT_PARAMSPEC) @cached_property def self_parameter(self) -> inspect.Parameter | None: return self.reserved_parameters.get(SELF_PARAMSPEC) @cached_property def parent_parameter(self) -> inspect.Parameter | None: return self.reserved_parameters.get(PARENT_PARAMSPEC) @cached_property def name(self) -> str: # TODO: What to do if resolver is a lambda? return self._unbound_wrapped_func.__name__ # TODO: consider deprecating @cached_property def annotations(self) -> dict[str, object]: """Annotations for the resolver. Does not include special args defined in `RESERVED_PARAMSPEC` (e.g. self, root, info) """ reserved_parameters = self.reserved_parameters reserved_names = {p.name for p in reserved_parameters.values() if p is not None} annotations = self._unbound_wrapped_func.__annotations__ return { name: annotation for name, annotation in annotations.items() if name not in reserved_names } @cached_property def type_annotation(self) -> StrawberryAnnotation | None: return_annotation = self.signature.return_annotation if return_annotation is inspect.Signature.empty: return None return StrawberryAnnotation( annotation=return_annotation, namespace=self._namespace ) @property def type(self) -> StrawberryType | type | None: if self._type_override: return self._type_override if self.type_annotation is None: return None return self.type_annotation.resolve() @property def is_graphql_generic(self) -> bool: from strawberry.schema.compat import is_graphql_generic has_generic_arguments = any( argument.is_graphql_generic for argument in self.arguments ) return has_generic_arguments or bool( self.type and is_graphql_generic(self.type) ) @cached_property def is_async(self) -> bool: return iscoroutinefunction(self._unbound_wrapped_func) or isasyncgenfunction( self._unbound_wrapped_func ) def copy_with( self, type_var_map: Mapping[str, StrawberryType | builtins.type] ) -> StrawberryResolver: type_override = None if self.type: if isinstance(self.type, StrawberryType): type_override = self.type.copy_with(type_var_map) elif has_object_definition(self.type): type_override = self.type.__strawberry_definition__.copy_with( type_var_map, ) other = type(self)( func=self.wrapped_func, description=self._description, type_override=type_override, ) # Resolve generic arguments for argument in other.arguments: if ( isinstance(argument.type, StrawberryType) and argument.type.is_graphql_generic ): argument.type_annotation = StrawberryAnnotation( annotation=argument.type.copy_with(type_var_map), namespace=argument.type_annotation.namespace, ) return other @cached_property def _namespace(self) -> dict[str, Any]: return sys.modules[self._unbound_wrapped_func.__module__].__dict__ @cached_property def _unbound_wrapped_func(self) -> Callable[..., T]: if isinstance(self.wrapped_func, (staticmethod, classmethod)): return self.wrapped_func.__func__ return self.wrapped_func class UncallableResolverError(Exception): def __init__(self, resolver: StrawberryResolver) -> None: message = ( f"Attempted to call resolver {resolver} with uncallable function " f"{resolver.wrapped_func}" ) super().__init__(message) __all__ = ["StrawberryResolver"] strawberry-graphql-0.287.0/strawberry/types/graphql.py000066400000000000000000000015431511033167500231150ustar00rootroot00000000000000from __future__ import annotations import enum from typing import TYPE_CHECKING if TYPE_CHECKING: from strawberry.http.types import HTTPMethod class OperationType(enum.Enum): QUERY = "query" MUTATION = "mutation" SUBSCRIPTION = "subscription" @staticmethod def from_http(method: HTTPMethod) -> set[OperationType]: if method == "GET": return { OperationType.QUERY, # subscriptions are supported via GET in the multipart protocol OperationType.SUBSCRIPTION, } if method == "POST": return { OperationType.QUERY, OperationType.MUTATION, OperationType.SUBSCRIPTION, } raise ValueError(f"Unsupported HTTP method: {method}") # pragma: no cover __all__ = ["OperationType"] strawberry-graphql-0.287.0/strawberry/types/info.py000066400000000000000000000111321511033167500224050ustar00rootroot00000000000000from __future__ import annotations import dataclasses import warnings from functools import cached_property from typing import ( TYPE_CHECKING, Any, Generic, ) from typing_extensions import TypeVar from .nodes import convert_selections if TYPE_CHECKING: from graphql import GraphQLResolveInfo, OperationDefinitionNode from graphql.language import FieldNode from graphql.pyutils.path import Path from strawberry.schema import Schema from strawberry.types.arguments import StrawberryArgument from strawberry.types.field import FieldType, StrawberryField from .nodes import Selection ContextType = TypeVar("ContextType", default=Any) RootValueType = TypeVar("RootValueType", default=Any) @dataclasses.dataclass class Info(Generic[ContextType, RootValueType]): """Class containing information about the current execution. This class is passed to resolvers when there's an argument with type `Info`. Example: ```python import strawberry @strawberry.type class Query: @strawberry.field def hello(self, info: strawberry.Info) -> str: return f"Hello, {info.context['name']}!" ``` It also supports passing the type of the context and root types: ```python import strawberry @strawberry.type class Query: @strawberry.field def hello(self, info: strawberry.Info[str, str]) -> str: return f"Hello, {info.context}!" ``` """ _raw_info: GraphQLResolveInfo _field: StrawberryField def __class_getitem__(cls, types: type | tuple[type, ...]) -> type[Info]: """Workaround for when passing only one type. Python doesn't yet support directly passing only one type to a generic class that has typevars with defaults. This is a workaround for that. See: https://discuss.python.org/t/passing-only-one-typevar-of-two-when-using-defaults/49134 """ if not isinstance(types, tuple): types = (types, Any) return super().__class_getitem__(types) # type: ignore @property def field_name(self) -> str: """The name of the current field being resolved.""" return self._raw_info.field_name @property def schema(self) -> Schema: """The schema of the current execution.""" return self._raw_info.schema._strawberry_schema # type: ignore @property def field_nodes(self) -> list[FieldNode]: # deprecated warnings.warn( "`info.field_nodes` is deprecated, use `selected_fields` instead", DeprecationWarning, stacklevel=2, ) return self._raw_info.field_nodes @cached_property def selected_fields(self) -> list[Selection]: """The fields that were selected on the current field's type.""" info = self._raw_info return convert_selections(info, info.field_nodes) @property def context(self) -> ContextType: """The context passed to the query execution.""" return self._raw_info.context @property def input_extensions(self) -> dict[str, Any]: """The input extensions passed to the query execution.""" return self._raw_info.operation_extensions # type: ignore @property def root_value(self) -> RootValueType: """The root value passed to the query execution.""" return self._raw_info.root_value @property def variable_values(self) -> dict[str, Any]: """The variable values passed to the query execution.""" return self._raw_info.variable_values @property def return_type( self, ) -> FieldType: """The return type of the current field being resolved.""" return self._field.type @property def python_name(self) -> str: """The name of the current field being resolved in Python format.""" return self._field.python_name # TODO: create an abstraction on these fields @property def operation(self) -> OperationDefinitionNode: """The operation being executed.""" return self._raw_info.operation @property def path(self) -> Path: """The path of the current field being resolved.""" return self._raw_info.path # TODO: parent_type as strawberry types # Helper functions def get_argument_definition(self, name: str) -> StrawberryArgument | None: """Get the StrawberryArgument definition for the current field by name.""" try: return next(arg for arg in self._field.arguments if arg.python_name == name) except StopIteration: return None __all__ = ["Info"] strawberry-graphql-0.287.0/strawberry/types/lazy_type.py000066400000000000000000000117251511033167500235020ustar00rootroot00000000000000import importlib import inspect import sys import warnings from dataclasses import dataclass from pathlib import Path from typing import ( Any, ForwardRef, Generic, TypeVar, Union, cast, ) from typing_extensions import Self TypeName = TypeVar("TypeName") Module = TypeVar("Module") Other = TypeVar("Other") @dataclass(frozen=True) class LazyType(Generic[TypeName, Module]): """A class that represents a type that will be resolved at runtime. This is useful when you have circular dependencies between types. This class is not meant to be used directly, instead use the `strawberry.lazy` function. """ type_name: str module: str package: str | None = None def __class_getitem__(cls, params: tuple[str, str]) -> "Self": warnings.warn( ( "LazyType is deprecated, use " "Annotated[YourType, strawberry.lazy(path)] instead" ), DeprecationWarning, stacklevel=2, ) type_name, module = params package = None if module.startswith("."): current_frame = inspect.currentframe() assert current_frame is not None assert current_frame.f_back is not None package = current_frame.f_back.f_globals["__package__"] return cls(type_name, module, package) def __or__(self, other: Other) -> object: return Union[self, other] # noqa: UP007 def resolve_type(self) -> type[Any]: module = importlib.import_module(self.module, self.package) main_module = sys.modules.get("__main__", None) if main_module: # If lazy type points to the main module, use it instead of the imported # module. Otherwise duplication checks during schema-conversion might fail. # Refer to: https://github.com/strawberry-graphql/strawberry/issues/2397 if main_module.__spec__ and main_module.__spec__.name == self.module: module = main_module elif hasattr(main_module, "__file__") and hasattr(module, "__file__"): main_file = main_module.__file__ module_file = module.__file__ if main_file and module_file: try: is_samefile = Path(main_file).samefile(module_file) except FileNotFoundError: # Can be raised when run through the CLI as the __main__ file # path contains `strawberry.exe` is_samefile = False module = main_module if is_samefile else module return module.__dict__[self.type_name] # this empty call method allows LazyTypes to be used in generic types # for example: list[LazyType["A", "module"]] def __call__(self) -> None: # pragma: no cover return None class StrawberryLazyReference: """A class that represents a lazy reference to a type in another module. This is useful when you have circular dependencies between types. This class is not meant to be used directly, instead use the `strawberry.lazy` function. """ def __init__(self, module: str) -> None: self.module = module self.package = None if module.startswith("."): frame = sys._getframe(2) # TODO: raise a nice error if frame is None assert frame is not None self.package = cast("str", frame.f_globals["__package__"]) def resolve_forward_ref(self, forward_ref: ForwardRef) -> LazyType: return LazyType(forward_ref.__forward_arg__, self.module, self.package) def __eq__(self, other: object) -> bool: if not isinstance(other, StrawberryLazyReference): return NotImplemented return self.module == other.module and self.package == other.package def __hash__(self) -> int: return hash((self.__class__, self.module, self.package)) def lazy(module_path: str) -> StrawberryLazyReference: """Creates a lazy reference to a type in another module. Args: module_path: The path to the module containing the type, supports relative paths starting with `.` Returns: A `StrawberryLazyReference` object that can be used to reference a type in another module. This is useful when you have circular dependencies between types. For example, assuming you have a `Post` type that has a field `author` that references a `User` type (which also has a field `posts` that references a list of `Post`), you can use `strawberry.lazy` to avoid the circular dependency: ```python from typing import TYPE_CHECKING, Annotated import strawberry if TYPE_CHECKING: from .users import User @strawberry.type class Post: title: str author: Annotated["User", strawberry.lazy(".users")] ``` """ return StrawberryLazyReference(module_path) __all__ = ["LazyType", "StrawberryLazyReference", "lazy"] strawberry-graphql-0.287.0/strawberry/types/maybe.py000066400000000000000000000027441511033167500225600ustar00rootroot00000000000000import re import typing from typing import TYPE_CHECKING, Any, Generic, TypeAlias, TypeVar T = TypeVar("T") class Some(Generic[T]): """A special value that can be used to represent an unset value in a field or argument. Similar to `undefined` in JavaScript, this value can be used to differentiate between a field that was not set and a field that was set to `None` or `null`. """ __slots__ = ("value",) def __init__(self, value: T) -> None: self.value = value def __repr__(self) -> str: return f"Some({self.value!r})" def __eq__(self, other: object) -> bool: return self.value == other.value if isinstance(other, Some) else False def __hash__(self) -> int: return hash(self.value) def __bool__(self) -> bool: return True if TYPE_CHECKING: Maybe: TypeAlias = Some[T] | None else: # we do this trick so we can inspect that at runtime class Maybe(Generic[T]): ... _maybe_re = re.compile(r"^(?:strawberry\.)?Maybe\[(.+)\]$") def _annotation_is_maybe(annotation: Any) -> bool: if isinstance(annotation, str): # Ideally we would try to evaluate the annotation, but the args inside # may still not be available, as the module is still being constructed. # Checking for the pattern should be good enough for now. return _maybe_re.match(annotation) is not None return (orig := typing.get_origin(annotation)) and orig is Maybe __all__ = [ "Maybe", "Some", ] strawberry-graphql-0.287.0/strawberry/types/mutation.py000066400000000000000000000264351511033167500233260ustar00rootroot00000000000000from __future__ import annotations import dataclasses from typing import ( TYPE_CHECKING, Any, Literal, overload, ) from strawberry.types.field import ( _RESOLVER_TYPE, _RESOLVER_TYPE_ASYNC, _RESOLVER_TYPE_SYNC, StrawberryField, T, field, ) if TYPE_CHECKING: from collections.abc import Callable, Mapping, Sequence from typing import Literal from strawberry.extensions.field_extension import FieldExtension from strawberry.permission import BasePermission # NOTE: we are separating the sync and async resolvers because using both # in the same function will cause mypy to raise an error. Not sure if it is a bug @overload def mutation( *, resolver: _RESOLVER_TYPE_ASYNC[T], name: str | None = None, description: str | None = None, init: Literal[False] = False, permission_classes: list[type[BasePermission]] | None = None, deprecation_reason: str | None = None, default: Any = dataclasses.MISSING, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: Mapping[Any, Any] | None = None, directives: Sequence[object] | None = (), extensions: list[FieldExtension] | None = None, graphql_type: Any | None = None, ) -> T: ... @overload def mutation( *, resolver: _RESOLVER_TYPE_SYNC[T], name: str | None = None, description: str | None = None, init: Literal[False] = False, permission_classes: list[type[BasePermission]] | None = None, deprecation_reason: str | None = None, default: Any = dataclasses.MISSING, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: Mapping[Any, Any] | None = None, directives: Sequence[object] | None = (), extensions: list[FieldExtension] | None = None, graphql_type: Any | None = None, ) -> T: ... @overload def mutation( *, name: str | None = None, description: str | None = None, init: Literal[True] = True, permission_classes: list[type[BasePermission]] | None = None, deprecation_reason: str | None = None, default: Any = dataclasses.MISSING, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: Mapping[Any, Any] | None = None, directives: Sequence[object] | None = (), extensions: list[FieldExtension] | None = None, graphql_type: Any | None = None, ) -> Any: ... @overload def mutation( resolver: _RESOLVER_TYPE_ASYNC[T], *, name: str | None = None, description: str | None = None, permission_classes: list[type[BasePermission]] | None = None, deprecation_reason: str | None = None, default: Any = dataclasses.MISSING, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: Mapping[Any, Any] | None = None, directives: Sequence[object] | None = (), extensions: list[FieldExtension] | None = None, graphql_type: Any | None = None, ) -> StrawberryField: ... @overload def mutation( resolver: _RESOLVER_TYPE_SYNC[T], *, name: str | None = None, description: str | None = None, permission_classes: list[type[BasePermission]] | None = None, deprecation_reason: str | None = None, default: Any = dataclasses.MISSING, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: Mapping[Any, Any] | None = None, directives: Sequence[object] | None = (), extensions: list[FieldExtension] | None = None, graphql_type: Any | None = None, ) -> StrawberryField: ... def mutation( resolver: _RESOLVER_TYPE[Any] | None = None, *, name: str | None = None, description: str | None = None, permission_classes: list[type[BasePermission]] | None = None, deprecation_reason: str | None = None, default: Any = dataclasses.MISSING, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: Mapping[Any, Any] | None = None, directives: Sequence[object] | None = (), extensions: list[FieldExtension] | None = None, graphql_type: Any | None = None, # This init parameter is used by PyRight to determine whether this field # is added in the constructor or not. It is not used to change # any behavior at the moment. init: Literal[True, False] | None = None, ) -> Any: """Annotates a method or property as a GraphQL mutation. Args: resolver: The resolver for the field. It can be a sync or async function. name: The GraphQL name of the field. description: The GraphQL description of the field. permission_classes: The permission classes required to access the field. deprecation_reason: The deprecation reason for the field. default: The default value for the field. default_factory: The default factory for the field. metadata: The metadata for the field. directives: The directives for the field. extensions: The extensions for the field. graphql_type: The GraphQL type for the field, useful when you want to use a different type in the resolver than the one in the schema. init: This parameter is used by PyRight to determine whether this field is added in the constructor or not. It is not used to change any behavior at the moment. Returns: The field object. This is normally used inside a type declaration: ```python import strawberry @strawberry.type class Mutation: @strawberry.mutation def create_post(self, title: str, content: str) -> Post: ... ``` It can be used both as decorator and as a normal function. """ return field( resolver=resolver, # type: ignore name=name, description=description, permission_classes=permission_classes, deprecation_reason=deprecation_reason, default=default, default_factory=default_factory, metadata=metadata, directives=directives, extensions=extensions, graphql_type=graphql_type, ) # NOTE: we are separating the sync and async resolvers because using both # in the same function will cause mypy to raise an error. Not sure if it is a bug @overload def subscription( *, resolver: _RESOLVER_TYPE_ASYNC[T], name: str | None = None, description: str | None = None, init: Literal[False] = False, permission_classes: list[type[BasePermission]] | None = None, deprecation_reason: str | None = None, default: Any = dataclasses.MISSING, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: Mapping[Any, Any] | None = None, directives: Sequence[object] | None = (), extensions: list[FieldExtension] | None = None, graphql_type: Any | None = None, ) -> T: ... @overload def subscription( *, resolver: _RESOLVER_TYPE_SYNC[T], name: str | None = None, description: str | None = None, init: Literal[False] = False, permission_classes: list[type[BasePermission]] | None = None, deprecation_reason: str | None = None, default: Any = dataclasses.MISSING, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: Mapping[Any, Any] | None = None, directives: Sequence[object] | None = (), extensions: list[FieldExtension] | None = None, graphql_type: Any | None = None, ) -> T: ... @overload def subscription( *, name: str | None = None, description: str | None = None, init: Literal[True] = True, permission_classes: list[type[BasePermission]] | None = None, deprecation_reason: str | None = None, default: Any = dataclasses.MISSING, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: Mapping[Any, Any] | None = None, directives: Sequence[object] | None = (), extensions: list[FieldExtension] | None = None, graphql_type: Any | None = None, ) -> Any: ... @overload def subscription( resolver: _RESOLVER_TYPE_ASYNC[T], *, name: str | None = None, description: str | None = None, permission_classes: list[type[BasePermission]] | None = None, deprecation_reason: str | None = None, default: Any = dataclasses.MISSING, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: Mapping[Any, Any] | None = None, directives: Sequence[object] | None = (), extensions: list[FieldExtension] | None = None, graphql_type: Any | None = None, ) -> StrawberryField: ... @overload def subscription( resolver: _RESOLVER_TYPE_SYNC[T], *, name: str | None = None, description: str | None = None, permission_classes: list[type[BasePermission]] | None = None, deprecation_reason: str | None = None, default: Any = dataclasses.MISSING, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: Mapping[Any, Any] | None = None, directives: Sequence[object] | None = (), extensions: list[FieldExtension] | None = None, graphql_type: Any | None = None, ) -> StrawberryField: ... def subscription( resolver: _RESOLVER_TYPE[Any] | None = None, *, name: str | None = None, description: str | None = None, permission_classes: list[type[BasePermission]] | None = None, deprecation_reason: str | None = None, default: Any = dataclasses.MISSING, default_factory: Callable[..., object] | object = dataclasses.MISSING, metadata: Mapping[Any, Any] | None = None, directives: Sequence[object] | None = (), extensions: list[FieldExtension] | None = None, graphql_type: Any | None = None, init: Literal[True, False] | None = None, ) -> Any: """Annotates a method or property as a GraphQL subscription. Args: resolver: The resolver for the field. name: The GraphQL name of the field. description: The GraphQL description of the field. permission_classes: The permission classes required to access the field. deprecation_reason: The deprecation reason for the field. default: The default value for the field. default_factory: The default factory for the field. metadata: The metadata for the field. directives: The directives for the field. extensions: The extensions for the field. graphql_type: The GraphQL type for the field, useful when you want to use a different type in the resolver than the one in the schema. init: This parameter is used by PyRight to determine whether this field is added in the constructor or not. It is not used to change any behavior at the moment. Returns: The field for the subscription. This is normally used inside a type declaration: ```python import strawberry @strawberry.type class Subscription: @strawberry.subscription def post_created(self, title: str, content: str) -> Post: ... ``` it can be used both as decorator and as a normal function. """ return field( resolver=resolver, # type: ignore name=name, description=description, is_subscription=True, permission_classes=permission_classes, deprecation_reason=deprecation_reason, default=default, default_factory=default_factory, metadata=metadata, directives=directives, extensions=extensions, graphql_type=graphql_type, ) __all__ = ["mutation", "subscription"] strawberry-graphql-0.287.0/strawberry/types/nodes.py000066400000000000000000000120011511033167500225560ustar00rootroot00000000000000"""Abstraction layer for graphql-core field nodes. Call `convert_sections` on a list of GraphQL `FieldNode`s, such as in `info.field_nodes`. If a node has only one useful value, it's value is inlined. If a list of nodes have unique names, it's transformed into a mapping. Note Python dicts maintain ordering (for all supported versions). """ from __future__ import annotations import dataclasses from typing import TYPE_CHECKING, Any, Union from graphql.language import FieldNode as GQLFieldNode from graphql.language import FragmentSpreadNode as GQLFragmentSpreadNode from graphql.language import InlineFragmentNode as GQLInlineFragmentNode from graphql.language import ListValueNode as GQLListValueNode from graphql.language import ObjectValueNode as GQLObjectValueNode from graphql.language import VariableNode as GQLVariableNode if TYPE_CHECKING: from collections.abc import Collection, Iterable from graphql import GraphQLResolveInfo from graphql.language import ArgumentNode as GQLArgumentNode from graphql.language import DirectiveNode as GQLDirectiveNode from graphql.language import ValueNode as GQLValueNode Arguments = dict[str, Any] Directives = dict[str, Arguments] Selection = Union["SelectedField", "FragmentSpread", "InlineFragment"] def convert_value(info: GraphQLResolveInfo, node: GQLValueNode) -> Any: """Return useful value from any node.""" if isinstance(node, GQLVariableNode): # Look up variable name = node.name.value return info.variable_values.get(name) if isinstance(node, GQLListValueNode): return [convert_value(info, value) for value in node.values] if isinstance(node, GQLObjectValueNode): return { field.name.value: convert_value(info, field.value) for field in node.fields } return getattr(node, "value", None) def convert_arguments( info: GraphQLResolveInfo, nodes: Iterable[GQLArgumentNode] ) -> Arguments: """Return mapping of arguments.""" return {node.name.value: convert_value(info, node.value) for node in nodes} def convert_directives( info: GraphQLResolveInfo, nodes: Iterable[GQLDirectiveNode] ) -> Directives: """Return mapping of directives.""" return {node.name.value: convert_arguments(info, node.arguments) for node in nodes} def convert_selections( info: GraphQLResolveInfo, field_nodes: Collection[GQLFieldNode] ) -> list[Selection]: """Return typed `Selection` based on node type.""" selections: list[Selection] = [] for node in field_nodes: if isinstance(node, GQLFieldNode): selections.append(SelectedField.from_node(info, node)) elif isinstance(node, GQLInlineFragmentNode): selections.append(InlineFragment.from_node(info, node)) elif isinstance(node, GQLFragmentSpreadNode): selections.append(FragmentSpread.from_node(info, node)) else: raise TypeError(f"Unknown node type: {node}") return selections @dataclasses.dataclass class FragmentSpread: """Wrapper for a FragmentSpreadNode.""" name: str type_condition: str directives: Directives selections: list[Selection] @classmethod def from_node( cls, info: GraphQLResolveInfo, node: GQLFragmentSpreadNode, ) -> FragmentSpread: # Look up fragment name = node.name.value fragment = info.fragments[name] return cls( name=name, directives=convert_directives(info, node.directives), type_condition=fragment.type_condition.name.value, selections=convert_selections( info, getattr(fragment.selection_set, "selections", []) ), ) @dataclasses.dataclass class InlineFragment: """Wrapper for a InlineFragmentNode.""" type_condition: str selections: list[Selection] directives: Directives @classmethod def from_node( cls, info: GraphQLResolveInfo, node: GQLInlineFragmentNode, ) -> InlineFragment: return cls( type_condition=node.type_condition.name.value, selections=convert_selections( info, getattr(node.selection_set, "selections", []) ), directives=convert_directives(info, node.directives), ) @dataclasses.dataclass class SelectedField: """Wrapper for a FieldNode.""" name: str directives: Directives arguments: Arguments selections: list[Selection] alias: str | None = None @classmethod def from_node(cls, info: GraphQLResolveInfo, node: GQLFieldNode) -> SelectedField: return cls( name=node.name.value, directives=convert_directives(info, node.directives), alias=getattr(node.alias, "value", None), arguments=convert_arguments(info, node.arguments), selections=convert_selections( info, getattr(node.selection_set, "selections", []) ), ) __all__ = ["FragmentSpread", "InlineFragment", "SelectedField", "convert_selections"] strawberry-graphql-0.287.0/strawberry/types/object_type.py000066400000000000000000000355131511033167500237720ustar00rootroot00000000000000import builtins import dataclasses import inspect import types from collections.abc import Callable, Sequence from typing import ( Any, TypeVar, overload, ) from typing_extensions import dataclass_transform, get_annotations from strawberry.exceptions import ( InvalidSuperclassInterfaceError, MissingFieldAnnotationError, MissingReturnAnnotationError, ObjectIsNotClassError, ) from strawberry.types.base import get_object_definition from strawberry.types.maybe import _annotation_is_maybe from strawberry.utils.deprecations import DEPRECATION_MESSAGES, DeprecatedDescriptor from strawberry.utils.str_converters import to_camel_case from .base import StrawberryObjectDefinition from .field import StrawberryField, field from .type_resolver import _get_fields T = TypeVar("T", bound=builtins.type) def _get_interfaces(cls: builtins.type[Any]) -> list[StrawberryObjectDefinition]: interfaces: list[StrawberryObjectDefinition] = [] for base in cls.__mro__[1:]: # Exclude current class type_definition = get_object_definition(base) if type_definition and type_definition.is_interface: interfaces.append(type_definition) return interfaces def _check_field_annotations(cls: builtins.type[Any]) -> None: """Are any of the dataclass Fields missing type annotations? This is similar to the check that dataclasses do during creation, but allows us to manually add fields to cls' __annotations__ or raise proper Strawberry exceptions if necessary https://github.com/python/cpython/blob/6fed3c85402c5ca704eb3f3189ca3f5c67a08d19/Lib/dataclasses.py#L881-L884 """ cls_annotations = get_annotations(cls) # TODO: do we need this? cls.__annotations__ = cls_annotations for field_name, field_ in cls.__dict__.items(): if not isinstance(field_, (StrawberryField, dataclasses.Field)): # Not a dataclasses.Field, nor a StrawberryField. Ignore continue # If the field is a StrawberryField we need to do a bit of extra work # to make sure dataclasses.dataclass is ready for it if isinstance(field_, StrawberryField): # If the field has a type override then use that instead of using # the class annotations or resolver annotation if field_.type_annotation is not None: if field_name not in cls_annotations: cls_annotations[field_name] = field_.type_annotation.annotation continue # Make sure the cls has an annotation if field_name not in cls_annotations: # If the field uses the default resolver, the field _must_ be # annotated if not field_.base_resolver: raise MissingFieldAnnotationError(field_name, cls) # The resolver _must_ have a return type annotation # TODO: Maybe check this immediately when adding resolver to # field if field_.base_resolver.type_annotation is None: raise MissingReturnAnnotationError( field_name, resolver=field_.base_resolver ) if field_name not in cls_annotations: cls_annotations[field_name] = field_.base_resolver.type_annotation # TODO: Make sure the cls annotation agrees with the field's type # >>> if cls_annotations[field_name] != field.base_resolver.type: # >>> # TODO: Proper error # >>> raise Exception # If somehow a non-StrawberryField field is added to the cls without annotations # it raises an exception. This would occur if someone manually uses # dataclasses.field if field_name not in cls_annotations: # Field object exists but did not get an annotation raise MissingFieldAnnotationError(field_name, cls) def _wrap_dataclass(cls: builtins.type[T]) -> builtins.type[T]: """Wrap a strawberry.type class with a dataclass and check for any issues before doing so.""" # Ensure all Fields have been properly type-annotated _check_field_annotations(cls) return dataclasses.dataclass(kw_only=True)(cls) def _inject_default_for_maybe_annotations( cls: builtins.type[T], annotations: dict[str, Any] ) -> None: """Inject `= None` for fields with `Maybe` annotations and no default value.""" for name, annotation in annotations.copy().items(): if _annotation_is_maybe(annotation) and not hasattr(cls, name): setattr(cls, name, None) def _process_type( cls: T, *, name: str | None = None, is_input: bool = False, is_interface: bool = False, description: str | None = None, directives: Sequence[object] | None = (), extend: bool = False, original_type_annotations: dict[str, Any] | None = None, ) -> T: name = name or to_camel_case(cls.__name__) original_type_annotations = original_type_annotations or {} interfaces = _get_interfaces(cls) fields = _get_fields(cls, original_type_annotations) is_type_of = getattr(cls, "is_type_of", None) resolve_type = getattr(cls, "resolve_type", None) if is_input and interfaces: raise InvalidSuperclassInterfaceError( cls=cls, input_name=name, interfaces=interfaces ) cls.__strawberry_definition__ = StrawberryObjectDefinition( # type: ignore[attr-defined] name=name, is_input=is_input, is_interface=is_interface, interfaces=interfaces, description=description, directives=directives, origin=cls, extend=extend, fields=fields, is_type_of=is_type_of, resolve_type=resolve_type, ) # TODO: remove when deprecating _type_definition DeprecatedDescriptor( DEPRECATION_MESSAGES._TYPE_DEFINITION, cls.__strawberry_definition__, # type: ignore[attr-defined] "_type_definition", ).inject(cls) # dataclasses removes attributes from the class here: # https://github.com/python/cpython/blob/577d7c4e/Lib/dataclasses.py#L873-L880 # so we need to restore them, this will change in future, but for now this # solution should suffice for field_ in fields: if field_.base_resolver and field_.python_name: wrapped_func = field_.base_resolver.wrapped_func # Bind the functions to the class object. This is necessary because when # the @strawberry.field decorator is used on @staticmethod/@classmethods, # we get the raw staticmethod/classmethod objects before class evaluation # binds them to the class. We need to do this manually. if isinstance(wrapped_func, staticmethod): bound_method = wrapped_func.__get__(cls) field_.base_resolver.wrapped_func = bound_method elif isinstance(wrapped_func, classmethod): bound_method = types.MethodType(wrapped_func.__func__, cls) field_.base_resolver.wrapped_func = bound_method setattr(cls, field_.python_name, wrapped_func) return cls @overload @dataclass_transform( order_default=True, kw_only_default=True, field_specifiers=(field, StrawberryField) ) def type( cls: T, *, name: str | None = None, is_input: bool = False, is_interface: bool = False, description: str | None = None, directives: Sequence[object] | None = (), extend: bool = False, ) -> T: ... @overload @dataclass_transform( order_default=True, kw_only_default=True, field_specifiers=(field, StrawberryField) ) def type( *, name: str | None = None, is_input: bool = False, is_interface: bool = False, description: str | None = None, directives: Sequence[object] | None = (), extend: bool = False, ) -> Callable[[T], T]: ... def type( cls: T | None = None, *, name: str | None = None, is_input: bool = False, is_interface: bool = False, description: str | None = None, directives: Sequence[object] | None = (), extend: bool = False, ) -> T | Callable[[T], T]: """Annotates a class as a GraphQL type. Similar to `dataclasses.dataclass`, but with additional functionality for defining GraphQL types. Args: cls: The class we want to create a GraphQL type from. name: The name of the GraphQL type. is_input: Whether the class is an input type. Used internally, use `@strawerry.input` instead of passing this flag. is_interface: Whether the class is an interface. Used internally, use `@strawerry.interface` instead of passing this flag. description: The description of the GraphQL type. directives: The directives of the GraphQL type. extend: Whether the class is extending an existing type. Returns: The class. Example usage: ```python @strawberry.type class User: name: str = "A name" ``` You can also pass parameters to the decorator: ```python @strawberry.type(name="UserType", description="A user type") class MyUser: name: str = "A name" ``` """ def wrap(cls: T) -> T: if not inspect.isclass(cls): if is_input: exc = ObjectIsNotClassError.input elif is_interface: exc = ObjectIsNotClassError.interface else: exc = ObjectIsNotClassError.type raise exc(cls) # when running `_wrap_dataclass` we lose some of the information about the # the passed types, especially the type_annotation inside the StrawberryField # this makes it impossible to customise the field type, like this: # >>> @strawberry.type # >>> class Query: # >>> a: int = strawberry.field(graphql_type=str) # so we need to extract the information before running `_wrap_dataclass` original_type_annotations: dict[str, Any] = {} annotations = getattr(cls, "__annotations__", {}) for field_name in annotations: field = getattr(cls, field_name, None) if field and isinstance(field, StrawberryField) and field.type_annotation: original_type_annotations[field_name] = field.type_annotation.annotation if is_input: _inject_default_for_maybe_annotations(cls, annotations) wrapped = _wrap_dataclass(cls) return _process_type( # type: ignore wrapped, name=name, is_input=is_input, is_interface=is_interface, description=description, directives=directives, extend=extend, original_type_annotations=original_type_annotations, ) if cls is None: return wrap return wrap(cls) @overload @dataclass_transform( order_default=True, kw_only_default=True, field_specifiers=(field, StrawberryField) ) def input( cls: T, *, name: str | None = None, one_of: bool | None = None, description: str | None = None, directives: Sequence[object] | None = (), ) -> T: ... @overload @dataclass_transform( order_default=True, kw_only_default=True, field_specifiers=(field, StrawberryField) ) def input( *, name: str | None = None, one_of: bool | None = None, description: str | None = None, directives: Sequence[object] | None = (), ) -> Callable[[T], T]: ... def input( cls: T | None = None, *, name: str | None = None, one_of: bool | None = None, description: str | None = None, directives: Sequence[object] | None = (), ): """Annotates a class as a GraphQL Input type. Similar to `@strawberry.type`, but for input types. Args: cls: The class we want to create a GraphQL input type from. name: The name of the GraphQL input type. description: The description of the GraphQL input type. directives: The directives of the GraphQL input type. one_of: Whether the input type is a `oneOf` type. Returns: The class. Example usage: ```python @strawberry.input class UserInput: name: str ``` You can also pass parameters to the decorator: ```python @strawberry.input(name="UserInputType", description="A user input type") class MyUserInput: name: str ``` """ from strawberry.schema_directives import OneOf if one_of: directives = (*(directives or ()), OneOf()) return type( # type: ignore # not sure why mypy complains here cls, name=name, description=description, directives=directives, is_input=True, ) @overload @dataclass_transform( order_default=True, kw_only_default=True, field_specifiers=(field, StrawberryField) ) def interface( cls: T, *, name: str | None = None, description: str | None = None, directives: Sequence[object] | None = (), ) -> T: ... @overload @dataclass_transform( order_default=True, kw_only_default=True, field_specifiers=(field, StrawberryField) ) def interface( *, name: str | None = None, description: str | None = None, directives: Sequence[object] | None = (), ) -> Callable[[T], T]: ... @dataclass_transform( order_default=True, kw_only_default=True, field_specifiers=(field, StrawberryField) ) def interface( cls: T | None = None, *, name: str | None = None, description: str | None = None, directives: Sequence[object] | None = (), ): """Annotates a class as a GraphQL Interface. Similar to `@strawberry.type`, but for interfaces. Args: cls: The class we want to create a GraphQL interface from. name: The name of the GraphQL interface. description: The description of the GraphQL interface. directives: The directives of the GraphQL interface. Returns: The class. Example usage: ```python @strawberry.interface class Node: id: str ``` You can also pass parameters to the decorator: ```python @strawberry.interface(name="NodeType", description="A node type") class MyNode: id: str ``` """ return type( # type: ignore # not sure why mypy complains here cls, name=name, description=description, directives=directives, is_interface=True, ) def asdict(obj: Any) -> dict[str, object]: """Convert a strawberry object into a dictionary. This wraps the dataclasses.asdict function to strawberry. Args: obj: The object to convert into a dictionary. Returns: A dictionary representation of the object. Example usage: ```python @strawberry.type class User: name: str age: int strawberry.asdict(User(name="Lorem", age=25)) # {"name": "Lorem", "age": 25} ``` """ return dataclasses.asdict(obj) __all__ = [ "StrawberryObjectDefinition", "asdict", "input", "interface", "type", ] strawberry-graphql-0.287.0/strawberry/types/private.py000066400000000000000000000010061511033167500231230ustar00rootroot00000000000000from typing import Annotated, TypeVar from strawberry.utils.typing import type_has_annotation class StrawberryPrivate: ... T = TypeVar("T") Private = Annotated[T, StrawberryPrivate()] """Represents a field that won't be exposed in the GraphQL schema. Example: ```python import strawberry @strawberry.type class User: name: str age: strawberry.Private[int] ``` """ def is_private(type_: object) -> bool: return type_has_annotation(type_, StrawberryPrivate) __all__ = ["Private", "is_private"] strawberry-graphql-0.287.0/strawberry/types/scalar.py000066400000000000000000000140111511033167500227160ustar00rootroot00000000000000from __future__ import annotations import sys from dataclasses import dataclass from typing import ( TYPE_CHECKING, Any, NewType, Optional, TypeVar, overload, ) from strawberry.exceptions import InvalidUnionTypeError from strawberry.types.base import StrawberryType from strawberry.utils.str_converters import to_camel_case if TYPE_CHECKING: from collections.abc import Callable, Iterable, Mapping from graphql import GraphQLScalarType _T = TypeVar("_T", bound=type | NewType) def identity(x: _T) -> _T: return x @dataclass class ScalarDefinition(StrawberryType): name: str description: str | None specified_by_url: str | None serialize: Callable | None parse_value: Callable | None parse_literal: Callable | None directives: Iterable[object] = () origin: GraphQLScalarType | type | None = None # Optionally store the GraphQLScalarType instance so that we don't get # duplicates implementation: GraphQLScalarType | None = None # used for better error messages _source_file: str | None = None _source_line: int | None = None def copy_with( self, type_var_map: Mapping[str, StrawberryType | type] ) -> StrawberryType | type: return super().copy_with(type_var_map) # type: ignore[safe-super] @property def is_graphql_generic(self) -> bool: return False class ScalarWrapper: _scalar_definition: ScalarDefinition def __init__(self, wrap: Callable[[Any], Any]) -> None: self.wrap = wrap def __call__(self, *args: str, **kwargs: Any) -> Any: return self.wrap(*args, **kwargs) def __or__(self, other: StrawberryType | type) -> StrawberryType: if other is None: # Return the correct notation when using `StrawberryUnion | None`. return Optional[self] # noqa: UP045 # Raise an error in any other case. # There is Work in progress to deal with more merging cases, see: # https://github.com/strawberry-graphql/strawberry/pull/1455 raise InvalidUnionTypeError(str(other), self.wrap) def _process_scalar( cls: _T, *, name: str | None = None, description: str | None = None, specified_by_url: str | None = None, serialize: Callable | None = None, parse_value: Callable | None = None, parse_literal: Callable | None = None, directives: Iterable[object] = (), ) -> ScalarWrapper: from strawberry.exceptions.handler import should_use_rich_exceptions name = name or to_camel_case(cls.__name__) # type: ignore[union-attr] _source_file = None _source_line = None if should_use_rich_exceptions(): frame = sys._getframe(3) _source_file = frame.f_code.co_filename _source_line = frame.f_lineno wrapper = ScalarWrapper(cls) wrapper._scalar_definition = ScalarDefinition( name=name, description=description, specified_by_url=specified_by_url, serialize=serialize, parse_literal=parse_literal, parse_value=parse_value, directives=directives, origin=cls, # type: ignore[arg-type] _source_file=_source_file, _source_line=_source_line, ) return wrapper @overload def scalar( *, name: str | None = None, description: str | None = None, specified_by_url: str | None = None, serialize: Callable = identity, parse_value: Callable | None = None, parse_literal: Callable | None = None, directives: Iterable[object] = (), ) -> Callable[[_T], _T]: ... @overload def scalar( cls: _T, *, name: str | None = None, description: str | None = None, specified_by_url: str | None = None, serialize: Callable = identity, parse_value: Callable | None = None, parse_literal: Callable | None = None, directives: Iterable[object] = (), ) -> _T: ... # TODO: We are tricking pyright into thinking that we are returning the given type # here or else it won't let us use any custom scalar to annotate attributes in # dataclasses/types. This should be properly solved when implementing StrawberryScalar def scalar( cls: _T | None = None, *, name: str | None = None, description: str | None = None, specified_by_url: str | None = None, serialize: Callable = identity, parse_value: Callable | None = None, parse_literal: Callable | None = None, directives: Iterable[object] = (), ) -> Any: """Annotates a class or type as a GraphQL custom scalar. Args: cls: The class or type to annotate. name: The GraphQL name of the scalar. description: The description of the scalar. specified_by_url: The URL of the specification. serialize: The function to serialize the scalar. parse_value: The function to parse the value. parse_literal: The function to parse the literal. directives: The directives to apply to the scalar. Returns: The decorated class or type. Example usages: ```python strawberry.scalar( datetime.date, serialize=lambda value: value.isoformat(), parse_value=datetime.parse_date, ) Base64Encoded = strawberry.scalar( NewType("Base64Encoded", bytes), serialize=base64.b64encode, parse_value=base64.b64decode, ) @strawberry.scalar( serialize=lambda value: ",".join(value.items), parse_value=lambda value: CustomList(value.split(",")), ) class CustomList: def __init__(self, items): self.items = items ``` """ if parse_value is None: parse_value = cls def wrap(cls: _T) -> ScalarWrapper: return _process_scalar( cls, name=name, description=description, specified_by_url=specified_by_url, serialize=serialize, parse_value=parse_value, parse_literal=parse_literal, directives=directives, ) if cls is None: return wrap return wrap(cls) __all__ = ["ScalarDefinition", "ScalarWrapper", "scalar"] strawberry-graphql-0.287.0/strawberry/types/type_resolver.py000066400000000000000000000146011511033167500243600ustar00rootroot00000000000000from __future__ import annotations import dataclasses import sys from typing import Any from strawberry.annotation import StrawberryAnnotation from strawberry.exceptions import ( FieldWithResolverAndDefaultFactoryError, FieldWithResolverAndDefaultValueError, PrivateStrawberryFieldError, ) from strawberry.types.base import has_object_definition from strawberry.types.field import StrawberryField from strawberry.types.private import is_private from strawberry.types.unset import UNSET def _get_fields( cls: type[Any], original_type_annotations: dict[str, type[Any]] ) -> list[StrawberryField]: """Get all the strawberry fields off a strawberry.type cls. This function returns a list of StrawberryFields (one for each field item), while also paying attention the name and typing of the field. StrawberryFields can be defined on a strawberry.type class as either a dataclass- style field or using strawberry.field as a decorator. ```python import strawberry @strawberry.type class Query: type_1a: int = 5 type_1b: int = strawberry.field(...) type_1c: int = strawberry.field(resolver=...) @strawberry.field def type_2(self) -> int: ... ``` Type #1: A pure dataclass-style field. Will not have a StrawberryField; one will need to be created in this function. Type annotation is required. Type #2: A field defined using @strawberry.field as a decorator around the resolver. The resolver must be type-annotated. The StrawberryField.python_name value will be assigned to the field's name on the class if one is not set by either using an explicit strawberry.field(name=...) or by passing a named function (i.e. not an anonymous lambda) to strawberry.field (typically as a decorator). """ fields: dict[str, StrawberryField] = {} # before trying to find any fields, let's first add the fields defined in # parent classes, we do this by checking if parents have a type definition for base in cls.__bases__: if has_object_definition(base): base_fields = { field.python_name: field for field in base.__strawberry_definition__.fields } # Add base's fields to cls' fields fields = {**fields, **base_fields} # Find the class the each field was originally defined on so we can use # that scope later when resolving the type, as it may have different names # available to it. origins: dict[str, type] = dict.fromkeys(cls.__annotations__, cls) for base in cls.__mro__: if has_object_definition(base): for field in base.__strawberry_definition__.fields: if field.python_name in base.__annotations__: origins.setdefault(field.name, base) # then we can proceed with finding the fields for the current class for field in dataclasses.fields(cls): # type: ignore if isinstance(field, StrawberryField): # Check that the field type is not Private if is_private(field.type): raise PrivateStrawberryFieldError(field.python_name, cls) # Check that default is not set if a resolver is defined if ( field.default is not dataclasses.MISSING and field.default is not UNSET and field.base_resolver is not None ): raise FieldWithResolverAndDefaultValueError( field.python_name, cls.__name__ ) # Check that default_factory is not set if a resolver is defined # Note: using getattr because of this issue: # https://github.com/python/mypy/issues/6910 default_factory = getattr(field, "default_factory", None) if ( default_factory is not dataclasses.MISSING and default_factory is not UNSET and field.base_resolver is not None ): raise FieldWithResolverAndDefaultFactoryError( field.python_name, cls.__name__ ) # we make sure that the origin is either the field's resolver when # called as: # # >>> @strawberry.field # ... def x(self): ... # # or the class where this field was defined, so we always have # the correct origin for determining field types when resolving # the types. field.origin = field.origin or cls # Set the correct namespace for annotations if a namespace isn't # already set # Note: We do this here rather in the `Strawberry.type` setter # function because at that point we don't have a link to the object # type that the field as attached to. if ( isinstance(field.type_annotation, StrawberryAnnotation) and field.type_annotation.namespace is None ): field.type_annotation.set_namespace_from_field(field) # Create a StrawberryField for fields that didn't use strawberry.field else: # Only ignore Private fields that weren't defined using StrawberryFields if is_private(field.type): continue origin = origins.get(field.name, cls) module = sys.modules[origin.__module__] # Create a StrawberryField, for fields of Types #1 and #2a field = StrawberryField( # noqa: PLW2901 python_name=field.name, graphql_name=None, type_annotation=StrawberryAnnotation( annotation=field.type, namespace=module.__dict__, ), origin=origin, default=getattr(cls, field.name, dataclasses.MISSING), ) field_name = field.python_name assert_message = "Field must have a name by the time the schema is generated" assert field_name is not None, assert_message if field.name in original_type_annotations: field.type = original_type_annotations[field.name] field.type_annotation = StrawberryAnnotation(annotation=field.type) # TODO: Raise exception if field_name already in fields fields[field_name] = field return list(fields.values()) __all__ = ["_get_fields"] strawberry-graphql-0.287.0/strawberry/types/union.py000066400000000000000000000236661511033167500226210ustar00rootroot00000000000000from __future__ import annotations import itertools import sys import warnings from itertools import chain from typing import ( TYPE_CHECKING, Annotated, Any, NoReturn, TypeVar, cast, get_origin, ) from graphql import GraphQLNamedType, GraphQLUnionType from strawberry.annotation import StrawberryAnnotation from strawberry.exceptions import ( InvalidTypeForUnionMergeError, InvalidUnionTypeError, UnallowedReturnTypeForUnion, WrongReturnTypeForUnion, ) from strawberry.exceptions.handler import should_use_rich_exceptions from strawberry.types.base import ( StrawberryOptional, StrawberryType, has_object_definition, ) from strawberry.types.lazy_type import LazyType if TYPE_CHECKING: from collections.abc import Collection, Iterable, Mapping from graphql import ( GraphQLAbstractType, GraphQLResolveInfo, GraphQLType, GraphQLTypeResolver, ) from strawberry.schema.types.concrete_type import TypeMap class StrawberryUnion(StrawberryType): # used for better error messages _source_file: str | None = None _source_line: int | None = None def __init__( self, name: str | None = None, type_annotations: tuple[StrawberryAnnotation, ...] = (), description: str | None = None, directives: Iterable[object] = (), ) -> None: self.graphql_name = name self.type_annotations = type_annotations self.description = description self.directives = directives self._source_file = None self._source_line = None self.concrete_of: StrawberryUnion | None = None def __eq__(self, other: object) -> bool: if isinstance(other, StrawberryType): if isinstance(other, StrawberryUnion): return ( self.graphql_name == other.graphql_name and self.type_annotations == other.type_annotations and self.description == other.description ) return False return super().__eq__(other) def __hash__(self) -> int: return hash((self.graphql_name, self.type_annotations, self.description)) def __or__(self, other: StrawberryType | type) -> StrawberryType: # TODO: this will be removed in future versions, you should # use Annotated[Union[...], strawberry.union(...)] instead if other is None: # Return the correct notation when using `StrawberryUnion | None`. return StrawberryOptional(of_type=self) raise InvalidTypeForUnionMergeError(self, other) @property def types(self) -> tuple[StrawberryType, ...]: return tuple( cast("StrawberryType", annotation.resolve()) for annotation in self.type_annotations ) @property def type_params(self) -> list[TypeVar]: def _get_type_params(type_: StrawberryType) -> list[TypeVar]: if isinstance(type_, LazyType): type_ = cast("StrawberryType", type_.resolve_type()) if has_object_definition(type_): parameters = getattr(type_, "__parameters__", None) return list(parameters) if parameters else [] return type_.type_params # TODO: check if order is important: # https://github.com/strawberry-graphql/strawberry/issues/445 return list( set(itertools.chain(*(_get_type_params(type_) for type_ in self.types))) ) @property def is_graphql_generic(self) -> bool: def _is_generic(type_: object) -> bool: if has_object_definition(type_): type_ = type_.__strawberry_definition__ if isinstance(type_, StrawberryType): return type_.is_graphql_generic return False return any(map(_is_generic, self.types)) def copy_with( self, type_var_map: Mapping[str, StrawberryType | type] ) -> StrawberryType: if not self.is_graphql_generic: return self new_types = [] for type_ in self.types: new_type: StrawberryType | type if has_object_definition(type_): type_definition = type_.__strawberry_definition__ if type_definition.is_graphql_generic: new_type = type_definition.copy_with(type_var_map) if isinstance(type_, StrawberryType) and type_.is_graphql_generic: new_type = type_.copy_with(type_var_map) else: new_type = type_ new_types.append(new_type) new_union = StrawberryUnion( type_annotations=tuple(map(StrawberryAnnotation, new_types)), description=self.description, ) new_union.concrete_of = self return new_union def __call__(self, *args: str, **kwargs: Any) -> NoReturn: """Do not use. Used to bypass https://github.com/python/cpython/blob/5efb1a77e75648012f8b52960c8637fc296a5c6d/Lib/typing.py#L148-L149 """ raise ValueError("Cannot use union type directly") def get_type_resolver(self, type_map: TypeMap) -> GraphQLTypeResolver: def _resolve_union_type( root: Any, info: GraphQLResolveInfo, type_: GraphQLAbstractType ) -> str: assert isinstance(type_, GraphQLUnionType) from strawberry.types.base import StrawberryObjectDefinition # If the type given is not an Object type, try resolving using `is_type_of` # defined on the union's inner types if not has_object_definition(root): for inner_type in type_.types: if inner_type.is_type_of is not None and inner_type.is_type_of( root, info ): return inner_type.name # Couldn't resolve using `is_type_of` raise WrongReturnTypeForUnion(info.field_name, str(type(root))) return_type: GraphQLType | None # Iterate over all of our known types and find the first concrete # type that implements the type. We prioritise checking types named in the # Union in case a nested generic object matches against more than one type. concrete_types_for_union = (type_map[x.name] for x in type_.types) for possible_concrete_type in chain( concrete_types_for_union, type_map.values() ): possible_type = possible_concrete_type.definition if not isinstance(possible_type, StrawberryObjectDefinition): continue if possible_type.is_implemented_by(root): return_type = possible_concrete_type.implementation break else: return_type = None # Make sure the found type is expected by the Union if return_type is None or return_type not in type_.types: raise UnallowedReturnTypeForUnion( info.field_name, str(type(root)), set(type_.types) ) assert isinstance(return_type, GraphQLNamedType) return return_type.name return _resolve_union_type @staticmethod def is_valid_union_type(type_: object) -> bool: # Usual case: Union made of @strawberry.types if has_object_definition(type_): return True # Can't confidently assert that these types are valid/invalid within Unions # until full type resolving stage is complete ignored_types = (LazyType, TypeVar) if isinstance(type_, ignored_types): return True if isinstance(type_, StrawberryUnion): return True return get_origin(type_) is Annotated def union( name: str, types: Collection[type[Any]] | None = None, *, description: str | None = None, directives: Iterable[object] = (), ) -> StrawberryUnion: """Creates a new named Union type. Args: name: The GraphQL name of the Union type. types: The types that the Union can be. (Deprecated, use `Annotated[U, strawberry.union("Name")]` instead) description: The GraphQL description of the Union type. directives: The directives to attach to the Union type. Example usages: ```python import strawberry from typing import Annotated @strawberry.type class A: ... @strawberry.type class B: ... MyUnion = Annotated[A | B, strawberry.union("Name")] """ if types is None: union = StrawberryUnion( name=name, description=description, directives=directives, ) if should_use_rich_exceptions(): frame = sys._getframe(1) union._source_file = frame.f_code.co_filename union._source_line = frame.f_lineno # TODO: here union._source_file could be "" # (when using future annotations) # we should find a better way to handle this return union warnings.warn( ( "Passing types to `strawberry.union` is deprecated. Please use " f'{name} = Annotated[Union[A, B], strawberry.union("{name}")] instead' ), DeprecationWarning, stacklevel=2, ) # Validate types if not types: raise TypeError("No types passed to `union`") for type_ in types: # Due to TypeVars, Annotations, LazyTypes, etc., this does not perfectly detect # issues. This check also occurs in the Schema conversion stage as a backup. if not StrawberryUnion.is_valid_union_type(type_): raise InvalidUnionTypeError(union_name=name, invalid_type=type_) return StrawberryUnion( name=name, type_annotations=tuple(StrawberryAnnotation(type_) for type_ in types), description=description, directives=directives, ) __all__ = ["StrawberryUnion", "union"] strawberry-graphql-0.287.0/strawberry/types/unset.py000066400000000000000000000032221511033167500226110ustar00rootroot00000000000000import warnings from typing import Any, Optional DEPRECATED_NAMES: dict[str, str] = { "is_unset": "`is_unset` is deprecated use `value is UNSET` instead", } class UnsetType: __instance: Optional["UnsetType"] = None def __new__(cls: type["UnsetType"]) -> "UnsetType": if cls.__instance is None: ret = super().__new__(cls) cls.__instance = ret return ret return cls.__instance def __str__(self) -> str: return "" def __repr__(self) -> str: return "UNSET" def __bool__(self) -> bool: return False UNSET: Any = UnsetType() """A special value that can be used to represent an unset value in a field or argument. Similar to `undefined` in JavaScript, this value can be used to differentiate between a field that was not set and a field that was set to `None` or `null`. Example: ```python import strawberry @strawberry.input class UserInput: name: str | None = strawberry.UNSET age: int | None = strawberry.UNSET ``` In the example above, if `name` or `age` are not provided when creating a `UserInput` object, they will be set to `UNSET` instead of `None`. Use `is UNSET` to check whether a value is unset. """ def _deprecated_is_unset(value: Any) -> bool: warnings.warn(DEPRECATED_NAMES["is_unset"], DeprecationWarning, stacklevel=2) return value is UNSET def __getattr__(name: str) -> Any: if name in DEPRECATED_NAMES: warnings.warn(DEPRECATED_NAMES[name], DeprecationWarning, stacklevel=2) return globals()[f"_deprecated_{name}"] raise AttributeError(f"module {__name__} has no attribute {name}") __all__ = [ "UNSET", ] strawberry-graphql-0.287.0/strawberry/utils/000077500000000000000000000000001511033167500210765ustar00rootroot00000000000000strawberry-graphql-0.287.0/strawberry/utils/__init__.py000066400000000000000000000002151511033167500232050ustar00rootroot00000000000000from graphql.version import VersionInfo, version_info IS_GQL_33 = version_info >= VersionInfo.from_str("3.3.0a0") IS_GQL_32 = not IS_GQL_33 strawberry-graphql-0.287.0/strawberry/utils/aio.py000066400000000000000000000042311511033167500222200ustar00rootroot00000000000000import sys from collections.abc import ( AsyncGenerator, AsyncIterable, AsyncIterator, Awaitable, Callable, ) from contextlib import asynccontextmanager, suppress from typing import ( Any, TypeVar, cast, ) _T = TypeVar("_T") _R = TypeVar("_R") @asynccontextmanager async def aclosing(thing: _T) -> AsyncGenerator[_T, None]: """Ensure that an async generator is closed properly. Port from the stdlib contextlib.asynccontextmanager. Can be removed and replaced with the stdlib version when we drop support for Python versions before 3.10. """ try: yield thing finally: with suppress(Exception): await cast("AsyncGenerator", thing).aclose() async def aenumerate( iterable: AsyncIterator[_T] | AsyncIterable[_T], ) -> AsyncIterator[tuple[int, _T]]: """Async version of enumerate.""" i = 0 async for element in iterable: yield i, element i += 1 async def aislice( aiterable: AsyncIterator[_T] | AsyncIterable[_T], start: int | None = None, stop: int | None = None, step: int | None = None, ) -> AsyncIterator[_T]: """Async version of itertools.islice.""" # This is based on it = iter( range( start if start is not None else 0, stop if stop is not None else sys.maxsize, step if step is not None else 1, ) ) try: nexti = next(it) except StopIteration: return i = 0 try: async for element in aiterable: if i == nexti: yield element nexti = next(it) i += 1 except StopIteration: return async def asyncgen_to_list(generator: AsyncGenerator[_T, Any]) -> list[_T]: """Convert an async generator to a list.""" return [element async for element in generator] async def resolve_awaitable( awaitable: Awaitable[_T], callback: Callable[[_T], _R], ) -> _R: """Resolves an awaitable object and calls a callback with the resolved value.""" return callback(await awaitable) __all__ = [ "aenumerate", "aislice", "asyncgen_to_list", "resolve_awaitable", ] strawberry-graphql-0.287.0/strawberry/utils/await_maybe.py000066400000000000000000000007171511033167500237370ustar00rootroot00000000000000import inspect from collections.abc import AsyncIterator, Awaitable, Iterator from typing import TypeAlias, TypeVar T = TypeVar("T") AwaitableOrValue: TypeAlias = Awaitable[T] | T AsyncIteratorOrIterator: TypeAlias = AsyncIterator[T] | Iterator[T] async def await_maybe(value: AwaitableOrValue[T]) -> T: if inspect.isawaitable(value): return await value return value __all__ = ["AsyncIteratorOrIterator", "AwaitableOrValue", "await_maybe"] strawberry-graphql-0.287.0/strawberry/utils/deprecations.py000066400000000000000000000015551511033167500241360ustar00rootroot00000000000000from __future__ import annotations import warnings from typing import Any class DEPRECATION_MESSAGES: # noqa: N801 _TYPE_DEFINITION = ( "_type_definition is deprecated, use __strawberry_definition__ instead" ) _ENUM_DEFINITION = ( "_enum_definition is deprecated, use __strawberry_definition__ instead" ) class DeprecatedDescriptor: def __init__(self, msg: str, alias: object, attr_name: str) -> None: self.msg = msg self.alias = alias self.attr_name = attr_name def warn(self) -> None: warnings.warn(self.msg, stacklevel=2) def __get__(self, obj: object | None, type: type | None = None) -> Any: self.warn() return self.alias def inject(self, klass: type) -> None: setattr(klass, self.attr_name, self) __all__ = ["DEPRECATION_MESSAGES", "DeprecatedDescriptor"] strawberry-graphql-0.287.0/strawberry/utils/importer.py000066400000000000000000000011261511033167500233110ustar00rootroot00000000000000import importlib def import_module_symbol( selector: str, default_symbol_name: str | None = None ) -> object: if ":" in selector: module_name, symbol_name = selector.split(":", 1) elif default_symbol_name: module_name, symbol_name = selector, default_symbol_name else: raise ValueError("Selector does not include a symbol name") module = importlib.import_module(module_name) symbol = module for attribute_name in symbol_name.split("."): symbol = getattr(symbol, attribute_name) return symbol __all__ = ["import_module_symbol"] strawberry-graphql-0.287.0/strawberry/utils/inspect.py000066400000000000000000000065201511033167500231200ustar00rootroot00000000000000import asyncio import inspect from collections.abc import Callable from functools import lru_cache from typing import ( Any, Generic, Protocol, TypeVar, get_args, get_origin, ) import strawberry def in_async_context() -> bool: # Based on the way django checks if there's an event loop in the current thread # https://github.com/django/django/blob/main/django/utils/asyncio.py try: asyncio.get_running_loop() except RuntimeError: return False else: return True @lru_cache(maxsize=250) def get_func_args(func: Callable[[Any], Any]) -> list[str]: """Returns a list of arguments for the function.""" sig = inspect.signature(func) return [ arg_name for arg_name, param in sig.parameters.items() if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD ] def get_specialized_type_var_map(cls: type) -> dict[str, type] | None: """Get a type var map for specialized types. Consider the following: ```python class Foo(Generic[T]): ... class Bar(Generic[K]): ... class IntBar(Bar[int]): ... class IntBarSubclass(IntBar): ... class IntBarFoo(IntBar, Foo[str]): ... ``` This would return: ```python get_specialized_type_var_map(object) # None get_specialized_type_var_map(Foo) # {} get_specialized_type_var_map(Bar) # {} get_specialized_type_var_map(IntBar) # {~T: int, ~K: int} get_specialized_type_var_map(IntBarSubclass) # {~T: int, ~K: int} get_specialized_type_var_map(IntBarFoo) # {~T: int, ~K: str} ``` """ from strawberry.types.base import has_object_definition param_args: dict[TypeVar, TypeVar | type] = {} types: list[type] = [cls] while types: tp = types.pop(0) if (origin := get_origin(tp)) is None or origin in (Generic, Protocol): origin = tp # only get type vars for base generics (i.e. Generic[T]) and for strawberry types if not has_object_definition(origin): continue if (type_params := getattr(origin, "__parameters__", None)) is not None: args = get_args(tp) if args: for type_param, arg in zip(type_params, args, strict=True): if type_param not in param_args: param_args[type_param] = arg else: for type_param in type_params: if type_param not in param_args: param_args[type_param] = strawberry.UNSET if orig_bases := getattr(origin, "__orig_bases__", None): types.extend(orig_bases) if not param_args: return None for type_param, arg in list(param_args.items()): resolved_arg = arg while ( isinstance(resolved_arg, TypeVar) and resolved_arg is not strawberry.UNSET ): resolved_arg = ( param_args.get(resolved_arg, strawberry.UNSET) if resolved_arg is not type_param else strawberry.UNSET ) param_args[type_param] = resolved_arg return { k.__name__: v for k, v in reversed(param_args.items()) if v is not strawberry.UNSET and not isinstance(v, TypeVar) } __all__ = ["get_func_args", "get_specialized_type_var_map", "in_async_context"] strawberry-graphql-0.287.0/strawberry/utils/locate_definition.py000066400000000000000000000026251511033167500251340ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from strawberry.exceptions.utils.source_finder import SourceFinder from strawberry.types.scalar import ScalarDefinition from strawberry.types.union import StrawberryUnion from strawberry.utils.str_converters import to_snake_case if TYPE_CHECKING: from strawberry.schema.schema import Schema def locate_definition(schema_symbol: Schema, symbol: str) -> str | None: finder = SourceFinder() if "." in symbol: model, field = symbol.split(".", 1) else: model, field = symbol, None schema_type = schema_symbol.get_type_by_name(model) if not schema_type: return None if field: assert not isinstance(schema_type, StrawberryUnion) location = finder.find_class_attribute_from_object( schema_type.origin, # type: ignore to_snake_case(field) if schema_symbol.config.name_converter.auto_camel_case else field, ) elif isinstance(schema_type, StrawberryUnion): location = finder.find_annotated_union(schema_type, None) elif isinstance(schema_type, ScalarDefinition): location = finder.find_scalar_call(schema_type) else: location = finder.find_class_from_object(schema_type.origin) if not location: return None return f"{location.path}:{location.error_line}:{location.error_column + 1}" strawberry-graphql-0.287.0/strawberry/utils/logging.py000066400000000000000000000013351511033167500231000ustar00rootroot00000000000000from __future__ import annotations import logging from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from typing import Final from graphql.error import GraphQLError from strawberry.types import ExecutionContext class StrawberryLogger: logger: Final[logging.Logger] = logging.getLogger("strawberry.execution") @classmethod def error( cls, error: GraphQLError, execution_context: ExecutionContext | None = None, # https://www.python.org/dev/peps/pep-0484/#arbitrary-argument-lists-and-default-argument-values **logger_kwargs: Any, ) -> None: cls.logger.error(error, exc_info=error.original_error, **logger_kwargs) __all__ = ["StrawberryLogger"] strawberry-graphql-0.287.0/strawberry/utils/operation.py000066400000000000000000000022771511033167500234600ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from graphql.language import OperationDefinitionNode from strawberry.types.graphql import OperationType if TYPE_CHECKING: from graphql.language import DocumentNode def get_first_operation( graphql_document: DocumentNode, ) -> OperationDefinitionNode | None: for definition in graphql_document.definitions: if isinstance(definition, OperationDefinitionNode): return definition return None def get_operation_type( graphql_document: DocumentNode, operation_name: str | None = None ) -> OperationType: definition: OperationDefinitionNode | None = None if operation_name is not None: for d in graphql_document.definitions: if not isinstance(d, OperationDefinitionNode): continue if d.name and d.name.value == operation_name: definition = d break else: definition = get_first_operation(graphql_document) if not definition: raise RuntimeError("Can't get GraphQL operation type") return OperationType(definition.operation.value) __all__ = ["get_first_operation", "get_operation_type"] strawberry-graphql-0.287.0/strawberry/utils/str_converters.py000066400000000000000000000016011511033167500245300ustar00rootroot00000000000000import re # Adapted from this response in Stackoverflow # http://stackoverflow.com/a/19053800/1072990 def to_camel_case(snake_str: str) -> str: components = snake_str.split("_") # We capitalize the first letter of each component except the first one # with the 'capitalize' method and join them together. return components[0] + "".join(x.capitalize() if x else "_" for x in components[1:]) TO_KEBAB_CASE_RE = re.compile("((?<=[a-z0-9])[A-Z]|(?!^)[A-Z](?=[a-z]))") def to_kebab_case(name: str) -> str: return TO_KEBAB_CASE_RE.sub(r"-\1", name).lower() def capitalize_first(name: str) -> str: return name[0].upper() + name[1:] def to_snake_case(name: str) -> str: name = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name) return re.sub("([a-z0-9])([A-Z])", r"\1_\2", name).lower() __all__ = ["capitalize_first", "to_camel_case", "to_kebab_case", "to_snake_case"] strawberry-graphql-0.287.0/strawberry/utils/typing.py000066400000000000000000000270641511033167500227730ustar00rootroot00000000000000import ast import dataclasses import sys import typing from collections.abc import AsyncGenerator from functools import lru_cache from types import UnionType from typing import ( # type: ignore Annotated, Any, ClassVar, ForwardRef, Generic, TypeGuard, TypeVar, Union, _eval_type, _GenericAlias, _SpecialForm, cast, get_args, get_origin, ) @lru_cache def get_generic_alias(type_: type) -> type: """Get the generic alias for a type. Given a type, its generic alias from `typing` module will be returned if it exists. For example: ```python get_generic_alias(list) # typing.List get_generic_alias(dict) # typing.Dict ``` This is mostly useful for python versions prior to 3.9, to get a version of a concrete type which supports `__class_getitem__`. In 3.9+ types like `list`/`dict`/etc are subscriptable and can be used directly instead of their generic alias version. """ if isinstance(type_, _SpecialForm): return type_ for attr_name in dir(typing): # ignore private attributes, they are not Generic aliases if attr_name.startswith("_"): # pragma: no cover continue attr = getattr(typing, attr_name) if is_generic_alias(attr) and attr.__origin__ is type_: return attr raise AssertionError(f"No GenericAlias available for {type_}") # pragma: no cover def is_generic_alias(type_: Any) -> TypeGuard[_GenericAlias]: """Returns True if the type is a generic alias.""" # _GenericAlias overrides all the methods that we can use to know if # this is a subclass of it. But if it has an "_inst" attribute # then it for sure is a _GenericAlias return hasattr(type_, "_inst") def is_list(annotation: object) -> bool: """Returns True if annotation is a List.""" annotation_origin = getattr(annotation, "__origin__", None) return annotation_origin is list def is_union(annotation: object) -> bool: """Returns True if annotation is a Union.""" # this check is needed because unions declared with the new syntax `A | B` # don't have a `__origin__` property on them, but they are instances of if isinstance(annotation, UnionType): return True # unions declared as Union[A, B] fall through to this check, even on python 3.10+ annotation_origin = getattr(annotation, "__origin__", None) return annotation_origin == Union def is_optional(annotation: type) -> bool: """Returns True if the annotation is Optional[SomeType].""" # Optionals are represented as unions if not is_union(annotation): return False types = annotation.__args__ # type: ignore[attr-defined] # A Union to be optional needs to have at least one None type return any(x == None.__class__ for x in types) def get_optional_annotation(annotation: type) -> type: types = annotation.__args__ # type: ignore[attr-defined] non_none_types = tuple(x for x in types if x != None.__class__) # if we have multiple non none types we want to return a copy of this # type (normally a Union type). if len(non_none_types) > 1: return Union[non_none_types] # type: ignore # noqa: UP007 return non_none_types[0] def get_list_annotation(annotation: type) -> type: return annotation.__args__[0] # type: ignore[attr-defined] def is_concrete_generic(annotation: type) -> bool: ignored_generics = (list, tuple, Union, ClassVar, AsyncGenerator) return ( isinstance(annotation, _GenericAlias) and annotation.__origin__ not in ignored_generics ) def is_generic_subclass(annotation: type) -> bool: return isinstance(annotation, type) and issubclass(annotation, Generic) def is_generic(annotation: type) -> bool: """Returns True if the annotation is or extends a generic.""" return ( # TODO: These two lines appear to have the same effect. When will an # annotation have parameters but not satisfy the first condition? (is_generic_subclass(annotation) or is_concrete_generic(annotation)) and bool(get_parameters(annotation)) ) def is_type_var(annotation: type) -> bool: """Returns True if the annotation is a TypeVar.""" return isinstance(annotation, TypeVar) def is_classvar(cls: type, annotation: ForwardRef | str) -> bool: """Returns True if the annotation is a ClassVar.""" # This code was copied from the dataclassses cpython implementation to check # if a field is annotated with ClassVar or not, taking future annotations # in consideration. if dataclasses._is_classvar(annotation, typing): # type: ignore return True annotation_str = ( annotation.__forward_arg__ if isinstance(annotation, ForwardRef) else annotation ) return isinstance(annotation_str, str) and dataclasses._is_type( # type: ignore annotation_str, cls, typing, typing.ClassVar, dataclasses._is_classvar, # type: ignore ) def type_has_annotation(type_: object, annotation: type) -> bool: """Returns True if the type_ has been annotated with annotation.""" if get_origin(type_) is Annotated: return any(isinstance(argument, annotation) for argument in get_args(type_)) return False def get_parameters(annotation: type) -> tuple[object] | tuple[()]: if isinstance(annotation, _GenericAlias) or ( isinstance(annotation, type) and issubclass(annotation, Generic) and annotation is not Generic ): return annotation.__parameters__ # type: ignore[union-attr] return () # pragma: no cover def _get_namespace_from_ast( expr: ast.Expr | ast.expr, globalns: dict | None = None, localns: dict | None = None, ) -> dict[str, type]: from strawberry.types.lazy_type import StrawberryLazyReference extra = {} if isinstance(expr, ast.Expr) and isinstance( expr.value, (ast.BinOp, ast.Subscript) ): extra.update(_get_namespace_from_ast(expr.value, globalns, localns)) elif isinstance(expr, ast.BinOp): for elt in (expr.left, expr.right): extra.update(_get_namespace_from_ast(elt, globalns, localns)) elif ( isinstance(expr, ast.Subscript) and isinstance(expr.value, ast.Name) and expr.value.id == "Union" ): if hasattr(ast, "Index") and isinstance(expr.slice, ast.Index): expr_slice = cast("Any", expr.slice).value else: expr_slice = expr.slice for elt in cast("ast.Tuple", expr_slice).elts: extra.update(_get_namespace_from_ast(elt, globalns, localns)) elif ( isinstance(expr, ast.Subscript) and isinstance(expr.value, ast.Name) and expr.value.id in {"list", "List"} ): extra.update(_get_namespace_from_ast(expr.slice, globalns, localns)) elif ( isinstance(expr, ast.Subscript) and isinstance(expr.value, ast.Name) and expr.value.id == "Annotated" ): if hasattr(ast, "Index") and isinstance(expr.slice, ast.Index): expr_slice = cast("Any", expr.slice).value else: expr_slice = expr.slice args: list[str] = [] for elt in cast("ast.Tuple", expr_slice).elts: extra.update(_get_namespace_from_ast(elt, globalns, localns)) args.append(ast.unparse(elt)) # When using forward refs, the whole # Annotated[SomeType, strawberry.lazy("type.module")] is a forward ref, # and trying to _eval_type on it will fail. Take a different approach # here to resolve lazy types by execing the annotated args, resolving the # type directly and then adding it to extra namespace, so that _eval_type # can properly resolve it later type_name = args[0].strip(" '\"\n") for arg in args[1:]: evaled_arg = eval(arg, globalns, localns) # noqa: S307 if isinstance(evaled_arg, StrawberryLazyReference): extra[type_name] = evaled_arg.resolve_forward_ref(ForwardRef(type_name)) return extra def eval_type( type_: Any, globalns: dict | None = None, localns: dict | None = None, ) -> type: """Evaluates a type, resolving forward references.""" from strawberry.parent import StrawberryParent from strawberry.types.auto import StrawberryAuto from strawberry.types.lazy_type import StrawberryLazyReference from strawberry.types.private import StrawberryPrivate globalns = globalns or {} # If this is not a string, maybe its args are (e.g. list["Foo"]) if isinstance(type_, ForwardRef): ast_obj = cast("ast.Expr", ast.parse(type_.__forward_arg__).body[0]) globalns.update(_get_namespace_from_ast(ast_obj, globalns, localns)) type_ = ForwardRef(ast.unparse(ast_obj)) extra: dict[str, Any] = {} if sys.version_info >= (3, 13): extra = {"type_params": None} return _eval_type(type_, globalns, localns, **extra) origin = get_origin(type_) if origin is not None: args = get_args(type_) if origin is Annotated: for arg in args[1:]: if isinstance(arg, StrawberryPrivate): return type_ if isinstance(arg, StrawberryLazyReference): remaining_args = [ a for a in args[1:] if not isinstance(a, StrawberryLazyReference) ] type_arg = ( arg.resolve_forward_ref(args[0]) if isinstance(args[0], ForwardRef) else args[0] ) args = (type_arg, *remaining_args) break if isinstance(arg, StrawberryAuto): remaining_args = [ a for a in args[1:] if not isinstance(a, StrawberryAuto) ] args = (args[0], arg, *remaining_args) break if isinstance(arg, StrawberryParent): remaining_args = [ a for a in args[1:] if not isinstance(a, StrawberryParent) ] try: type_arg = ( eval_type(args[0], globalns, localns) if isinstance(args[0], ForwardRef) else args[0] ) except (NameError, TypeError): type_arg = args[0] args = (type_arg, arg, *remaining_args) break # If we have only a StrawberryLazyReference and no more annotations, # we need to return the argument directly because Annotated # will raise an error if trying to instantiate it with only # one argument. if len(args) == 1: return args[0] # python 3.10 will return UnionType for origin, and it cannot be # subscripted like Union[Foo, Bar] if origin is UnionType: origin = Union type_ = ( origin[tuple(eval_type(a, globalns, localns) for a in args)] if args else origin ) return type_ __all__ = [ "eval_type", "get_generic_alias", "get_list_annotation", "get_optional_annotation", "get_parameters", "is_classvar", "is_concrete_generic", "is_generic", "is_generic_alias", "is_generic_subclass", "is_list", "is_optional", "is_type_var", "is_union", "type_has_annotation", ] strawberry-graphql-0.287.0/tests/000077500000000000000000000000001511033167500166745ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/__init__.py000066400000000000000000000000001511033167500207730ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/a.py000066400000000000000000000012451511033167500174700ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Annotated import strawberry if TYPE_CHECKING: from tests.b import B @strawberry.type class A: id: strawberry.ID @strawberry.field async def b(self) -> Annotated[B, strawberry.lazy("tests.b")]: from tests.b import B return B(id=self.id) @strawberry.field async def optional_b(self) -> Annotated[B, strawberry.lazy("tests.b")] | None: from tests.b import B return B(id=self.id) @strawberry.field async def optional_b2(self) -> Annotated[B, strawberry.lazy("tests.b")] | None: from tests.b import B return B(id=self.id) strawberry-graphql-0.287.0/tests/asgi/000077500000000000000000000000001511033167500176175ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/asgi/__init__.py000066400000000000000000000000001511033167500217160ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/asgi/test_async.py000066400000000000000000000014161511033167500223470ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING import pytest import strawberry if TYPE_CHECKING: from starlette.testclient import TestClient @pytest.fixture def test_client() -> TestClient: from starlette.testclient import TestClient from strawberry.asgi import GraphQL @strawberry.type class Query: @strawberry.field async def hello(self, name: str | None = None) -> str: return f"Hello {name or 'world'}" async_schema = strawberry.Schema(Query) app = GraphQL[None, None](async_schema) return TestClient(app) def test_simple_query(test_client: TestClient): response = test_client.post("/", json={"query": "{ hello }"}) assert response.json() == {"data": {"hello": "Hello world"}} strawberry-graphql-0.287.0/tests/b.py000066400000000000000000000016471511033167500174770ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Annotated import strawberry if TYPE_CHECKING: from tests.a import A @strawberry.type class B: id: strawberry.ID @strawberry.field async def a(self) -> Annotated[A, strawberry.lazy("tests.a"), object()]: from tests.a import A return A(id=self.id) @strawberry.field async def a_list( self, ) -> list[Annotated[A, strawberry.lazy("tests.a")]]: # pragma: no cover from tests.a import A return [A(id=self.id)] @strawberry.field async def optional_a( self, ) -> Annotated[A, strawberry.lazy("tests.a"), object()] | None: from tests.a import A return A(id=self.id) @strawberry.field async def optional_a2( self, ) -> Annotated[A, strawberry.lazy("tests.a"), object()] | None: from tests.a import A return A(id=self.id) strawberry-graphql-0.287.0/tests/benchmarks/000077500000000000000000000000001511033167500210115ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/benchmarks/__init__.py000066400000000000000000000000001511033167500231100ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/benchmarks/api.py000066400000000000000000000037651511033167500221470ustar00rootroot00000000000000from collections.abc import AsyncIterator import strawberry from strawberry.directive import DirectiveLocation, DirectiveValue @strawberry.type class Item: name: str index: int @strawberry.type class Person: name: str age: int description: str address: str prop_a: str prop_b: str prop_c: str prop_d: str prop_e: str prop_f: str prop_g: str prop_h: str prop_i: str prop_j: str def create_people(n: int): for i in range(n): yield Person( name=f"Person {i}", age=i, description=f"Description {i}", address=f"Address {i}", prop_a=f"Prop A {i}", prop_b=f"Prop B {i}", prop_c=f"Prop C {i}", prop_d=f"Prop D {i}", prop_e=f"Prop E {i}", prop_f=f"Prop F {i}", prop_g=f"Prop G {i}", prop_h=f"Prop H {i}", prop_i=f"Prop I {i}", prop_j=f"Prop J {i}", ) people = list(create_people(n=1_000)) @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "Hello World!" @strawberry.field def people(self, limit: int = 100) -> list[Person]: return people[:limit] if limit else people @strawberry.field def items(self, count: int) -> list[Item]: return [Item(name="Item", index=i) for i in range(count)] @strawberry.type class Subscription: @strawberry.subscription async def something(self) -> AsyncIterator[str]: yield "Hello World!" @strawberry.subscription async def long_running(self, count: int) -> AsyncIterator[int]: for i in range(count): yield i @strawberry.directive(locations=[DirectiveLocation.FIELD]) def uppercase(value: DirectiveValue[str]) -> str: return value.upper() schema = strawberry.Schema(query=Query, subscription=Subscription) schema_with_directives = strawberry.Schema( query=Query, directives=[uppercase], subscription=Subscription ) strawberry-graphql-0.287.0/tests/benchmarks/queries/000077500000000000000000000000001511033167500224665ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/benchmarks/queries/items.graphql000066400000000000000000000001161511033167500251650ustar00rootroot00000000000000query Items($count: Int!) { items(count: $count) { name index } } strawberry-graphql-0.287.0/tests/benchmarks/queries/many_fields.graphql000066400000000000000000000002441511033167500263400ustar00rootroot00000000000000{ people { age description address name propA propB propC propD propE propF propG propH propI propJ } } strawberry-graphql-0.287.0/tests/benchmarks/queries/many_fields_directives.graphql000066400000000000000000000002721511033167500305620ustar00rootroot00000000000000{ people { age description address name propA propB propC propD propE propF propG @uppercase propH @uppercase propI propJ } } strawberry-graphql-0.287.0/tests/benchmarks/queries/simple.graphql000066400000000000000000000000141511033167500253320ustar00rootroot00000000000000{ hello } strawberry-graphql-0.287.0/tests/benchmarks/queries/stadium.graphql000066400000000000000000000003611511033167500255140ustar00rootroot00000000000000query StadiumQuery($seatsPerRow: Int!) { stadium(seatsPerRow: $seatsPerRow) { city country name stands { sectionType seats { labels x y } priceCategory name } } } strawberry-graphql-0.287.0/tests/benchmarks/schema.py000066400000000000000000000073561511033167500226360ustar00rootroot00000000000000from __future__ import annotations from enum import Enum from typing import Annotated import strawberry @strawberry.enum class Role(Enum): ADMIN = "ADMIN" USER = "USER" GUEST = "GUEST" @strawberry.interface class Node: id: strawberry.ID @strawberry.type class PageInfo: has_next_page: bool has_previous_page: bool start_cursor: str | None end_cursor: str | None @strawberry.type class User(Node): id: strawberry.ID username: str email: str role: Role @classmethod def random(cls, seed: int) -> User: return User( id=strawberry.ID(str(int)), username=f"username={seed}", email=f"email={seed}", role=Role.ADMIN, ) @strawberry.field def posts(self, first: int = 10, after: str | None = None) -> PostConnection | None: return PostConnection( edges=[ PostEdge( node=Post.random(i), cursor=str(i), ) for i in range(first) ], page_info=PageInfo( has_next_page=False, has_previous_page=False, start_cursor=None, end_cursor=None, ), ) @strawberry.type class Post(Node): id: strawberry.ID title: str content: str author: User @classmethod def random(cls, seed: int) -> Post: return Post( id=strawberry.ID(str(int)), title=f"title={seed}", content=f"content={seed}", author=User.random(seed), ) @strawberry.field def comments( self, first: int = 10, after: str | None = None ) -> CommentConnection | None: return CommentConnection( edges=[ CommentEdge( node=Comment.random(i), cursor=str(i), ) for i in range(first) ], page_info=PageInfo( has_next_page=False, has_previous_page=False, start_cursor=None, end_cursor=None, ), ) @strawberry.type class Comment(Node): id: strawberry.ID text: str author: User post: Post @classmethod def random(cls, seed: int) -> Comment: return Comment( id=strawberry.ID(str(int)), text=f"text={seed}", author=User.random(seed), post=Post.random(seed), ) @strawberry.type class UserConnection: edges: list[UserEdge | None] | None page_info: PageInfo @strawberry.type class UserEdge: node: User | None cursor: str @strawberry.type class PostConnection: edges: list[PostEdge | None] | None page_info: PageInfo @strawberry.type class PostEdge: node: Post | None cursor: str @strawberry.type class CommentConnection: edges: list[CommentEdge | None] | None page_info: PageInfo @strawberry.type class CommentEdge: node: Comment | None cursor: str SearchResult = Annotated[User | Post | Comment, strawberry.union(name="SearchResult")] @strawberry.type class Query: users: UserConnection | None posts: PostConnection | None comments: CommentConnection | None @strawberry.field async def search( self, query: str, first: int = 10, after: str | None = None ) -> list[SearchResult | None] | None: div = 3 chunks = [first // div + (1 if x < first % div else 0) for x in range(div)] return [ *[User.random(i) for i in range(chunks[0])], *[Post.random(i) for i in range(chunks[1])], *[Comment.random(i) for i in range(chunks[2])], ] schema = strawberry.Schema(query=Query) strawberry-graphql-0.287.0/tests/benchmarks/test_arguments.py000066400000000000000000000012651511033167500244330ustar00rootroot00000000000000import pytest from pytest_codspeed import BenchmarkFixture from strawberry.schema.config import StrawberryConfig from strawberry.schema.types.scalar import DEFAULT_SCALAR_REGISTRY from strawberry.types.arguments import convert_argument from strawberry.types.base import StrawberryList @pytest.mark.parametrize("ntypes", [2**k for k in range(14, 23, 2)]) def test_convert_argument_large_list(benchmark: BenchmarkFixture, ntypes): test_value = list(range(ntypes)) type_ = StrawberryList(int) def run(): result = convert_argument( test_value, type_, DEFAULT_SCALAR_REGISTRY, StrawberryConfig() ) assert test_value == result benchmark(run) strawberry-graphql-0.287.0/tests/benchmarks/test_complex_schema.py000066400000000000000000000030721511033167500254130ustar00rootroot00000000000000import asyncio import pytest from pytest_codspeed.plugin import BenchmarkFixture from .schema import schema query = """ fragment AuthorFragment on User { id username email role } fragment PostFragment on Post { id content title author { ...AuthorFragment } comments { edges { node { author { id username email role } } } } } query Query($query: String!, $first: Int!) { search(query: $query, first: $first) { ... on User { id username email role posts { pageInfo { hasNextPage hasPreviousPage startCursor endCursor } edges { cursor node { author { email id posts { edges { node { ...PostFragment } } } } } } } } ... on Post { ...PostFragment } ... on Comment { id text author { ...AuthorFragment } post { ...PostFragment } } } } """ @pytest.mark.parametrize("number", [50]) def test_execute_complex_schema(benchmark: BenchmarkFixture, number: int): def run(): coroutine = schema.execute( query, variable_values={"query": "test", "first": number}, ) return asyncio.run(coroutine) result = benchmark(run) assert not result.errors strawberry-graphql-0.287.0/tests/benchmarks/test_execute.py000066400000000000000000000046371511033167500240760ustar00rootroot00000000000000import asyncio import datetime import random from datetime import date from typing import cast import pytest from pytest_codspeed.plugin import BenchmarkFixture import strawberry from strawberry.scalars import ID @pytest.mark.benchmark def test_execute(benchmark: BenchmarkFixture): birthday = datetime.datetime.now() pets = ("cat", "shark", "dog", "lama") @strawberry.type class Pet: id: int name: str @strawberry.type class Patron: id: int name: str age: int birthday: date tags: list[str] @strawberry.field def pets(self) -> list[Pet]: return [ Pet( id=i, name=random.choice(pets), # noqa: S311 ) for i in range(5) ] @strawberry.type class Query: @strawberry.field def patrons(self) -> list[Patron]: return [ Patron( id=i, name="Patrick", age=100, birthday=birthday, tags=["go", "ajax"], ) for i in range(1000) ] schema = strawberry.Schema(query=Query) query = """ query something{ patrons { id name age birthday tags pets { id name } } } """ def run(): return asyncio.run(schema.execute(query)) benchmark(run) @pytest.mark.parametrize("ntypes", [2**k for k in range(0, 13, 4)]) def test_interface_performance(benchmark: BenchmarkFixture, ntypes: int): @strawberry.interface class Item: id: ID CONCRETE_TYPES: list[type[Item]] = [ strawberry.type(type(f"Item{i}", (Item,), {})) for i in range(ntypes) ] @strawberry.type class Query: items: list[Item] schema = strawberry.Schema(query=Query, types=CONCRETE_TYPES) query = "query { items { id } }" def run(): return asyncio.run( schema.execute( query, root_value=Query( items=[ CONCRETE_TYPES[i % ntypes](id=cast("ID", i)) for i in range(1000) ] ), ) ) benchmark(run) strawberry-graphql-0.287.0/tests/benchmarks/test_execute_sync.py000066400000000000000000000025561511033167500251300ustar00rootroot00000000000000from pathlib import Path import pytest from pytest_codspeed.plugin import BenchmarkFixture from .api import schema, schema_with_directives ROOT = Path(__file__).parent / "queries" basic_query = (ROOT / "simple.graphql").read_text() many_fields_query = (ROOT / "many_fields.graphql").read_text() many_fields_query_directives = (ROOT / "many_fields_directives.graphql").read_text() items_query = (ROOT / "items.graphql").read_text() @pytest.mark.benchmark def test_execute_basic(benchmark: BenchmarkFixture): benchmark(schema.execute_sync, basic_query) @pytest.mark.benchmark def test_execute_with_many_fields(benchmark: BenchmarkFixture): benchmark(schema.execute_sync, many_fields_query) @pytest.mark.benchmark def test_execute_with_many_fields_and_directives(benchmark: BenchmarkFixture): benchmark(schema_with_directives.execute_sync, many_fields_query_directives) @pytest.mark.benchmark def test_execute_with_10_items(benchmark: BenchmarkFixture): benchmark(schema.execute_sync, items_query, variable_values={"count": 10}) @pytest.mark.benchmark def test_execute_with_100_items(benchmark: BenchmarkFixture): benchmark(schema.execute_sync, items_query, variable_values={"count": 100}) @pytest.mark.benchmark def test_execute_with_1000_items(benchmark: BenchmarkFixture): benchmark(schema.execute_sync, items_query, variable_values={"count": 1000}) strawberry-graphql-0.287.0/tests/benchmarks/test_execute_with_extensions.py000066400000000000000000000027231511033167500274020ustar00rootroot00000000000000import asyncio from inspect import isawaitable from pathlib import Path from typing import Any import pytest from pytest_codspeed.plugin import BenchmarkFixture import strawberry from strawberry.extensions.base_extension import SchemaExtension from strawberry.utils.await_maybe import AwaitableOrValue from .api import Query class SimpleExtension(SchemaExtension): def get_results(self) -> AwaitableOrValue[dict[str, Any]]: return super().get_results() class ResolveExtension(SchemaExtension): async def resolve(self, _next, root, info, *args: Any, **kwargs: Any) -> Any: result = _next(root, info, *args, **kwargs) if isawaitable(result): result = await result return result ROOT = Path(__file__).parent / "queries" items_query = (ROOT / "items.graphql").read_text() @pytest.mark.benchmark @pytest.mark.parametrize("items", [1_000, 10_000], ids=lambda x: f"items_{x}") @pytest.mark.parametrize( "extensions", [[], [SimpleExtension()], [ResolveExtension()]], ids=lambda x: f"with_{'_'.join(type(ext).__name__.lower() for ext in x) or 'no_extensions'}", ) def test_execute( benchmark: BenchmarkFixture, items: int, extensions: list[SchemaExtension] ): schema = strawberry.Schema(query=Query, extensions=extensions) def run(): return asyncio.run( schema.execute(items_query, variable_values={"count": items}) ) results = benchmark(run) assert results.errors is None strawberry-graphql-0.287.0/tests/benchmarks/test_generic_input.py000066400000000000000000000025361511033167500252630ustar00rootroot00000000000000import asyncio from typing import Generic, TypeVar from pytest_codspeed.plugin import BenchmarkFixture import strawberry T = TypeVar("T") @strawberry.input(description="Filter for GraphQL queries") class GraphQLFilter(Generic[T]): """EXTERNAL Filter for GraphQL queries""" eq: T | None = None in_: list[T] | None = None nin: list[T] | None = None gt: T | None = None gte: T | None = None lt: T | None = None lte: T | None = None contains: T | None = None icontains: T | None = None @strawberry.type class Author: name: str @strawberry.type class Book: title: str @strawberry.field async def authors( self, name: GraphQLFilter[str] | None = None, ) -> list[Author]: return [Author(name="F. Scott Fitzgerald")] def get_books(): return [ Book(title="The Great Gatsby"), ] * 1000 @strawberry.type class Query: books: list[Book] = strawberry.field(resolver=get_books) schema = strawberry.Schema(query=Query) query = """{ books { title authors(name: {eq: "F. Scott Fitzgerald"}) { name } } } """ def test_execute_generic_input(benchmark: BenchmarkFixture): def run(): coroutine = schema.execute(query) return asyncio.run(coroutine) result = benchmark(run) assert not result.errors strawberry-graphql-0.287.0/tests/benchmarks/test_stadium.py000066400000000000000000000120061511033167500240670ustar00rootroot00000000000000"""Benchmark for a complex nested query with a large stadium dataset. This benchmark tests Strawberry's performance when dealing with deeply nested objects and large result sets. The stadium query generates approximately 50,000 seat objects across multiple stands, each with multiple labels and coordinates. """ import asyncio from pathlib import Path import pytest from pytest_codspeed.plugin import BenchmarkFixture import strawberry @strawberry.type class Seat: x: int y: int labels: list[str] @strawberry.type class Stand: name: str seats: list[Seat] section_type: str price_category: str @strawberry.type class Stadium: name: str city: str country: str stands: list[Stand] def generate_seats_for_stand( stand_name: str, rows: int, seats_per_row: int, x_offset: int, y_offset: int, section_type: str, ) -> list[Seat]: """Generate seats for a stand with proper coordinates and labels.""" seats = [] for row in range(rows): for seat_num in range(seats_per_row): x = x_offset + seat_num y = y_offset + row row_label = ( chr(65 + row) if row < 26 else f"{chr(65 + row // 26)}{chr(65 + row % 26)}" ) labels = [ stand_name, f"Row-{row_label}", f"Seat-{seat_num + 1}", section_type, f"Block-{(row // 5) + 1}", ] seats.append(Seat(x=x, y=y, labels=labels)) return seats def create_stadium(seats_per_row: int = 250) -> Stadium: """Create a stadium with a configurable number of seats per row. Default configuration (250 seats/row) creates approximately 50,000 seats: - North Stand: 12,500 seats (50 rows × 250 seats) - South Stand: 12,500 seats (50 rows × 250 seats) - East Stand: 10,000 seats (40 rows × 250 seats) - West Stand: 10,000 seats (40 rows × 250 seats) """ stands = [] # North Stand north_stand_seats = generate_seats_for_stand( stand_name="North-Stand", rows=50, seats_per_row=seats_per_row, x_offset=0, y_offset=0, section_type="Standard", ) stands.append( Stand( name="North Stand", seats=north_stand_seats, section_type="Standard", price_category="Bronze", ) ) # South Stand south_stand_seats = generate_seats_for_stand( stand_name="South-Stand", rows=50, seats_per_row=seats_per_row, x_offset=0, y_offset=100, section_type="Standard", ) stands.append( Stand( name="South Stand", seats=south_stand_seats, section_type="Standard", price_category="Bronze", ) ) # East Stand east_stand_seats = generate_seats_for_stand( stand_name="East-Stand", rows=40, seats_per_row=seats_per_row, x_offset=300, y_offset=20, section_type="Premium", ) stands.append( Stand( name="East Stand", seats=east_stand_seats, section_type="Premium", price_category="Gold", ) ) # West Stand west_stand_seats = generate_seats_for_stand( stand_name="West-Stand", rows=40, seats_per_row=seats_per_row, x_offset=-300, y_offset=20, section_type="Premium", ) stands.append( Stand( name="West Stand", seats=west_stand_seats, section_type="Premium", price_category="Gold", ) ) return Stadium( name="Grand Metropolitan Stadium", city="London", country="United Kingdom", stands=stands, ) @strawberry.type class Query: @strawberry.field def stadium(self, seats_per_row: int) -> Stadium: return create_stadium(seats_per_row) ROOT = Path(__file__).parent / "queries" stadium_query = (ROOT / "stadium.graphql").read_text() @pytest.mark.benchmark @pytest.mark.parametrize( "seats_per_row", [250, 500], ids=lambda x: f"seats_per_row_{x}" ) def test_stadium(benchmark: BenchmarkFixture, seats_per_row: int): """Benchmark a complex nested query with a large dataset. This test benchmarks the execution of a GraphQL query that returns a stadium with multiple stands, each containing thousands of seats. Each seat has multiple labels and coordinates. The benchmark tests with different seat counts: - 250 seats/row: ~45,000 total seats - 500 seats/row: ~90,000 total seats """ schema = strawberry.Schema(query=Query) def run(): return asyncio.run( schema.execute( stadium_query, variable_values={"seatsPerRow": seats_per_row} ) ) results = benchmark(run) assert results.errors is None assert results.data is not None assert results.data["stadium"]["name"] == "Grand Metropolitan Stadium" strawberry-graphql-0.287.0/tests/benchmarks/test_subscriptions.py000066400000000000000000000024311511033167500253310ustar00rootroot00000000000000import asyncio from collections.abc import AsyncIterator import pytest from graphql import ExecutionResult from pytest_codspeed.plugin import BenchmarkFixture from .api import schema @pytest.mark.benchmark def test_subscription(benchmark: BenchmarkFixture): s = """ subscription { something } """ async def _run(): for _ in range(100): iterator = await schema.subscribe(s) value = await iterator.__anext__() # type: ignore[union-attr] assert value.data is not None assert value.data["something"] == "Hello World!" benchmark(lambda: asyncio.run(_run())) @pytest.mark.benchmark @pytest.mark.parametrize("count", [1000, 20000]) def test_subscription_long_run(benchmark: BenchmarkFixture, count: int) -> None: s = """#graphql subscription LongRunning($count: Int!) { longRunning(count: $count) } """ async def _run(): i = 0 aiterator: AsyncIterator[ExecutionResult] = await schema.subscribe( s, variable_values={"count": count} ) # type: ignore[assignment] async for res in aiterator: assert res.data is not None assert res.data["longRunning"] == i i += 1 benchmark(lambda: asyncio.run(_run())) strawberry-graphql-0.287.0/tests/c.py000066400000000000000000000001501511033167500174640ustar00rootroot00000000000000from __future__ import annotations import strawberry @strawberry.type class C: id: strawberry.ID strawberry-graphql-0.287.0/tests/channels/000077500000000000000000000000001511033167500204675ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/channels/__init__.py000066400000000000000000000000001511033167500225660ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/channels/test_layers.py000066400000000000000000000564161511033167500234130ustar00rootroot00000000000000from __future__ import annotations import asyncio from collections.abc import AsyncGenerator from typing import TYPE_CHECKING import pytest from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL from strawberry.subscriptions.protocols.graphql_transport_ws.types import ( CompleteMessage, ConnectionAckMessage, ConnectionInitMessage, NextMessage, SubscribeMessage, ) from tests.views.schema import schema if TYPE_CHECKING: from channels.testing import WebsocketCommunicator @pytest.fixture async def ws() -> AsyncGenerator[WebsocketCommunicator, None]: from channels.testing import WebsocketCommunicator from strawberry.channels import GraphQLWSConsumer client = WebsocketCommunicator( GraphQLWSConsumer.as_asgi(schema=schema), "/graphql", subprotocols=[GRAPHQL_TRANSPORT_WS_PROTOCOL], ) res = await client.connect() assert res == (True, GRAPHQL_TRANSPORT_WS_PROTOCOL) yield client await client.disconnect() async def test_no_layers(): from strawberry.channels.handlers.base import ChannelsConsumer consumer = ChannelsConsumer() # Mimic lack of layers. If layers is not installed/configured in channels, # consumer.channel_layer will be `None` consumer.channel_layer = None msg = ( "Layers integration is required listening for channels.\n" "Check https://channels.readthedocs.io/en/stable/topics/channel_layers.html " "for more information" ) with ( pytest.deprecated_call(match="Use listen_to_channel instead"), pytest.raises(RuntimeError, match=msg), ): await consumer.channel_listen("foobar").__anext__() with pytest.raises(RuntimeError, match=msg): async with consumer.listen_to_channel("foobar"): pass @pytest.mark.django_db async def test_channel_listen(ws: WebsocketCommunicator): from channels.layers import get_channel_layer await ws.send_json_to(ConnectionInitMessage({"type": "connection_init"})) connection_ack_message: ConnectionAckMessage = await ws.receive_json_from() assert connection_ack_message == {"type": "connection_ack"} await ws.send_json_to( SubscribeMessage( { "id": "sub1", "type": "subscribe", "payload": { "query": "subscription { listener }", }, } ) ) channel_layer = get_channel_layer() assert channel_layer next_message1: NextMessage = await ws.receive_json_from() assert "data" in next_message1["payload"] assert next_message1["payload"]["data"] is not None channel_name = next_message1["payload"]["data"]["listener"] await channel_layer.send( channel_name, { "type": "test.message", "text": "Hello there!", }, ) next_message2: NextMessage = await ws.receive_json_from() assert next_message2 == { "id": "sub1", "type": "next", "payload": { "data": {"listener": "Hello there!"}, "extensions": {"example": "example"}, }, } await ws.send_json_to(CompleteMessage({"id": "sub1", "type": "complete"})) @pytest.mark.django_db async def test_channel_listen_with_confirmation(ws: WebsocketCommunicator): from channels.layers import get_channel_layer await ws.send_json_to(ConnectionInitMessage({"type": "connection_init"})) connection_ack_message: ConnectionAckMessage = await ws.receive_json_from() assert connection_ack_message == {"type": "connection_ack"} await ws.send_json_to( SubscribeMessage( { "id": "sub1", "type": "subscribe", "payload": { "query": "subscription { listenerWithConfirmation }", }, } ) ) channel_layer = get_channel_layer() assert channel_layer next_message1: NextMessage = await ws.receive_json_from() assert "data" in next_message1["payload"] assert next_message1["payload"]["data"] is not None confirmation = next_message1["payload"]["data"]["listenerWithConfirmation"] assert confirmation is None next_message2: NextMessage = await ws.receive_json_from() assert "data" in next_message2["payload"] assert next_message2["payload"]["data"] is not None channel_name = next_message2["payload"]["data"]["listenerWithConfirmation"] await channel_layer.send( channel_name, { "type": "test.message", "text": "Hello there!", }, ) next_message3: NextMessage = await ws.receive_json_from() assert next_message3 == { "id": "sub1", "type": "next", "payload": { "data": {"listenerWithConfirmation": "Hello there!"}, "extensions": {"example": "example"}, }, } await ws.send_json_to(CompleteMessage({"id": "sub1", "type": "complete"})) @pytest.mark.django_db async def test_channel_listen_timeout(ws: WebsocketCommunicator): from channels.layers import get_channel_layer await ws.send_json_to(ConnectionInitMessage({"type": "connection_init"})) connection_ack_message: ConnectionAckMessage = await ws.receive_json_from() assert connection_ack_message == {"type": "connection_ack"} await ws.send_json_to( SubscribeMessage( { "id": "sub1", "type": "subscribe", "payload": { "query": "subscription { listener(timeout: 0.5) }", }, } ) ) channel_layer = get_channel_layer() assert channel_layer next_message: NextMessage = await ws.receive_json_from() assert "data" in next_message["payload"] assert next_message["payload"]["data"] is not None channel_name = next_message["payload"]["data"]["listener"] assert channel_name complete_message = await ws.receive_json_from() assert complete_message == {"id": "sub1", "type": "complete"} @pytest.mark.django_db async def test_channel_listen_timeout_cm(ws: WebsocketCommunicator): from channels.layers import get_channel_layer await ws.send_json_to(ConnectionInitMessage({"type": "connection_init"})) connection_ack_message: ConnectionAckMessage = await ws.receive_json_from() assert connection_ack_message == {"type": "connection_ack"} await ws.send_json_to( SubscribeMessage( { "id": "sub1", "type": "subscribe", "payload": { "query": "subscription { listenerWithConfirmation(timeout: 0.5) }", }, } ) ) channel_layer = get_channel_layer() assert channel_layer next_message1: NextMessage = await ws.receive_json_from() assert "data" in next_message1["payload"] assert next_message1["payload"]["data"] is not None confirmation = next_message1["payload"]["data"]["listenerWithConfirmation"] assert confirmation is None next_message2 = await ws.receive_json_from() assert "data" in next_message2["payload"] assert next_message2["payload"]["data"] is not None channel_name = next_message2["payload"]["data"]["listenerWithConfirmation"] assert channel_name complete_message: CompleteMessage = await ws.receive_json_from() assert complete_message == {"id": "sub1", "type": "complete"} @pytest.mark.django_db async def test_channel_listen_no_message_on_channel(ws: WebsocketCommunicator): from channels.layers import get_channel_layer await ws.send_json_to(ConnectionInitMessage({"type": "connection_init"})) connection_ack_message: ConnectionAckMessage = await ws.receive_json_from() assert connection_ack_message == {"type": "connection_ack"} await ws.send_json_to( SubscribeMessage( { "id": "sub1", "type": "subscribe", "payload": { "query": "subscription { listener(timeout: 0.5) }", }, } ) ) channel_layer = get_channel_layer() assert channel_layer next_message: NextMessage = await ws.receive_json_from() assert "data" in next_message["payload"] assert next_message["payload"]["data"] is not None channel_name = next_message["payload"]["data"]["listener"] assert channel_name await channel_layer.send( "totally-not-out-channel", { "type": "test.message", "text": "Hello there!", }, ) complete_message: CompleteMessage = await ws.receive_json_from() assert complete_message == {"id": "sub1", "type": "complete"} @pytest.mark.django_db async def test_channel_listen_no_message_on_channel_cm(ws: WebsocketCommunicator): from channels.layers import get_channel_layer await ws.send_json_to(ConnectionInitMessage({"type": "connection_init"})) connection_ack_message: ConnectionAckMessage = await ws.receive_json_from() assert connection_ack_message == {"type": "connection_ack"} await ws.send_json_to( SubscribeMessage( { "id": "sub1", "type": "subscribe", "payload": { "query": "subscription { listenerWithConfirmation(timeout: 0.5) }", }, } ) ) channel_layer = get_channel_layer() assert channel_layer next_message1: NextMessage = await ws.receive_json_from() assert "data" in next_message1["payload"] assert next_message1["payload"]["data"] is not None confirmation = next_message1["payload"]["data"]["listenerWithConfirmation"] assert confirmation is None next_message2 = await ws.receive_json_from() assert "data" in next_message2["payload"] assert next_message2["payload"]["data"] is not None channel_name = next_message2["payload"]["data"]["listenerWithConfirmation"] assert channel_name await channel_layer.send( "totally-not-out-channel", { "type": "test.message", "text": "Hello there!", }, ) complete_message: CompleteMessage = await ws.receive_json_from() assert complete_message == {"id": "sub1", "type": "complete"} @pytest.mark.django_db async def test_channel_listen_group(ws: WebsocketCommunicator): from channels.layers import get_channel_layer await ws.send_json_to(ConnectionInitMessage({"type": "connection_init"})) connection_ack_message: ConnectionAckMessage = await ws.receive_json_from() assert connection_ack_message == {"type": "connection_ack"} await ws.send_json_to( SubscribeMessage( { "id": "sub1", "type": "subscribe", "payload": { "query": 'subscription { listener(group: "foobar") }', }, } ) ) channel_layer = get_channel_layer() assert channel_layer next_message1 = await ws.receive_json_from() assert "data" in next_message1["payload"] assert next_message1["payload"]["data"] is not None channel_name = next_message1["payload"]["data"]["listener"] # Sent at least once to the consumer to make sure the groups were registered await channel_layer.send( channel_name, { "type": "test.message", "text": "Hello there!", }, ) next_message2: NextMessage = await ws.receive_json_from() assert next_message2 == { "id": "sub1", "type": "next", "payload": { "data": {"listener": "Hello there!"}, "extensions": {"example": "example"}, }, } await channel_layer.group_send( "foobar", { "type": "test.message", "text": "Hello there!", }, ) next_message3: NextMessage = await ws.receive_json_from() assert next_message3 == { "id": "sub1", "type": "next", "payload": { "data": {"listener": "Hello there!"}, "extensions": {"example": "example"}, }, } await ws.send_json_to(CompleteMessage({"id": "sub1", "type": "complete"})) @pytest.mark.django_db async def test_channel_listen_group_cm(ws: WebsocketCommunicator): from channels.layers import get_channel_layer await ws.send_json_to(ConnectionInitMessage({"type": "connection_init"})) connection_ack_message: ConnectionAckMessage = await ws.receive_json_from() assert connection_ack_message == {"type": "connection_ack"} await ws.send_json_to( SubscribeMessage( { "id": "sub1", "type": "subscribe", "payload": { "query": 'subscription { listenerWithConfirmation(group: "foobar") }', }, } ) ) channel_layer = get_channel_layer() assert channel_layer next_message1: NextMessage = await ws.receive_json_from() assert "data" in next_message1["payload"] assert next_message1["payload"]["data"] is not None confirmation = next_message1["payload"]["data"]["listenerWithConfirmation"] assert confirmation is None next_message2 = await ws.receive_json_from() assert "data" in next_message2["payload"] assert next_message2["payload"]["data"] is not None channel_name = next_message2["payload"]["data"]["listenerWithConfirmation"] # Sent at least once to the consumer to make sure the groups were registered await channel_layer.send( channel_name, { "type": "test.message", "text": "Hello there!", }, ) next_message3: NextMessage = await ws.receive_json_from() assert next_message3 == { "id": "sub1", "type": "next", "payload": { "data": {"listenerWithConfirmation": "Hello there!"}, "extensions": {"example": "example"}, }, } await channel_layer.group_send( "foobar", { "type": "test.message", "text": "Hello there!", }, ) next_message4: NextMessage = await ws.receive_json_from() assert next_message4 == { "id": "sub1", "type": "next", "payload": { "data": {"listenerWithConfirmation": "Hello there!"}, "extensions": {"example": "example"}, }, } await ws.send_json_to(CompleteMessage({"id": "sub1", "type": "complete"})) @pytest.mark.django_db async def test_channel_listen_group_twice(ws: WebsocketCommunicator): from channels.layers import get_channel_layer await ws.send_json_to(ConnectionInitMessage({"type": "connection_init"})) connection_ack_message: ConnectionAckMessage = await ws.receive_json_from() assert connection_ack_message == {"type": "connection_ack"} await ws.send_json_to( SubscribeMessage( { "id": "sub1", "type": "subscribe", "payload": { "query": 'subscription { listener(group: "group1") }', }, } ) ) await ws.send_json_to( SubscribeMessage( { "id": "sub2", "type": "subscribe", "payload": { "query": 'subscription { listener(group: "group2") }', }, } ) ) channel_layer = get_channel_layer() assert channel_layer # Wait for channel subscriptions to start next_message1: NextMessage = await ws.receive_json_from() next_message2: NextMessage = await ws.receive_json_from() assert {"sub1", "sub2"} == {next_message1["id"], next_message2["id"]} assert "data" in next_message1["payload"] assert next_message1["payload"]["data"] is not None channel_name = next_message1["payload"]["data"]["listener"] # Sent at least once to the consumer to make sure the groups were registered await channel_layer.send( channel_name, { "type": "test.message", "text": "Hello there!", }, ) next_message3: NextMessage = await ws.receive_json_from() next_message4: NextMessage = await ws.receive_json_from() assert {"sub1", "sub2"} == {next_message3["id"], next_message4["id"]} assert "data" in next_message3["payload"] assert next_message3["payload"]["data"] is not None assert next_message3["payload"]["data"]["listener"] == "Hello there!" assert "data" in next_message4["payload"] assert next_message4["payload"]["data"] is not None assert next_message4["payload"]["data"]["listener"] == "Hello there!" # We now have two channel_listen AsyncGenerators waiting, one for id="sub1" # and one for id="sub2". This group message will be received by both of them # as they are both running on the same ChannelsConsumer instance so even # though "sub2" was initialised with "group2" as the argument, it will receive # this message for "group1" await channel_layer.group_send( "group1", { "type": "test.message", "text": "Hello group 1!", }, ) next_message5: NextMessage = await ws.receive_json_from() next_message6: NextMessage = await ws.receive_json_from() assert {"sub1", "sub2"} == {next_message5["id"], next_message6["id"]} assert "data" in next_message5["payload"] assert next_message5["payload"]["data"] is not None assert next_message5["payload"]["data"]["listener"] == "Hello group 1!" assert "data" in next_message6["payload"] assert next_message6["payload"]["data"] is not None assert next_message6["payload"]["data"]["listener"] == "Hello group 1!" await channel_layer.group_send( "group2", { "type": "test.message", "text": "Hello group 2!", }, ) next_message7: NextMessage = await ws.receive_json_from() next_message8: NextMessage = await ws.receive_json_from() assert {"sub1", "sub2"} == {next_message7["id"], next_message8["id"]} assert "data" in next_message7["payload"] assert next_message7["payload"]["data"] is not None assert next_message7["payload"]["data"]["listener"] == "Hello group 2!" assert "data" in next_message8["payload"] assert next_message8["payload"]["data"] is not None assert next_message8["payload"]["data"]["listener"] == "Hello group 2!" await ws.send_json_to(CompleteMessage({"id": "sub1", "type": "complete"})) await ws.send_json_to(CompleteMessage({"id": "sub2", "type": "complete"})) async def test_channel_listen_group_twice_cm(ws: WebsocketCommunicator): from channels.layers import get_channel_layer await ws.send_json_to(ConnectionInitMessage({"type": "connection_init"})) connection_ack_message: ConnectionAckMessage = await ws.receive_json_from() assert connection_ack_message == {"type": "connection_ack"} await ws.send_json_to( SubscribeMessage( { "id": "sub1", "type": "subscribe", "payload": { "query": 'subscription { listenerWithConfirmation(group: "group1") }', }, } ) ) await ws.send_json_to( SubscribeMessage( { "id": "sub2", "type": "subscribe", "payload": { "query": 'subscription { listenerWithConfirmation(group: "group2") }', }, } ) ) channel_layer = get_channel_layer() assert channel_layer # Wait for confirmation for channel subscriptions messages = await asyncio.gather( ws.receive_json_from(), ws.receive_json_from(), ws.receive_json_from(), ws.receive_json_from(), ) confirmation1 = next( i for i in messages if not i["payload"]["data"]["listenerWithConfirmation"] and i["id"] == "sub1" ) confirmation2 = next( i for i in messages if not i["payload"]["data"]["listenerWithConfirmation"] and i["id"] == "sub2" ) channel_name1 = next( i for i in messages if i["payload"]["data"]["listenerWithConfirmation"] and i["id"] == "sub1" ) channel_name2 = next( i for i in messages if i["payload"]["data"]["listenerWithConfirmation"] and i["id"] == "sub2" ) # Ensure correct ordering of responses assert messages.index(confirmation1) < messages.index(channel_name1) assert messages.index(confirmation2) < messages.index(channel_name2) channel_name = channel_name1["payload"]["data"]["listenerWithConfirmation"] # Sent at least once to the consumer to make sure the groups were registered await channel_layer.send( channel_name, { "type": "test.message", "text": "Hello there!", }, ) next_message1: NextMessage = await ws.receive_json_from() next_message2: NextMessage = await ws.receive_json_from() assert {"sub1", "sub2"} == {next_message1["id"], next_message2["id"]} assert "data" in next_message1["payload"] assert next_message1["payload"]["data"] is not None assert ( next_message1["payload"]["data"]["listenerWithConfirmation"] == "Hello there!" ) assert "data" in next_message2["payload"] assert next_message2["payload"]["data"] is not None assert ( next_message2["payload"]["data"]["listenerWithConfirmation"] == "Hello there!" ) # We now have two channel_listen AsyncGenerators waiting, one for id="sub1" # and one for id="sub2". This group message will be received by both of them # as they are both running on the same ChannelsConsumer instance so even # though "sub2" was initialised with "group2" as the argument, it will receive # this message for "group1" await channel_layer.group_send( "group1", { "type": "test.message", "text": "Hello group 1!", }, ) next_message3: NextMessage = await ws.receive_json_from() next_message4: NextMessage = await ws.receive_json_from() assert {"sub1", "sub2"} == {next_message3["id"], next_message4["id"]} assert "data" in next_message3["payload"] assert next_message3["payload"]["data"] is not None assert ( next_message3["payload"]["data"]["listenerWithConfirmation"] == "Hello group 1!" ) assert "data" in next_message4["payload"] assert next_message4["payload"]["data"] is not None assert ( next_message4["payload"]["data"]["listenerWithConfirmation"] == "Hello group 1!" ) await channel_layer.group_send( "group2", { "type": "test.message", "text": "Hello group 2!", }, ) next_message5: NextMessage = await ws.receive_json_from() next_message6: NextMessage = await ws.receive_json_from() assert {"sub1", "sub2"} == {next_message5["id"], next_message6["id"]} assert "data" in next_message5["payload"] assert next_message5["payload"]["data"] is not None assert ( next_message5["payload"]["data"]["listenerWithConfirmation"] == "Hello group 2!" ) assert "data" in next_message6["payload"] assert next_message6["payload"]["data"] is not None assert ( next_message6["payload"]["data"]["listenerWithConfirmation"] == "Hello group 2!" ) await ws.send_json_to(CompleteMessage({"id": "sub1", "type": "complete"})) await ws.send_json_to(CompleteMessage({"id": "sub2", "type": "complete"})) strawberry-graphql-0.287.0/tests/channels/test_router.py000066400000000000000000000047231511033167500234260ustar00rootroot00000000000000from unittest import mock import pytest from tests.views.schema import schema def _fake_asgi(): return lambda: None @mock.patch("strawberry.channels.router.GraphQLHTTPConsumer.as_asgi") @mock.patch("strawberry.channels.router.GraphQLWSConsumer.as_asgi") @pytest.mark.parametrize("pattern", ["^graphql", "^foo"]) def test_included_paths(ws_asgi: mock.Mock, http_asgi: mock.Mock, pattern: str): from strawberry.channels.router import GraphQLProtocolTypeRouter http_ret = _fake_asgi() http_asgi.return_value = http_ret ws_ret = _fake_asgi() ws_asgi.return_value = ws_ret router = GraphQLProtocolTypeRouter(schema, url_pattern=pattern) assert set(router.application_mapping) == {"http", "websocket"} assert len(router.application_mapping["http"].routes) == 1 http_route = router.application_mapping["http"].routes[0] assert http_route.pattern._regex == pattern assert http_route.callback is http_ret assert len(router.application_mapping["websocket"].routes) == 1 http_route = router.application_mapping["websocket"].routes[0] assert http_route.pattern._regex == pattern assert http_route.callback is ws_ret @mock.patch("strawberry.channels.router.GraphQLHTTPConsumer.as_asgi") @mock.patch("strawberry.channels.router.GraphQLWSConsumer.as_asgi") @pytest.mark.parametrize("pattern", ["^graphql", "^foo"]) def test_included_paths_with_django_app( ws_asgi: mock.Mock, http_asgi: mock.Mock, pattern: str, ): from strawberry.channels.router import GraphQLProtocolTypeRouter http_ret = _fake_asgi() http_asgi.return_value = http_ret ws_ret = _fake_asgi() ws_asgi.return_value = ws_ret django_app = _fake_asgi() router = GraphQLProtocolTypeRouter( schema, django_application=django_app, url_pattern=pattern, ) assert set(router.application_mapping) == {"http", "websocket"} assert len(router.application_mapping["http"].routes) == 2 http_route = router.application_mapping["http"].routes[0] assert http_route.pattern._regex == pattern assert http_route.callback is http_ret django_route = router.application_mapping["http"].routes[1] assert django_route.pattern._regex == "^" assert django_route.callback is django_app assert len(router.application_mapping["websocket"].routes) == 1 http_route = router.application_mapping["websocket"].routes[0] assert http_route.pattern._regex == pattern assert http_route.callback is ws_ret strawberry-graphql-0.287.0/tests/channels/test_testing.py000066400000000000000000000034221511033167500235560ustar00rootroot00000000000000from __future__ import annotations from collections.abc import AsyncGenerator from typing import TYPE_CHECKING, Any import pytest from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL from tests.views.schema import schema if TYPE_CHECKING: from strawberry.channels.testing import GraphQLWebsocketCommunicator @pytest.fixture(params=[GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL]) async def communicator( request: Any, ) -> AsyncGenerator[GraphQLWebsocketCommunicator, None]: from strawberry.channels import GraphQLWSConsumer from strawberry.channels.testing import GraphQLWebsocketCommunicator application = GraphQLWSConsumer.as_asgi(schema=schema, keep_alive_interval=50) async with GraphQLWebsocketCommunicator( protocol=request.param, application=application, path="/graphql", connection_params={"strawberry": "Hi"}, ) as client: yield client async def test_simple_subscribe(communicator: GraphQLWebsocketCommunicator): async for res in communicator.subscribe( query='subscription { echo(message: "Hi") }' ): assert res.data == {"echo": "Hi"} async def test_subscribe_unexpected_error(communicator): async for res in communicator.subscribe( query='subscription { exception(message: "Hi") }' ): assert res.errors[0].message == "Hi" async def test_graphql_error(communicator): async for res in communicator.subscribe( query='subscription { error(message: "Hi") }' ): assert res.errors[0].message == "Hi" async def test_simple_connection_params(communicator): async for res in communicator.subscribe(query="subscription { connectionParams }"): assert res.data["connectionParams"]["strawberry"] == "Hi" strawberry-graphql-0.287.0/tests/cli/000077500000000000000000000000001511033167500174435ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/cli/__init__.py000066400000000000000000000000001511033167500215420ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/cli/conftest.py000066400000000000000000000014241511033167500216430ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING import pytest from typer.testing import CliRunner from strawberry.cli.constants import DEV_SERVER_SCHEMA_ENV_VAR_KEY if TYPE_CHECKING: from starlette.testclient import TestClient from typer import Typer @pytest.fixture def cli_runner() -> CliRunner: return CliRunner() @pytest.fixture def dev_server_client(monkeypatch: pytest.MonkeyPatch) -> TestClient: from starlette.testclient import TestClient from strawberry.cli.dev_server import app monkeypatch.setenv( DEV_SERVER_SCHEMA_ENV_VAR_KEY, "tests.fixtures.sample_package.sample_module", ) return TestClient(app) @pytest.fixture def cli_app() -> Typer: from strawberry.cli.app import app return app strawberry-graphql-0.287.0/tests/cli/fixtures/000077500000000000000000000000001511033167500213145ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/cli/fixtures/__init__.py000066400000000000000000000000001511033167500234130ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/cli/fixtures/unions.py000066400000000000000000000005761511033167500232110ustar00rootroot00000000000000import strawberry # create a few types and then a union type @strawberry.type class Foo: a: str @strawberry.type class Bar: b: str @strawberry.type class Baz: c: str @strawberry.type class Qux: d: str # this is the union type Union1 = strawberry.union(name="Union1", types=(Foo, Bar, Baz, Qux)) Union2 = strawberry.union(name="Union2", types=(Baz, Qux)) strawberry-graphql-0.287.0/tests/cli/snapshots/000077500000000000000000000000001511033167500214655ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/cli/snapshots/unions.py000066400000000000000000000006451511033167500233570ustar00rootroot00000000000000import strawberry from typing import Annotated # create a few types and then a union type @strawberry.type class Foo: a: str @strawberry.type class Bar: b: str @strawberry.type class Baz: c: str @strawberry.type class Qux: d: str # this is the union type Union1 = Annotated[Foo | Bar | Baz | Qux, strawberry.union(name="Union1")] Union2 = Annotated[Baz | Qux, strawberry.union(name="Union2")] strawberry-graphql-0.287.0/tests/cli/snapshots/unions_typing_extension.py000066400000000000000000000006601511033167500270420ustar00rootroot00000000000000import strawberry from typing_extensions import Annotated # create a few types and then a union type @strawberry.type class Foo: a: str @strawberry.type class Bar: b: str @strawberry.type class Baz: c: str @strawberry.type class Qux: d: str # this is the union type Union1 = Annotated[Foo | Bar | Baz | Qux, strawberry.union(name="Union1")] Union2 = Annotated[Baz | Qux, strawberry.union(name="Union2")] strawberry-graphql-0.287.0/tests/cli/test_codegen.py000066400000000000000000000166141511033167500224700ustar00rootroot00000000000000from pathlib import Path import pytest from typer import Typer from typer.testing import CliRunner from strawberry.cli.commands.codegen import ConsolePlugin from strawberry.codegen import CodegenFile, CodegenResult, QueryCodegenPlugin from strawberry.codegen.types import GraphQLOperation, GraphQLType class ConsoleTestPlugin(ConsolePlugin): def on_end(self, result: CodegenResult): result.files[0].path = "renamed.py" return super().on_end(result) class QueryCodegenTestPlugin(QueryCodegenPlugin): def generate_code( self, types: list[GraphQLType], operation: GraphQLOperation ) -> list[CodegenFile]: return [ CodegenFile( path="test.py", content=f"# This is a test file for {operation.name}", ) ] class EmptyPlugin(QueryCodegenPlugin): def generate_code( self, types: list[GraphQLType], operation: GraphQLOperation ) -> list[CodegenFile]: return [ CodegenFile( path="test.py", content="# Empty", ) ] @pytest.fixture def query_file_path(tmp_path: Path) -> Path: output_path = tmp_path / "query.graphql" output_path.write_text( """ query GetUser { user { name } } """ ) return output_path @pytest.fixture def query_file_path2(tmp_path: Path) -> Path: output_path = tmp_path / "query2.graphql" output_path.write_text( """ query GetUser { user { name } } """ ) return output_path def test_codegen( cli_app: Typer, cli_runner: CliRunner, query_file_path: Path, tmp_path: Path ): selector = "tests.fixtures.sample_package.sample_module:schema" result = cli_runner.invoke( cli_app, [ "codegen", "-p", "tests.cli.test_codegen:QueryCodegenTestPlugin", "-o", str(tmp_path), "--schema", selector, str(query_file_path), ], ) assert result.exit_code == 0 code_path = tmp_path / "test.py" assert code_path.exists() assert code_path.read_text() == "# This is a test file for GetUser" def test_codegen_multiple_files( cli_app: Typer, cli_runner: CliRunner, query_file_path: Path, query_file_path2: Path, tmp_path: Path, ): expected_paths = [ tmp_path / "query.py", tmp_path / "query2.py", tmp_path / "query.ts", tmp_path / "query2.ts", ] for path in expected_paths: assert not path.exists() selector = "tests.fixtures.sample_package.sample_module:schema" result = cli_runner.invoke( cli_app, [ "codegen", "-p", "python", "-p", "typescript", "-o", str(tmp_path), "--schema", selector, str(query_file_path), str(query_file_path2), ], ) assert result.exit_code == 0 for path in expected_paths: assert path.exists() assert " GetUserResult" in path.read_text() def test_codegen_pass_no_query(cli_app: Typer, cli_runner: CliRunner, tmp_path: Path): selector = "tests.fixtures.sample_package.sample_module:schema" result = cli_runner.invoke( cli_app, [ "codegen", "-p", "tests.cli.test_codegen:EmptyPlugin", "-o", str(tmp_path), "--schema", selector, ], ) assert result.exit_code == 0 def test_codegen_passing_plugin_symbol( cli_app: Typer, cli_runner: CliRunner, query_file_path: Path, tmp_path: Path ): selector = "tests.fixtures.sample_package.sample_module:schema" result = cli_runner.invoke( cli_app, [ "codegen", "-p", "tests.cli.test_codegen:EmptyPlugin", "-o", str(tmp_path), "--schema", selector, str(query_file_path), ], ) assert result.exit_code == 0 code_path = tmp_path / "test.py" assert code_path.exists() assert code_path.read_text() == "# Empty" def test_codegen_returns_error_when_symbol_does_not_exist( cli_app: Typer, cli_runner: CliRunner, query_file_path: Path, tmp_path: Path ): selector = "tests.fixtures.sample_package.sample_module:schema" result = cli_runner.invoke( cli_app, [ "codegen", "-p", "tests.cli.test_codegen:SomePlugin", "--schema", selector, "-o", str(tmp_path), str(query_file_path), ], ) assert result.exit_code == 1 assert result.exception assert result.exception.args == ( "module 'tests.cli.test_codegen' has no attribute 'SomePlugin'", ) def test_codegen_returns_error_when_module_does_not_exist( cli_app: Typer, cli_runner: CliRunner, query_file_path: Path, tmp_path: Path ): selector = "tests.fixtures.sample_package.sample_module:schema" result = cli_runner.invoke( cli_app, [ "codegen", "-p", "fake_module_plugin", "--schema", selector, "-o", str(tmp_path), str(query_file_path), ], ) assert result.exit_code == 1 assert "Error: Plugin fake_module_plugin not found" in result.output def test_codegen_returns_error_when_does_not_find_plugin( cli_app: Typer, cli_runner: CliRunner, query_file_path: Path, tmp_path: Path ): selector = "tests.fixtures.sample_package.sample_module:schema" result = cli_runner.invoke( cli_app, [ "codegen", "-p", "tests.cli.test_server", "--schema", selector, "-o", str(tmp_path), str(query_file_path), ], ) assert result.exit_code == 1 assert "Error: Plugin tests.cli.test_server not found" in result.output def test_codegen_finds_our_plugins( cli_app: Typer, cli_runner: CliRunner, query_file_path: Path, tmp_path: Path ): selector = "tests.fixtures.sample_package.sample_module:schema" result = cli_runner.invoke( cli_app, [ "codegen", "-p", "python", "--schema", selector, "-o", str(tmp_path), str(query_file_path), ], ) assert result.exit_code == 0 code_path = tmp_path / query_file_path.with_suffix(".py").name assert code_path.exists() assert "class GetUserResult" in code_path.read_text() def test_can_use_custom_cli_plugin( cli_app: Typer, cli_runner: CliRunner, query_file_path: Path, tmp_path: Path ): selector = "tests.fixtures.sample_package.sample_module:schema" result = cli_runner.invoke( cli_app, [ "codegen", "--cli-plugin", "tests.cli.test_codegen:ConsoleTestPlugin", "-p", "python", "--schema", selector, "-o", str(tmp_path), str(query_file_path), ], ) assert result.exit_code == 0 code_path = tmp_path / "renamed.py" assert code_path.exists() assert "class GetUserResult" in code_path.read_text() strawberry-graphql-0.287.0/tests/cli/test_dev.py000066400000000000000000000072231511033167500216360ustar00rootroot00000000000000import re import sys from unittest.mock import MagicMock import pytest from pytest_mock import MockerFixture, MockFixture from starlette.testclient import TestClient from typer import Typer from typer.testing import CliRunner # UTF-8 chars are not supported by default console on Windows BOOT_MSG_END = " 🍓\n" if sys.platform != "win32" else "\n" BOOT_MSG = f"Running strawberry on http://0.0.0.0:8000/graphql{BOOT_MSG_END}" cli_runner = CliRunner() @pytest.fixture def uvicorn_run_mock(mocker: MockFixture) -> MagicMock: # uvicorn is only conditionally imported by the cli command, # so we need to import it here to be able to mock it import uvicorn return mocker.patch.object(uvicorn, "run") def test_without_options(cli_app: Typer, uvicorn_run_mock: MagicMock): schema = "tests.fixtures.sample_package.sample_module" result = cli_runner.invoke(cli_app, ["dev", schema]) assert result.exit_code == 0, result.stdout assert uvicorn_run_mock.call_count == 1 assert re.match(BOOT_MSG, result.stdout) def test_app_dir_option(cli_app: Typer, uvicorn_run_mock: MagicMock): result = cli_runner.invoke( cli_app, ["dev", "--app-dir=./tests/fixtures/sample_package", "sample_module"], ) assert result.exit_code == 0, result.stdout assert uvicorn_run_mock.call_count == 1 assert re.match(BOOT_MSG, result.stdout) def test_default_schema_symbol_name(cli_app: Typer, uvicorn_run_mock: MagicMock): schema = "tests.fixtures.sample_package.sample_module" result = cli_runner.invoke(cli_app, ["dev", schema]) assert result.exit_code == 0, result.stdout assert uvicorn_run_mock.call_count == 1 def test_invalid_app_dir(cli_app: Typer): result = cli_runner.invoke(cli_app, ["dev", "--app-dir=./non/existing/path", "app"]) expected_error = "Error: No module named 'app'" assert result.exit_code == 2 assert expected_error in result.stdout def test_invalid_module(cli_app: Typer): schema = "not.existing.module" result = cli_runner.invoke(cli_app, ["dev", schema]) expected_error = "Error: No module named 'not'" assert result.exit_code == 2 assert expected_error in result.stdout def test_invalid_symbol(cli_app: Typer): schema = "tests.fixtures.sample_package.sample_module:not.existing.symbol" result = cli_runner.invoke(cli_app, ["dev", schema]) expected_error = ( "Error: module 'tests.fixtures.sample_package.sample_module' " "has no attribute 'not'" ) assert result.exit_code == 2 assert expected_error in result.stdout.replace("\n", "") def test_invalid_schema_instance(cli_app: Typer): schema = "tests.fixtures.sample_package.sample_module:not_a_schema" result = cli_runner.invoke(cli_app, ["dev", schema]) expected_error = "Error: The `schema` must be an instance of strawberry.Schema" assert result.exit_code == 2 assert expected_error in result.stdout @pytest.mark.parametrize("dependency", ["uvicorn", "starlette"]) def test_missing_dev_server_dependencies( cli_app: Typer, mocker: MockerFixture, dependency: str ): mocker.patch.dict(sys.modules, {dependency: None}) schema = "tests.fixtures.sample_package.sample_module" result = cli_runner.invoke(cli_app, ["dev", schema]) assert result.exit_code == 1 assert result.stdout == ( "Error: " "The dev server requires additional packages, install them by running:\n" "pip install 'strawberry-graphql[cli]'\n" ) @pytest.mark.parametrize("path", ["/", "/graphql"]) def test_dev_server_routes(dev_server_client: TestClient, path: str): response = dev_server_client.get(path) assert response.status_code == 200 strawberry-graphql-0.287.0/tests/cli/test_export_schema.py000066400000000000000000000066441511033167500237270ustar00rootroot00000000000000from inline_snapshot import snapshot from typer import Typer from typer.testing import CliRunner def test_schema_export(cli_app: Typer, cli_runner: CliRunner): selector = "tests.fixtures.sample_package.sample_module:schema" result = cli_runner.invoke(cli_app, ["export-schema", selector]) assert result.exit_code == 0 assert result.stdout == snapshot( """\ type A { name: String! } type B { a: A! } scalar ExampleScalar union InlineUnion = A | B type Query { user: User! } enum Role { ADMIN USER } union UnionExample = A | B type User { name: String! age: Int! role: Role! exampleScalar: ExampleScalar! unionExample: UnionExample! inlineUnion: InlineUnion! } """ ) def test_schema_symbol_is_callable(cli_app: Typer, cli_runner: CliRunner): selector = "tests.fixtures.sample_package.sample_module:create_schema" result = cli_runner.invoke(cli_app, ["export-schema", selector]) assert result.exit_code == 0 def test_default_schema_symbol_name(cli_app: Typer, cli_runner: CliRunner): selector = "tests.fixtures.sample_package.sample_module" result = cli_runner.invoke(cli_app, ["export-schema", selector]) assert result.exit_code == 0 def test_app_dir_option(cli_app: Typer, cli_runner: CliRunner): selector = "sample_module" result = cli_runner.invoke( cli_app, ["export-schema", "--app-dir=./tests/fixtures/sample_package", selector], ) assert result.exit_code == 0 def test_invalid_module(cli_app: Typer, cli_runner: CliRunner): selector = "not.existing.module" result = cli_runner.invoke(cli_app, ["export-schema", selector]) expected_error = "Error: No module named 'not'" assert result.exit_code == 2 assert expected_error in result.stdout.replace("\n", "") def test_invalid_symbol(cli_app: Typer, cli_runner: CliRunner): selector = "tests.fixtures.sample_package.sample_module:not.existing.symbol" result = cli_runner.invoke(cli_app, ["export-schema", selector]) expected_error = ( "Error: module 'tests.fixtures.sample_package.sample_module' " "has no attribute 'not'" ) assert result.exit_code == 2 assert expected_error in result.stdout.replace("\n", "") def test_invalid_schema_instance(cli_app: Typer, cli_runner: CliRunner): selector = "tests.fixtures.sample_package.sample_module:not_a_schema" result = cli_runner.invoke(cli_app, ["export-schema", selector]) expected_error = "Error: The `schema` must be an instance of strawberry.Schema" assert result.exit_code == 2 assert expected_error in result.stdout.replace("\n", "") def test_output_option(cli_app: Typer, cli_runner: CliRunner, tmp_path): selector = "tests.fixtures.sample_package.sample_module:schema" output = tmp_path / "schema.graphql" output_commands = ["--output", "-o"] for output_command in output_commands: result = cli_runner.invoke( cli_app, ["export-schema", selector, output_command, str(output)] ) assert result.exit_code == 0 assert output.read_text() == snapshot( """\ type A { name: String! } type B { a: A! } scalar ExampleScalar union InlineUnion = A | B type Query { user: User! } enum Role { ADMIN USER } union UnionExample = A | B type User { name: String! age: Int! role: Role! exampleScalar: ExampleScalar! unionExample: UnionExample! inlineUnion: InlineUnion! } """ ) strawberry-graphql-0.287.0/tests/cli/test_locate_definition.py000066400000000000000000000040161511033167500245340ustar00rootroot00000000000000from pathlib import Path from inline_snapshot import snapshot from typer import Typer from typer.testing import CliRunner from tests.typecheckers.utils.marks import skip_on_windows pytestmark = skip_on_windows def _simplify_path(path: str) -> str: path = Path(path) root = Path(__file__).parents[1] return str(path.relative_to(root)) def test_find_model_name(cli_app: Typer, cli_runner: CliRunner): selector = "tests.fixtures.sample_package.sample_module:schema" result = cli_runner.invoke(cli_app, ["locate-definition", selector, "User"]) assert result.exit_code == 0 assert _simplify_path(result.stdout.strip()) == snapshot( "fixtures/sample_package/sample_module.py:38:7" ) def test_find_model_field(cli_app: Typer, cli_runner: CliRunner): selector = "tests.fixtures.sample_package.sample_module:schema" result = cli_runner.invoke(cli_app, ["locate-definition", selector, "User.name"]) assert result.exit_code == 0 assert _simplify_path(result.stdout.strip()) == snapshot( "fixtures/sample_package/sample_module.py:39:5" ) def test_find_missing_model(cli_app: Typer, cli_runner: CliRunner): selector = "tests.fixtures.sample_package.sample_module:schema" result = cli_runner.invoke(cli_app, ["locate-definition", selector, "Missing"]) assert result.exit_code == 1 assert result.stderr.strip() == snapshot("Definition not found: Missing") def test_find_missing_model_field(cli_app: Typer, cli_runner: CliRunner): selector = "tests.fixtures.sample_package.sample_module:schema" result = cli_runner.invoke( cli_app, ["locate-definition", selector, "Missing.field"] ) assert result.exit_code == 1 assert result.stderr.strip() == snapshot("Definition not found: Missing.field") def test_find_missing_schema(cli_app: Typer, cli_runner: CliRunner): selector = "tests.fixtures.sample_package.sample_module:missing" result = cli_runner.invoke(cli_app, ["locate-definition", selector, "User"]) assert result.exit_code == 2 strawberry-graphql-0.287.0/tests/cli/test_schema_codegen.py000066400000000000000000000031421511033167500240000ustar00rootroot00000000000000from pathlib import Path import pytest from typer import Typer from typer.testing import CliRunner schema = """ type Query { hello: String! } """ expected_output = """ import strawberry @strawberry.type class Query: hello: str schema = strawberry.Schema(query=Query) """.strip() @pytest.fixture def schema_file(tmp_path: Path) -> Path: schema_file = tmp_path / "schema.graphql" schema_file.write_text(schema) return schema_file def test_schema_codegen(cli_app: Typer, cli_runner: CliRunner, schema_file: Path): result = cli_runner.invoke(cli_app, ["schema-codegen", str(schema_file)]) assert result.exit_code == 0 assert result.stdout.strip() == expected_output def test_schema_codegen_to_file( cli_app: Typer, cli_runner: CliRunner, schema_file: Path, tmp_path: Path ): output_file = tmp_path / "schema.py" result = cli_runner.invoke( cli_app, ["schema-codegen", str(schema_file), "--output", str(output_file)] ) assert "Code generated at `schema.py`" in result.stdout.strip() assert result.exit_code == 0 assert output_file.read_text().strip() == expected_output def test_overrides_file_if_exists( cli_app: Typer, cli_runner: CliRunner, schema_file: Path, tmp_path: Path ): output_file = tmp_path / "schema.py" output_file.write_text("old content") result = cli_runner.invoke( cli_app, ["schema-codegen", str(schema_file), "--output", str(output_file)] ) assert "Code generated at `schema.py`" in result.stdout.strip() assert result.exit_code == 0 assert output_file.read_text().strip() == expected_output strawberry-graphql-0.287.0/tests/cli/test_server.py000066400000000000000000000006641511033167500223700ustar00rootroot00000000000000from typer import Typer from typer.testing import CliRunner def test_command_fails_with_deprecation_message(cli_app: Typer, cli_runner: CliRunner): schema = "tests.fixtures.sample_package.sample_module" result = cli_runner.invoke(cli_app, ["server", schema]) assert result.exit_code == 1 assert ( result.stdout == "The `strawberry server` command is deprecated, use `strawberry dev` instead.\n" ) strawberry-graphql-0.287.0/tests/cli/test_upgrade.py000066400000000000000000000034141511033167500225050ustar00rootroot00000000000000from pathlib import Path from pytest_snapshot.plugin import Snapshot from typer import Typer from typer.testing import CliRunner HERE = Path(__file__).parent def test_upgrade_returns_error_code_if_codemod_does_not_exist( cli_app: Typer, cli_runner: CliRunner ): result = cli_runner.invoke( cli_app, ["upgrade", "a_random_codemod", "."], ) assert result.exit_code == 2 assert 'Upgrade named "a_random_codemod" does not exist' in result.stdout def test_upgrade_works_annotated_unions( cli_app: Typer, cli_runner: CliRunner, tmp_path: Path, snapshot: Snapshot ): source = HERE / "fixtures/unions.py" target = tmp_path / "unions.py" target.write_text(source.read_text()) result = cli_runner.invoke( cli_app, ["upgrade", "--python-target", "3.11", "annotated-union", str(target)], ) assert result.exit_code == 1 assert "1 files changed\n - 0 files skipped" in result.stdout snapshot.snapshot_dir = HERE / "snapshots" snapshot.assert_match(target.read_text(), "unions.py") def test_upgrade_works_annotated_unions_typing_extensions( cli_app: Typer, cli_runner: CliRunner, tmp_path: Path, snapshot: Snapshot ): source = HERE / "fixtures/unions.py" target = tmp_path / "unions.py" target.write_text(source.read_text()) result = cli_runner.invoke( cli_app, [ "upgrade", "--use-typing-extensions", "--python-target", "3.11", "annotated-union", str(target), ], ) assert result.exit_code == 1 assert "1 files changed\n - 0 files skipped" in result.stdout snapshot.snapshot_dir = HERE / "snapshots" snapshot.assert_match(target.read_text(), "unions_typing_extension.py") strawberry-graphql-0.287.0/tests/codegen/000077500000000000000000000000001511033167500203005ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/codegen/__init__.py000066400000000000000000000000001511033167500223770ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/codegen/conftest.py000066400000000000000000000073211511033167500225020ustar00rootroot00000000000000import datetime import decimal import enum import random from typing import ( TYPE_CHECKING, Annotated, Generic, NewType, TypeVar, ) from uuid import UUID import pytest import strawberry if TYPE_CHECKING: from .lazy_type import LaziestType JSON = strawberry.scalar(NewType("JSON", str)) @strawberry.enum class Color(enum.Enum): RED = "red" GREEN = "green" BLUE = "blue" @strawberry.type class Person: name: str age: int @strawberry.type class Animal: name: str age: int LivingThing1 = TypeVar("LivingThing1") LivingThing2 = TypeVar("LivingThing2") @strawberry.type class LifeContainer(Generic[LivingThing1, LivingThing2]): items1: list[LivingThing1] items2: list[LivingThing2] PersonOrAnimal = Annotated[Person | Animal, strawberry.union("PersonOrAnimal")] @strawberry.interface class Node: id: str @strawberry.type class BlogPost(Node): title: str def __init__(self, id: str, title: str) -> None: self.id = id self.title = title @strawberry.type class Image(Node): url: str @strawberry.input class PersonInput: name: str age: int | None = strawberry.UNSET @strawberry.input class ExampleInput: id: strawberry.ID name: str age: int person: PersonInput | None people: list[PersonInput] optional_people: list[PersonInput] | None @strawberry.type class Query: id: strawberry.ID integer: int float: float boolean: bool uuid: UUID date: datetime.date datetime: datetime.datetime time: datetime.time decimal: decimal.Decimal optional_int: int | None list_of_int: list[int] list_of_optional_int: list[int | None] optional_list_of_optional_int: list[int | None] | None person: Person optional_person: Person | None list_of_people: list[Person] optional_list_of_people: list[Person] | None enum: Color json: JSON union: PersonOrAnimal optional_union: PersonOrAnimal | None interface: Node lazy: Annotated["LaziestType", strawberry.lazy("tests.codegen.lazy_type")] @strawberry.field def with_inputs(self, id: strawberry.ID | None, input: ExampleInput) -> bool: return True @strawberry.field def get_person_or_animal(self) -> Person | Animal: """Randomly get a person or an animal.""" p_or_a = random.choice([Person, Animal])() # noqa: S311 p_or_a.name = "Howard" p_or_a.age = 7 return p_or_a @strawberry.field def list_life() -> LifeContainer[Person, Animal]: """Get lists of living things.""" person = Person(name="Henry", age=10) dinosaur = Animal(name="rex", age=66_000_000) return LifeContainer([person], [dinosaur]) @strawberry.input class BlogPostInput: title: str = "I replaced my doorbell. You wouldn't believe what happened next!" color: Color = Color.RED pi: float = 3.14159 a_bool: bool = True an_int: int = 42 an_optional_int: int | None = None @strawberry.input class AddBlogPostsInput: posts: list[BlogPostInput] @strawberry.type class AddBlogPostsOutput: posts: list[BlogPost] @strawberry.type class Mutation: @strawberry.mutation def add_book(self, name: str) -> BlogPost: return BlogPost(id="c6f1c3ce-5249-4570-9182-c2836b836d14", name=name) @strawberry.mutation def add_blog_posts(self, input: AddBlogPostsInput) -> AddBlogPostsOutput: output = AddBlogPostsOutput() output.posts = [] for i, title in enumerate(input.posts): output.posts.append(BlogPost(str(i), title)) return output @pytest.fixture def schema() -> strawberry.Schema: return strawberry.Schema(query=Query, mutation=Mutation, types=[BlogPost, Image]) strawberry-graphql-0.287.0/tests/codegen/lazy_type.py000066400000000000000000000001141511033167500226660ustar00rootroot00000000000000import strawberry @strawberry.type class LaziestType: something: bool strawberry-graphql-0.287.0/tests/codegen/queries/000077500000000000000000000000001511033167500217555ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/codegen/queries/alias.graphql000066400000000000000000000001371511033167500244270ustar00rootroot00000000000000query OperationName { id second_id: id a_float: float lazy { lazy: something } } strawberry-graphql-0.287.0/tests/codegen/queries/basic.graphql000066400000000000000000000001761511033167500244220ustar00rootroot00000000000000query OperationName { id integer float boolean uuid date datetime time decimal lazy { something } } strawberry-graphql-0.287.0/tests/codegen/queries/custom_scalar.graphql000066400000000000000000000000371511033167500261740ustar00rootroot00000000000000query OperationName { json } strawberry-graphql-0.287.0/tests/codegen/queries/enum.graphql000066400000000000000000000000371511033167500243010ustar00rootroot00000000000000query OperationName { enum } strawberry-graphql-0.287.0/tests/codegen/queries/fragment.graphql000066400000000000000000000001431511033167500251360ustar00rootroot00000000000000fragment PersonName on Person { name } query OperationName { person { ...PersonName } } strawberry-graphql-0.287.0/tests/codegen/queries/generic_types.graphql000066400000000000000000000001731511033167500261760ustar00rootroot00000000000000query ListLifeGeneric { listLife { items1 { name age } items2 { name age } } } strawberry-graphql-0.287.0/tests/codegen/queries/interface.graphql000066400000000000000000000000611511033167500252720ustar00rootroot00000000000000query OperationName { interface { id } } strawberry-graphql-0.287.0/tests/codegen/queries/interface_fragments.graphql000066400000000000000000000001741511033167500273450ustar00rootroot00000000000000query OperationName { interface { id ... on BlogPost { title } ... on Image { url } } } strawberry-graphql-0.287.0/tests/codegen/queries/interface_fragments_with_spread.graphql000066400000000000000000000002731511033167500317360ustar00rootroot00000000000000fragment PartialBlogPost on BlogPost { title } query OperationName { interface { id ... on BlogPost { ...PartialBlogPost } ... on Image { url } } } strawberry-graphql-0.287.0/tests/codegen/queries/interface_single_fragment.graphql000066400000000000000000000001311511033167500305140ustar00rootroot00000000000000query OperationName { interface { id ... on BlogPost { title } } } strawberry-graphql-0.287.0/tests/codegen/queries/multiple_types.graphql000066400000000000000000000001161511033167500264120ustar00rootroot00000000000000query OperationName { person { name } listOfPeople { name } } strawberry-graphql-0.287.0/tests/codegen/queries/multiple_types_optional.graphql000066400000000000000000000000701511033167500303160ustar00rootroot00000000000000query OperationName { optionalPerson { name } } strawberry-graphql-0.287.0/tests/codegen/queries/mutation-fragment.graphql000066400000000000000000000002011511033167500267670ustar00rootroot00000000000000fragment IdFragment on BlogPost { id } mutation addBook($input: String!) { addBook(input: $input) { ...IdFragment } } strawberry-graphql-0.287.0/tests/codegen/queries/mutation.graphql000066400000000000000000000001141511033167500251710ustar00rootroot00000000000000mutation addBook($input: String!) { addBook(input: $input) { id } } strawberry-graphql-0.287.0/tests/codegen/queries/mutation_with_object.graphql000066400000000000000000000002001511033167500275460ustar00rootroot00000000000000mutation AddBlogPosts($input: [BlogPostInput!]!) { addBlogPosts(input: {posts: $input}) { posts { title } } } strawberry-graphql-0.287.0/tests/codegen/queries/nullable_list_of_non_scalars.graphql000066400000000000000000000001061511033167500312310ustar00rootroot00000000000000query OperationName { optionalListOfPeople { name age } } strawberry-graphql-0.287.0/tests/codegen/queries/optional_and_lists.graphql000066400000000000000000000001421511033167500272170ustar00rootroot00000000000000query OperationName { optionalInt listOfInt listOfOptionalInt optionalListOfOptionalInt } strawberry-graphql-0.287.0/tests/codegen/queries/union.graphql000066400000000000000000000003161511033167500244650ustar00rootroot00000000000000query OperationName { union { ... on Animal { age } ... on Person { name } } optionalUnion { ... on Animal { age } ... on Person { name } } } strawberry-graphql-0.287.0/tests/codegen/queries/union_return.graphql000066400000000000000000000001411511033167500260600ustar00rootroot00000000000000query OperationName { getPersonOrAnimal { ... on Person { name age } } } strawberry-graphql-0.287.0/tests/codegen/queries/union_with_one_type.graphql000066400000000000000000000001251511033167500274200ustar00rootroot00000000000000query OperationName { union { ... on Animal { age name } } } strawberry-graphql-0.287.0/tests/codegen/queries/union_with_typename.graphql000066400000000000000000000003331511033167500274210ustar00rootroot00000000000000query OperationName { __typename union { ... on Animal { age } ... on Person { name } } optionalUnion { ... on Animal { age } ... on Person { name } } } strawberry-graphql-0.287.0/tests/codegen/queries/union_with_typename_and_fragment.graphql000066400000000000000000000004521511033167500321300ustar00rootroot00000000000000fragment AnimalProjection on Animal { age } query OperationName { __typename union { ... on Animal { ...AnimalProjection } ... on Person { name } } optionalUnion { ... on Animal { ...AnimalProjection } ... on Person { name } } } strawberry-graphql-0.287.0/tests/codegen/queries/variables.graphql000066400000000000000000000002071511033167500253040ustar00rootroot00000000000000query OperationName($id: ID, $input: ExampleInput!, $ids: [ID!]!, $ids2: [ID], $ids3: [[ID]]) { withInputs(id: $id, input: $input) } strawberry-graphql-0.287.0/tests/codegen/queries/with_directives.graphql000066400000000000000000000002011511033167500265220ustar00rootroot00000000000000query OperationName @owner(name: "Patrick", age: 30, items: [1, 2, 3], enum: NAME, bool: true) { person { name @root } } strawberry-graphql-0.287.0/tests/codegen/snapshots/000077500000000000000000000000001511033167500223225ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/codegen/snapshots/python/000077500000000000000000000000001511033167500236435ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/codegen/snapshots/python/alias.py000066400000000000000000000003411511033167500253040ustar00rootroot00000000000000class OperationNameResultLazy: # alias for something lazy: bool class OperationNameResult: id: str # alias for id second_id: str # alias for float a_float: float lazy: OperationNameResultLazy strawberry-graphql-0.287.0/tests/codegen/snapshots/python/basic.py000066400000000000000000000005471511033167500253040ustar00rootroot00000000000000from uuid import UUID from datetime import date, datetime, time from decimal import Decimal class OperationNameResultLazy: something: bool class OperationNameResult: id: str integer: int float: float boolean: bool uuid: UUID date: date datetime: datetime time: time decimal: Decimal lazy: OperationNameResultLazy strawberry-graphql-0.287.0/tests/codegen/snapshots/python/custom_scalar.py000066400000000000000000000001431511033167500270520ustar00rootroot00000000000000from typing import NewType JSON = NewType("JSON", str) class OperationNameResult: json: JSON strawberry-graphql-0.287.0/tests/codegen/snapshots/python/enum.py000066400000000000000000000002141511033167500251560ustar00rootroot00000000000000from enum import Enum class Color(Enum): RED = "RED" GREEN = "GREEN" BLUE = "BLUE" class OperationNameResult: enum: Color strawberry-graphql-0.287.0/tests/codegen/snapshots/python/fragment.py000066400000000000000000000001521511033167500260160ustar00rootroot00000000000000class PersonName: # typename: Person name: str class OperationNameResult: person: PersonName strawberry-graphql-0.287.0/tests/codegen/snapshots/python/generic_types.py000066400000000000000000000006031511033167500270540ustar00rootroot00000000000000from typing import List class ListLifeGenericResultListLifeItems1: name: str age: int class ListLifeGenericResultListLifeItems2: name: str age: int class ListLifeGenericResultListLife: items1: list[ListLifeGenericResultListLifeItems1] items2: list[ListLifeGenericResultListLifeItems2] class ListLifeGenericResult: list_life: ListLifeGenericResultListLife strawberry-graphql-0.287.0/tests/codegen/snapshots/python/interface.py000066400000000000000000000001701511033167500261530ustar00rootroot00000000000000class OperationNameResultInterface: id: str class OperationNameResult: interface: OperationNameResultInterface strawberry-graphql-0.287.0/tests/codegen/snapshots/python/interface_fragments.py000066400000000000000000000006121511033167500302220ustar00rootroot00000000000000from typing import Union class OperationNameResultInterfaceBlogPost: # typename: BlogPost id: str title: str class OperationNameResultInterfaceImage: # typename: Image id: str url: str OperationNameResultInterface = Union[OperationNameResultInterfaceBlogPost, OperationNameResultInterfaceImage] class OperationNameResult: interface: OperationNameResultInterface strawberry-graphql-0.287.0/tests/codegen/snapshots/python/interface_fragments_with_spread.py000066400000000000000000000007121511033167500326140ustar00rootroot00000000000000from typing import Union class PartialBlogPost: # typename: BlogPost title: str class OperationNameResultInterfaceBlogPost: # typename: BlogPost id: str title: str class OperationNameResultInterfaceImage: # typename: Image id: str url: str OperationNameResultInterface = Union[OperationNameResultInterfaceBlogPost, OperationNameResultInterfaceImage] class OperationNameResult: interface: OperationNameResultInterface strawberry-graphql-0.287.0/tests/codegen/snapshots/python/interface_single_fragment.py000066400000000000000000000002601511033167500313770ustar00rootroot00000000000000class OperationNameResultInterfaceBlogPost: # typename: BlogPost id: str title: str class OperationNameResult: interface: OperationNameResultInterfaceBlogPost strawberry-graphql-0.287.0/tests/codegen/snapshots/python/multiple_types.py000066400000000000000000000003721511033167500272760ustar00rootroot00000000000000from typing import List class OperationNameResultPerson: name: str class OperationNameResultListOfPeople: name: str class OperationNameResult: person: OperationNameResultPerson list_of_people: list[OperationNameResultListOfPeople] strawberry-graphql-0.287.0/tests/codegen/snapshots/python/multiple_types_optional.py000066400000000000000000000002611511033167500312000ustar00rootroot00000000000000from typing import Optional class OperationNameResultOptionalPerson: name: str class OperationNameResult: optional_person: Optional[OperationNameResultOptionalPerson] strawberry-graphql-0.287.0/tests/codegen/snapshots/python/mutation-fragment.py000066400000000000000000000002161511033167500276550ustar00rootroot00000000000000class IdFragment: # typename: BlogPost id: str class addBookResult: add_book: IdFragment class addBookVariables: input: str strawberry-graphql-0.287.0/tests/codegen/snapshots/python/mutation.py000066400000000000000000000002111511033167500260470ustar00rootroot00000000000000class addBookResultAddBook: id: str class addBookResult: add_book: addBookResultAddBook class addBookVariables: input: str strawberry-graphql-0.287.0/tests/codegen/snapshots/python/mutation_with_object.py000066400000000000000000000012331511033167500304350ustar00rootroot00000000000000from typing import List, Optional from enum import Enum class AddBlogPostsResultAddBlogPostsPosts: title: str class AddBlogPostsResultAddBlogPosts: posts: list[AddBlogPostsResultAddBlogPostsPosts] class AddBlogPostsResult: add_blog_posts: AddBlogPostsResultAddBlogPosts class Color(Enum): RED = "RED" GREEN = "GREEN" BLUE = "BLUE" class BlogPostInput: title: str = "I replaced my doorbell. You wouldn't believe what happened next!" color: Color = Color.RED pi: float = 3.14159 a_bool: bool = True an_int: int = 42 an_optional_int: Optional[int] = None class AddBlogPostsVariables: input: list[BlogPostInput] strawberry-graphql-0.287.0/tests/codegen/snapshots/python/nullable_list_of_non_scalars.py000066400000000000000000000003361511033167500321160ustar00rootroot00000000000000from typing import List, Optional class OperationNameResultOptionalListOfPeople: name: str age: int class OperationNameResult: optional_list_of_people: Optional[list[OperationNameResultOptionalListOfPeople]] strawberry-graphql-0.287.0/tests/codegen/snapshots/python/optional_and_lists.py000066400000000000000000000003501511033167500301000ustar00rootroot00000000000000from typing import List, Optional class OperationNameResult: optional_int: Optional[int] list_of_int: list[int] list_of_optional_int: list[Optional[int]] optional_list_of_optional_int: Optional[list[Optional[int]]] strawberry-graphql-0.287.0/tests/codegen/snapshots/python/union.py000066400000000000000000000012741511033167500253510ustar00rootroot00000000000000from typing import Optional, Union class OperationNameResultUnionAnimal: # typename: Animal age: int class OperationNameResultUnionPerson: # typename: Person name: str OperationNameResultUnion = Union[OperationNameResultUnionAnimal, OperationNameResultUnionPerson] class OperationNameResultOptionalUnionAnimal: # typename: Animal age: int class OperationNameResultOptionalUnionPerson: # typename: Person name: str OperationNameResultOptionalUnion = Union[OperationNameResultOptionalUnionAnimal, OperationNameResultOptionalUnionPerson] class OperationNameResult: union: OperationNameResultUnion optional_union: Optional[OperationNameResultOptionalUnion] strawberry-graphql-0.287.0/tests/codegen/snapshots/python/union_return.py000066400000000000000000000003051511033167500267420ustar00rootroot00000000000000class OperationNameResultGetPersonOrAnimalPerson: # typename: Person name: str age: int class OperationNameResult: get_person_or_animal: OperationNameResultGetPersonOrAnimalPerson strawberry-graphql-0.287.0/tests/codegen/snapshots/python/union_with_one_type.py000066400000000000000000000002361511033167500303030ustar00rootroot00000000000000class OperationNameResultUnionAnimal: # typename: Animal age: int name: str class OperationNameResult: union: OperationNameResultUnionAnimal strawberry-graphql-0.287.0/tests/codegen/snapshots/python/union_with_typename.py000066400000000000000000000012741511033167500303060ustar00rootroot00000000000000from typing import Optional, Union class OperationNameResultUnionAnimal: # typename: Animal age: int class OperationNameResultUnionPerson: # typename: Person name: str OperationNameResultUnion = Union[OperationNameResultUnionAnimal, OperationNameResultUnionPerson] class OperationNameResultOptionalUnionAnimal: # typename: Animal age: int class OperationNameResultOptionalUnionPerson: # typename: Person name: str OperationNameResultOptionalUnion = Union[OperationNameResultOptionalUnionAnimal, OperationNameResultOptionalUnionPerson] class OperationNameResult: union: OperationNameResultUnion optional_union: Optional[OperationNameResultOptionalUnion] strawberry-graphql-0.287.0/tests/codegen/snapshots/python/union_with_typename_and_fragment.py000066400000000000000000000010671511033167500330130ustar00rootroot00000000000000from typing import Optional, Union class AnimalProjection: # typename: Animal age: int class OperationNameResultUnionPerson: # typename: Person name: str OperationNameResultUnion = Union[AnimalProjection, OperationNameResultUnionPerson] class OperationNameResultOptionalUnionPerson: # typename: Person name: str OperationNameResultOptionalUnion = Union[AnimalProjection, OperationNameResultOptionalUnionPerson] class OperationNameResult: union: OperationNameResultUnion optional_union: Optional[OperationNameResultOptionalUnion] strawberry-graphql-0.287.0/tests/codegen/snapshots/python/variables.py000066400000000000000000000010011511033167500261550ustar00rootroot00000000000000from typing import List, Optional class OperationNameResult: with_inputs: bool class PersonInput: name: str age: Optional[int] = None class ExampleInput: id: str name: str age: int person: Optional[PersonInput] people: list[PersonInput] optional_people: Optional[list[PersonInput]] class OperationNameVariables: id: Optional[str] input: ExampleInput ids: list[str] ids2: Optional[list[Optional[str]]] ids3: Optional[list[Optional[list[Optional[str]]]]] strawberry-graphql-0.287.0/tests/codegen/snapshots/python/with_directives.py000066400000000000000000000001611511033167500274070ustar00rootroot00000000000000class OperationNameResultPerson: name: str class OperationNameResult: person: OperationNameResultPerson strawberry-graphql-0.287.0/tests/codegen/snapshots/typescript/000077500000000000000000000000001511033167500245305ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/codegen/snapshots/typescript/alias.ts000066400000000000000000000003661511033167500261760ustar00rootroot00000000000000type OperationNameResultLazy = { // alias for something lazy: boolean } type OperationNameResult = { id: string // alias for id second_id: string // alias for float a_float: number lazy: OperationNameResultLazy } strawberry-graphql-0.287.0/tests/codegen/snapshots/typescript/basic.ts000066400000000000000000000004421511033167500261610ustar00rootroot00000000000000type OperationNameResultLazy = { something: boolean } type OperationNameResult = { id: string integer: number float: number boolean: boolean uuid: string date: string datetime: string time: string decimal: string lazy: OperationNameResultLazy } strawberry-graphql-0.287.0/tests/codegen/snapshots/typescript/custom_scalar.ts000066400000000000000000000001021511033167500277300ustar00rootroot00000000000000type JSON = string type OperationNameResult = { json: JSON } strawberry-graphql-0.287.0/tests/codegen/snapshots/typescript/enum.ts000066400000000000000000000001701511033167500260420ustar00rootroot00000000000000enum Color { RED = "RED", GREEN = "GREEN", BLUE = "BLUE", } type OperationNameResult = { enum: Color } strawberry-graphql-0.287.0/tests/codegen/snapshots/typescript/fragment.ts000066400000000000000000000001361511033167500267030ustar00rootroot00000000000000type PersonName = { name: string } type OperationNameResult = { person: PersonName } strawberry-graphql-0.287.0/tests/codegen/snapshots/typescript/generic_types.ts000066400000000000000000000005761511033167500277500ustar00rootroot00000000000000type ListLifeGenericResultListLifeItems1 = { name: string age: number } type ListLifeGenericResultListLifeItems2 = { name: string age: number } type ListLifeGenericResultListLife = { items1: ListLifeGenericResultListLifeItems1[] items2: ListLifeGenericResultListLifeItems2[] } type ListLifeGenericResult = { list_life: ListLifeGenericResultListLife } strawberry-graphql-0.287.0/tests/codegen/snapshots/typescript/interface.ts000066400000000000000000000002031511033167500270330ustar00rootroot00000000000000type OperationNameResultInterface = { id: string } type OperationNameResult = { interface: OperationNameResultInterface } strawberry-graphql-0.287.0/tests/codegen/snapshots/typescript/interface_fragments.ts000066400000000000000000000005301511033167500311040ustar00rootroot00000000000000type OperationNameResultInterfaceBlogPost = { id: string title: string } type OperationNameResultInterfaceImage = { id: string url: string } type OperationNameResultInterface = OperationNameResultInterfaceBlogPost | OperationNameResultInterfaceImage type OperationNameResult = { interface: OperationNameResultInterface } strawberry-graphql-0.287.0/tests/codegen/snapshots/typescript/interface_fragments_with_spread.ts000066400000000000000000000006061511033167500335010ustar00rootroot00000000000000type PartialBlogPost = { title: string } type OperationNameResultInterfaceBlogPost = { id: string title: string } type OperationNameResultInterfaceImage = { id: string url: string } type OperationNameResultInterface = OperationNameResultInterfaceBlogPost | OperationNameResultInterfaceImage type OperationNameResult = { interface: OperationNameResultInterface } strawberry-graphql-0.287.0/tests/codegen/snapshots/typescript/interface_single_fragment.ts000066400000000000000000000002451511033167500322650ustar00rootroot00000000000000type OperationNameResultInterfaceBlogPost = { id: string title: string } type OperationNameResult = { interface: OperationNameResultInterfaceBlogPost } strawberry-graphql-0.287.0/tests/codegen/snapshots/typescript/multiple_types.ts000066400000000000000000000003571511033167500301640ustar00rootroot00000000000000type OperationNameResultPerson = { name: string } type OperationNameResultListOfPeople = { name: string } type OperationNameResult = { person: OperationNameResultPerson list_of_people: OperationNameResultListOfPeople[] } strawberry-graphql-0.287.0/tests/codegen/snapshots/typescript/multiple_types_optional.ts000066400000000000000000000002411511033167500320610ustar00rootroot00000000000000type OperationNameResultOptionalPerson = { name: string } type OperationNameResult = { optional_person: OperationNameResultOptionalPerson | undefined } strawberry-graphql-0.287.0/tests/codegen/snapshots/typescript/mutation-fragment.ts000066400000000000000000000002071511033167500305400ustar00rootroot00000000000000type IdFragment = { id: string } type addBookResult = { add_book: IdFragment } type addBookVariables = { input: string } strawberry-graphql-0.287.0/tests/codegen/snapshots/typescript/mutation.ts000066400000000000000000000002331511033167500267360ustar00rootroot00000000000000type addBookResultAddBook = { id: string } type addBookResult = { add_book: addBookResultAddBook } type addBookVariables = { input: string } strawberry-graphql-0.287.0/tests/codegen/snapshots/typescript/mutation_with_object.ts000066400000000000000000000010201511033167500313120ustar00rootroot00000000000000type AddBlogPostsResultAddBlogPostsPosts = { title: string } type AddBlogPostsResultAddBlogPosts = { posts: AddBlogPostsResultAddBlogPostsPosts[] } type AddBlogPostsResult = { add_blog_posts: AddBlogPostsResultAddBlogPosts } enum Color { RED = "RED", GREEN = "GREEN", BLUE = "BLUE", } type BlogPostInput = { title: string color: Color pi: number a_bool: boolean an_int: number an_optional_int: number | undefined } type AddBlogPostsVariables = { input: BlogPostInput[] } strawberry-graphql-0.287.0/tests/codegen/snapshots/typescript/nullable_list_of_non_scalars.ts000066400000000000000000000003071511033167500327770ustar00rootroot00000000000000type OperationNameResultOptionalListOfPeople = { name: string age: number } type OperationNameResult = { optional_list_of_people: OperationNameResultOptionalListOfPeople[] | undefined } strawberry-graphql-0.287.0/tests/codegen/snapshots/typescript/optional_and_lists.ts000066400000000000000000000003251511033167500307650ustar00rootroot00000000000000type OperationNameResult = { optional_int: number | undefined list_of_int: number[] list_of_optional_int: (number | undefined)[] optional_list_of_optional_int: (number | undefined)[] | undefined } strawberry-graphql-0.287.0/tests/codegen/snapshots/typescript/union.ts000066400000000000000000000011341511033167500262270ustar00rootroot00000000000000type OperationNameResultUnionAnimal = { age: number } type OperationNameResultUnionPerson = { name: string } type OperationNameResultUnion = OperationNameResultUnionAnimal | OperationNameResultUnionPerson type OperationNameResultOptionalUnionAnimal = { age: number } type OperationNameResultOptionalUnionPerson = { name: string } type OperationNameResultOptionalUnion = OperationNameResultOptionalUnionAnimal | OperationNameResultOptionalUnionPerson type OperationNameResult = { union: OperationNameResultUnion optional_union: OperationNameResultOptionalUnion | undefined } strawberry-graphql-0.287.0/tests/codegen/snapshots/typescript/union_return.ts000066400000000000000000000002741511033167500276320ustar00rootroot00000000000000type OperationNameResultGetPersonOrAnimalPerson = { name: string age: number } type OperationNameResult = { get_person_or_animal: OperationNameResultGetPersonOrAnimalPerson } strawberry-graphql-0.287.0/tests/codegen/snapshots/typescript/union_with_one_type.ts000066400000000000000000000002251511033167500311640ustar00rootroot00000000000000type OperationNameResultUnionAnimal = { age: number name: string } type OperationNameResult = { union: OperationNameResultUnionAnimal } strawberry-graphql-0.287.0/tests/codegen/snapshots/typescript/union_with_typename.ts000066400000000000000000000011631511033167500311660ustar00rootroot00000000000000type OperationNameResultUnionAnimal = { age: number } type OperationNameResultUnionPerson = { name: string } type OperationNameResultUnion = OperationNameResultUnionAnimal | OperationNameResultUnionPerson type OperationNameResultOptionalUnionAnimal = { age: number } type OperationNameResultOptionalUnionPerson = { name: string } type OperationNameResultOptionalUnion = OperationNameResultOptionalUnionAnimal | OperationNameResultOptionalUnionPerson type OperationNameResult = { __typename: string union: OperationNameResultUnion optional_union: OperationNameResultOptionalUnion | undefined } strawberry-graphql-0.287.0/tests/codegen/snapshots/typescript/union_with_typename_and_fragment.ts000066400000000000000000000007761511033167500337040ustar00rootroot00000000000000type AnimalProjection = { age: number } type OperationNameResultUnionPerson = { name: string } type OperationNameResultUnion = AnimalProjection | OperationNameResultUnionPerson type OperationNameResultOptionalUnionPerson = { name: string } type OperationNameResultOptionalUnion = AnimalProjection | OperationNameResultOptionalUnionPerson type OperationNameResult = { __typename: string union: OperationNameResultUnion optional_union: OperationNameResultOptionalUnion | undefined } strawberry-graphql-0.287.0/tests/codegen/snapshots/typescript/variables.ts000066400000000000000000000010051511033167500270440ustar00rootroot00000000000000type OperationNameResult = { with_inputs: boolean } type PersonInput = { name: string age: number | undefined } type ExampleInput = { id: string name: string age: number person: PersonInput | undefined people: PersonInput[] optional_people: PersonInput[] | undefined } type OperationNameVariables = { id: string | undefined input: ExampleInput ids: string[] ids2: (string | undefined)[] | undefined ids3: ((string | undefined)[] | undefined)[] | undefined } strawberry-graphql-0.287.0/tests/codegen/snapshots/typescript/with_directives.ts000066400000000000000000000001741511033167500302760ustar00rootroot00000000000000type OperationNameResultPerson = { name: string } type OperationNameResult = { person: OperationNameResultPerson } strawberry-graphql-0.287.0/tests/codegen/test_print_operation.py000066400000000000000000000010711511033167500251240ustar00rootroot00000000000000from pathlib import Path import pytest from strawberry.codegen import QueryCodegen from strawberry.codegen.plugins.print_operation import PrintOperationPlugin HERE = Path(__file__).parent QUERIES = list(HERE.glob("queries/*.graphql")) @pytest.mark.parametrize("query", QUERIES, ids=[x.name for x in QUERIES]) def test_codegen( query: Path, schema, ): generator = QueryCodegen(schema, plugins=[PrintOperationPlugin(query)]) query_content = query.read_text() result = generator.run(query_content) assert result.to_string() == query_content strawberry-graphql-0.287.0/tests/codegen/test_query_codegen.py000066400000000000000000000045361511033167500245520ustar00rootroot00000000000000# - 1. test fragments # - 2. test variables # - 3. test input objects # - 4. test mutations (raise?) # - 5. test subscriptions (raise) from pathlib import Path import pytest from pytest_snapshot.plugin import Snapshot from strawberry.codegen import QueryCodegen, QueryCodegenPlugin from strawberry.codegen.exceptions import ( MultipleOperationsProvidedError, NoOperationNameProvidedError, NoOperationProvidedError, ) from strawberry.codegen.plugins.python import PythonPlugin from strawberry.codegen.plugins.typescript import TypeScriptPlugin HERE = Path(__file__).parent QUERIES = list(HERE.glob("queries/*.graphql")) @pytest.mark.parametrize( ("plugin_class", "plugin_name", "extension"), [ (PythonPlugin, "python", "py"), (TypeScriptPlugin, "typescript", "ts"), ], ids=["python", "typescript"], ) @pytest.mark.parametrize("query", QUERIES, ids=[x.name for x in QUERIES]) def test_codegen( query: Path, plugin_class: type[QueryCodegenPlugin], plugin_name: str, extension: str, snapshot: Snapshot, schema, ): generator = QueryCodegen(schema, plugins=[plugin_class(query)]) result = generator.run(query.read_text()) code = result.to_string() snapshot.snapshot_dir = HERE / "snapshots" / plugin_name snapshot.assert_match(code, f"{query.with_suffix('').stem}.{extension}") def test_codegen_fails_if_no_operation_name(schema, tmp_path): query = tmp_path / "query.graphql" data = "query { hello }" with query.open("w") as f: f.write(data) generator = QueryCodegen(schema, plugins=[PythonPlugin(query)]) with pytest.raises(NoOperationNameProvidedError): generator.run(data) def test_codegen_fails_if_no_operation(schema, tmp_path): query = tmp_path / "query.graphql" data = "type X { hello: String }" with query.open("w") as f: f.write(data) generator = QueryCodegen(schema, plugins=[PythonPlugin(query)]) with pytest.raises(NoOperationProvidedError): generator.run(data) def test_fails_with_multiple_operations(schema, tmp_path): query = tmp_path / "query.graphql" data = "query { hello } query { world }" with query.open("w") as f: f.write(data) generator = QueryCodegen(schema, plugins=[PythonPlugin(query)]) with pytest.raises(MultipleOperationsProvidedError): generator.run(data) strawberry-graphql-0.287.0/tests/codemods/000077500000000000000000000000001511033167500204715ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/codemods/__init__.py000066400000000000000000000000001511033167500225700ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/codemods/test_annotated_unions.py000066400000000000000000000100651511033167500254540ustar00rootroot00000000000000from libcst.codemod import CodemodTest from strawberry.codemods.annotated_unions import ConvertUnionToAnnotatedUnion class TestConvertConstantCommand(CodemodTest): TRANSFORM = ConvertUnionToAnnotatedUnion def test_update_union(self) -> None: before = """ AUnion = strawberry.union(name="ABC", types=(Foo, Bar)) """ after = """ from typing import Annotated, Union AUnion = Annotated[Union[Foo, Bar], strawberry.union(name="ABC")] """ self.assertCodemod( before, after, use_pipe_syntax=False, use_typing_extensions=False ) def test_update_union_typing_extensions(self) -> None: before = """ AUnion = strawberry.union(name="ABC", types=(Foo, Bar)) """ after = """ from typing import Annotated, Union AUnion = Annotated[Union[Foo, Bar], strawberry.union(name="ABC")] """ self.assertCodemod(before, after, use_pipe_syntax=False) def test_update_union_using_import(self) -> None: before = """ from strawberry import union AUnion = union(name="ABC", types=(Foo, Bar)) """ after = """ from typing import Annotated, Union AUnion = Annotated[Union[Foo, Bar], strawberry.union(name="ABC")] """ self.assertCodemod(before, after, use_pipe_syntax=False) def test_noop_other_union(self) -> None: before = """ from potato import union union("A", "B") """ after = """ from potato import union union("A", "B") """ self.assertCodemod(before, after, use_pipe_syntax=False) def test_update_union_positional_name(self) -> None: before = """ AUnion = strawberry.union("ABC", types=(Foo, Bar)) """ after = """ from typing import Annotated, Union AUnion = Annotated[Union[Foo, Bar], strawberry.union(name="ABC")] """ self.assertCodemod(before, after, use_pipe_syntax=False) def test_update_swapped_kwargs(self) -> None: before = """ AUnion = strawberry.union(types=(Foo, Bar), name="ABC") """ after = """ from typing import Annotated, Union AUnion = Annotated[Union[Foo, Bar], strawberry.union(name="ABC")] """ self.assertCodemod(before, after, use_pipe_syntax=False) def test_update_union_list(self) -> None: before = """ AUnion = strawberry.union(name="ABC", types=[Foo, Bar]) """ after = """ from typing import Annotated, Union AUnion = Annotated[Union[Foo, Bar], strawberry.union(name="ABC")] """ self.assertCodemod(before, after, use_pipe_syntax=False) def test_update_positional_arguments(self) -> None: before = """ AUnion = strawberry.union("ABC", (Foo, Bar)) """ after = """ from typing import Annotated, Union AUnion = Annotated[Union[Foo, Bar], strawberry.union(name="ABC")] """ self.assertCodemod(before, after, use_pipe_syntax=False) def test_supports_directives_and_description(self) -> None: before = """ AUnion = strawberry.union( "ABC", (Foo, Bar), description="cool union", directives=[object()], ) """ after = """ from typing import Annotated, Union AUnion = Annotated[Union[Foo, Bar], strawberry.union(name="ABC", description="cool union", directives=[object()])] """ self.assertCodemod(before, after, use_pipe_syntax=False) def test_noop_with_annotated_unions(self) -> None: before = """ AUnion = Annotated[Union[Foo, Bar], strawberry.union(name="ABC")] """ after = """ AUnion = Annotated[Union[Foo, Bar], strawberry.union(name="ABC")] """ self.assertCodemod(before, after, use_pipe_syntax=False) strawberry-graphql-0.287.0/tests/codemods/test_annotated_unions_pipe.py000066400000000000000000000070171511033167500264740ustar00rootroot00000000000000from libcst.codemod import CodemodTest from strawberry.codemods.annotated_unions import ConvertUnionToAnnotatedUnion class TestConvertConstantCommand(CodemodTest): TRANSFORM = ConvertUnionToAnnotatedUnion def test_update_union(self) -> None: before = """ AUnion = strawberry.union(name="ABC", types=(Foo, Bar)) """ after = """ from typing import Annotated AUnion = Annotated[Foo | Bar, strawberry.union(name="ABC")] """ self.assertCodemod(before, after, use_pipe_syntax=True) def test_update_union_using_import(self) -> None: before = """ from strawberry import union AUnion = union(name="ABC", types=(Foo, Bar)) """ after = """ from typing import Annotated AUnion = Annotated[Foo | Bar, strawberry.union(name="ABC")] """ self.assertCodemod(before, after, use_pipe_syntax=True) def test_noop_other_union(self) -> None: before = """ from potato import union union("A", "B") """ after = """ from potato import union union("A", "B") """ self.assertCodemod(before, after, use_pipe_syntax=True) def test_update_union_positional_name(self) -> None: before = """ AUnion = strawberry.union("ABC", types=(Foo, Bar)) """ after = """ from typing import Annotated AUnion = Annotated[Foo | Bar, strawberry.union(name="ABC")] """ self.assertCodemod(before, after, use_pipe_syntax=True) def test_update_swapped_kwargs(self) -> None: before = """ AUnion = strawberry.union(types=(Foo, Bar), name="ABC") """ after = """ from typing import Annotated AUnion = Annotated[Foo | Bar, strawberry.union(name="ABC")] """ self.assertCodemod(before, after, use_pipe_syntax=True) def test_update_union_list(self) -> None: before = """ AUnion = strawberry.union(name="ABC", types=[Foo, Bar]) """ after = """ from typing import Annotated AUnion = Annotated[Foo | Bar, strawberry.union(name="ABC")] """ self.assertCodemod(before, after, use_pipe_syntax=True) def test_update_positional_arguments(self) -> None: before = """ AUnion = strawberry.union("ABC", (Foo, Bar)) """ after = """ from typing import Annotated AUnion = Annotated[Foo | Bar, strawberry.union(name="ABC")] """ self.assertCodemod(before, after, use_pipe_syntax=True) def test_supports_directives_and_description(self) -> None: before = """ AUnion = strawberry.union( "ABC", (Foo, Bar), description="cool union", directives=[object()], ) """ after = """ from typing import Annotated AUnion = Annotated[Foo | Bar, strawberry.union(name="ABC", description="cool union", directives=[object()])] """ self.assertCodemod(before, after, use_pipe_syntax=True) def test_noop_with_annotated_unions(self) -> None: before = """ AUnion = Annotated[Foo | Bar, strawberry.union(name="ABC")] """ after = """ AUnion = Annotated[Foo | Bar, strawberry.union(name="ABC")] """ self.assertCodemod(before, after, use_pipe_syntax=True) strawberry-graphql-0.287.0/tests/codemods/test_imports.py000066400000000000000000000112731511033167500236030ustar00rootroot00000000000000from libcst.codemod import CodemodTest from strawberry.codemods.update_imports import UpdateImportsCodemod class TestConvertConstantCommand(CodemodTest): TRANSFORM = UpdateImportsCodemod def test_update_field(self) -> None: before = """ from strawberry.field import something """ after = """ from strawberry.types.field import something """ self.assertCodemod(before, after) def test_update_import_strawberry_type(self) -> None: before = """ from strawberry.type import ( StrawberryContainer, StrawberryList, StrawberryOptional, StrawberryType, WithStrawberryObjectDefinition, ) """ after = """ from strawberry.types.base import ( StrawberryContainer, StrawberryList, StrawberryOptional, StrawberryType, WithStrawberryObjectDefinition, ) """ self.assertCodemod(before, after) def test_update_import_strawberry_type_object_definition(self) -> None: before = """ from strawberry.type import ( StrawberryContainer, StrawberryList, StrawberryOptional, StrawberryType, WithStrawberryObjectDefinition, get_object_definition, has_object_definition, ) """ after = """ from strawberry.types.base import ( StrawberryContainer, StrawberryList, StrawberryOptional, StrawberryType, WithStrawberryObjectDefinition) from strawberry.types import get_object_definition, has_object_definition """ self.assertCodemod(before, after) def test_update_import_strawberry_type_object_definition_only(self) -> None: before = """ from strawberry.type import get_object_definition """ after = """ from strawberry.types import get_object_definition """ self.assertCodemod(before, after) def test_update_import_union(self) -> None: before = """ from strawberry.union import StrawberryUnion """ after = """ from strawberry.types.union import StrawberryUnion """ self.assertCodemod(before, after) def test_update_import_auto(self) -> None: before = """ from strawberry.auto import auto """ after = """ from strawberry.types.auto import auto """ self.assertCodemod(before, after) def test_update_import_unset(self) -> None: before = """ from strawberry.unset import UNSET """ after = """ from strawberry.types.unset import UNSET """ self.assertCodemod(before, after) def test_update_import_arguments(self) -> None: before = """ from strawberry.arguments import StrawberryArgument """ after = """ from strawberry.types.arguments import StrawberryArgument """ self.assertCodemod(before, after) def test_update_import_lazy_type(self) -> None: before = """ from strawberry.lazy_type import LazyType """ after = """ from strawberry.types.lazy_type import LazyType """ self.assertCodemod(before, after) def test_update_import_object_type(self) -> None: before = """ from strawberry.object_type import StrawberryObjectDefinition """ after = """ from strawberry.types.object_type import StrawberryObjectDefinition """ self.assertCodemod(before, after) def test_update_import_enum(self) -> None: before = """ from strawberry.enum import StrawberryEnumDefinition """ after = """ from strawberry.types.enum import StrawberryEnumDefinition """ self.assertCodemod(before, after) def test_update_types_types(self) -> None: before = """ from strawberry.types.types import StrawberryObjectDefinition """ after = """ from strawberry.types.base import StrawberryObjectDefinition """ self.assertCodemod(before, after) def test_update_is_private(self) -> None: before = """ from strawberry.private import is_private """ after = """ from strawberry.types.private import is_private """ self.assertCodemod(before, after) strawberry-graphql-0.287.0/tests/codemods/test_maybe_optional.py000066400000000000000000000104751511033167500251130ustar00rootroot00000000000000from libcst.codemod import CodemodTest from strawberry.codemods.maybe_optional import ConvertMaybeToOptional class TestConvertMaybeToOptional(CodemodTest): TRANSFORM = ConvertMaybeToOptional def test_simple_maybe(self) -> None: before = """ from strawberry import Maybe field: Maybe[str] """ after = """ from strawberry import Maybe from typing import Union field: Maybe[Union[str, None]] """ self.assertCodemod(before, after, use_pipe_syntax=False) def test_simple_maybe_union_syntax(self) -> None: before = """ from strawberry import Maybe field: Maybe[str] """ after = """ from strawberry import Maybe from typing import Union field: Maybe[Union[str, None]] """ self.assertCodemod(before, after, use_pipe_syntax=False) def test_strawberry_maybe(self) -> None: before = """ import strawberry field: strawberry.Maybe[int] """ after = """ import strawberry from typing import Union field: strawberry.Maybe[Union[int, None]] """ self.assertCodemod(before, after, use_pipe_syntax=False) def test_nested_type(self) -> None: before = """ from strawberry import Maybe field: Maybe[List[str]] """ after = """ from strawberry import Maybe from typing import Union field: Maybe[Union[List[str], None]] """ self.assertCodemod(before, after, use_pipe_syntax=False) def test_already_has_none_pipe(self) -> None: before = """ from strawberry import Maybe field: Maybe[Union[str, None]] """ after = """ from strawberry import Maybe field: Maybe[Union[str, None]] """ self.assertCodemod(before, after, use_pipe_syntax=False) def test_already_has_none_union(self) -> None: before = """ from strawberry import Maybe from typing import Union field: Maybe[Union[str, None]] """ after = """ from strawberry import Maybe from typing import Union field: Maybe[Union[str, None]] """ self.assertCodemod(before, after, use_pipe_syntax=False) def test_multiple_maybe_fields(self) -> None: before = """ from strawberry import Maybe @strawberry.type class User: name: Maybe[str] age: Maybe[int] email: Maybe[str] """ after = """ from strawberry import Maybe from typing import Union @strawberry.type class User: name: Maybe[Union[str, None]] age: Maybe[Union[int, None]] email: Maybe[Union[str, None]] """ self.assertCodemod(before, after, use_pipe_syntax=False) def test_function_annotation(self) -> None: before = """ from strawberry import Maybe def get_user() -> Maybe[User]: return None """ after = """ from strawberry import Maybe from typing import Union def get_user() -> Maybe[Union[User, None]]: return None """ self.assertCodemod(before, after, use_pipe_syntax=False) def test_generic_type(self) -> None: before = """ from strawberry import Maybe field: Maybe[Dict[str, Any]] """ after = """ from strawberry import Maybe from typing import Union field: Maybe[Union[Dict[str, Any], None]] """ self.assertCodemod(before, after, use_pipe_syntax=False) def test_union_type_inside_maybe(self) -> None: before = """ from strawberry import Maybe from typing import Union field: Maybe[Union[str, int]] """ after = """ from strawberry import Maybe from typing import Union field: Maybe[Union[Union[str, int], None]] """ self.assertCodemod(before, after, use_pipe_syntax=False) strawberry-graphql-0.287.0/tests/conftest.py000066400000000000000000000026371511033167500211030ustar00rootroot00000000000000import pathlib import sys from typing import Any import pytest from strawberry.utils import IS_GQL_32 def pytest_emoji_xfailed(config: pytest.Config) -> tuple[str, str]: return "🤷‍♂️ ", "XFAIL 🤷‍♂️ " def pytest_emoji_skipped(config: pytest.Config) -> tuple[str, str]: return "🦘 ", "SKIPPED 🦘" pytest_plugins = ("tests.plugins.strawberry_exceptions",) @pytest.hookimpl # type: ignore def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item]): rootdir = pathlib.Path(config.rootdir) # type: ignore for item in items: rel_path = pathlib.Path(item.fspath).relative_to(rootdir) markers = [ "aiohttp", "asgi", "chalice", "channels", "django", "fastapi", "flask", "quart", "pydantic", "sanic", "litestar", ] for marker in markers: if marker in rel_path.parts: item.add_marker(getattr(pytest.mark, marker)) @pytest.hookimpl def pytest_ignore_collect( collection_path: pathlib.Path, path: Any, config: pytest.Config ): if sys.version_info < (3, 12) and "python_312" in collection_path.parts: return True return None def skip_if_gql_32(reason: str) -> pytest.MarkDecorator: return pytest.mark.skipif( IS_GQL_32, reason=reason, ) strawberry-graphql-0.287.0/tests/d.py000066400000000000000000000006101511033167500174660ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Annotated import strawberry if TYPE_CHECKING: from tests.c import C @strawberry.type class D: id: strawberry.ID @strawberry.field async def c_list( self, ) -> list[Annotated[C, strawberry.lazy("tests.c")]]: # pragma: no cover from tests.c import C return [C(id=self.id)] strawberry-graphql-0.287.0/tests/django/000077500000000000000000000000001511033167500201365ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/django/__init__.py000066400000000000000000000000001511033167500222350ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/django/app/000077500000000000000000000000001511033167500207165ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/django/app/__init__.py000066400000000000000000000000001511033167500230150ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/django/app/models.py000066400000000000000000000001671511033167500225570ustar00rootroot00000000000000from django.db import models class Example(models.Model): # noqa: DJ008 name = models.CharField(max_length=100) strawberry-graphql-0.287.0/tests/django/app/urls.py000066400000000000000000000005111511033167500222520ustar00rootroot00000000000000from django.urls import path from strawberry.django.views import GraphQLView as BaseGraphQLView from tests.views.schema import Query, schema class GraphQLView(BaseGraphQLView): def get_root_value(self, request) -> Query: return Query() urlpatterns = [ path("graphql/", GraphQLView.as_view(schema=schema)), ] strawberry-graphql-0.287.0/tests/django/conftest.py000066400000000000000000000005511511033167500223360ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING import pytest if TYPE_CHECKING: from strawberry.django.test import GraphQLTestClient @pytest.fixture def graphql_client() -> GraphQLTestClient: from django.test.client import Client from strawberry.django.test import GraphQLTestClient return GraphQLTestClient(Client()) strawberry-graphql-0.287.0/tests/django/django_settings.py000066400000000000000000000010021511033167500236630ustar00rootroot00000000000000SECRET_KEY = 1 INSTALLED_APPS = ["tests.django.app"] ROOT_URLCONF = "tests.django.app.urls" TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [], "APP_DIRS": True, } ] DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}} # This is for channels integration, but only one django settings can be used # per pytest_django settings CHANNEL_LAYERS = {"default": {"BACKEND": "channels.layers.InMemoryChannelLayer"}} strawberry-graphql-0.287.0/tests/django/test_dataloaders.py000066400000000000000000000047471511033167500240460ustar00rootroot00000000000000import json import pytest from asgiref.sync import sync_to_async from pytest_mock import MockerFixture import strawberry from strawberry.dataloader import DataLoader try: import django DJANGO_VERSION: tuple[int, int, int] = django.VERSION except ImportError: DJANGO_VERSION = (0, 0, 0) pytestmark = [ pytest.mark.asyncio, pytest.mark.skipif( DJANGO_VERSION < (3, 1), reason="Async views are only supported in Django >= 3.1", ), ] def _prepare_db(): from .app.models import Example return [ Example.objects.create(name=f"This is a demo async {index}").pk for index in range(5) ] @pytest.mark.django @pytest.mark.django_db async def test_fetch_data_from_db(mocker: MockerFixture): from django.test.client import RequestFactory from strawberry.django.views import AsyncGraphQLView from .app.models import Example def _sync_batch_load(keys: list[str]): data = Example.objects.filter(id__in=keys) return list(data) prepare_db = sync_to_async(_prepare_db) batch_load = sync_to_async(_sync_batch_load) ids = await prepare_db() async def idx(keys: list[str]) -> list[Example]: return await batch_load(keys) mock_loader = mocker.Mock(side_effect=idx) loader = DataLoader[str, Example](load_fn=mock_loader) @strawberry.type class Query: hello: str = "strawberry" @strawberry.field async def get_example(self, id: strawberry.ID) -> str: example = await loader.load(id) return example.name schema = strawberry.Schema(query=Query) query = f"""{{ a: getExample(id: "{ids[0]}") b: getExample(id: "{ids[1]}") c: getExample(id: "{ids[2]}") d: getExample(id: "{ids[3]}") e: getExample(id: "{ids[4]}") }}""" factory = RequestFactory() request = factory.post( "/graphql/", {"query": query}, content_type="application/json" ) response = await AsyncGraphQLView.as_view(schema=schema)(request) data = json.loads(response.content.decode()) assert not data.get("errors") assert data["data"] == { "a": "This is a demo async 0", "b": "This is a demo async 1", "c": "This is a demo async 2", "d": "This is a demo async 3", "e": "This is a demo async 4", } reset_db = sync_to_async(lambda: Example.objects.all().delete()) await reset_db() mock_loader.assert_called_once_with([str(id_) for id_ in ids]) strawberry-graphql-0.287.0/tests/django/test_extensions.py000066400000000000000000000002411511033167500237430ustar00rootroot00000000000000def test_extensions(graphql_client): query = "{ hello }" response = graphql_client.query(query) assert response.extensions["example"] == "example" strawberry-graphql-0.287.0/tests/enums/000077500000000000000000000000001511033167500200235ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/enums/__init__.py000066400000000000000000000000001511033167500221220ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/enums/test_enum.py000066400000000000000000000130021511033167500223740ustar00rootroot00000000000000from enum import Enum, IntEnum import pytest import strawberry from strawberry.exceptions import ObjectIsNotAnEnumError from strawberry.types.base import get_object_definition from strawberry.types.enum import StrawberryEnumDefinition def test_basic_enum(): @strawberry.enum class IceCreamFlavour(Enum): VANILLA = "vanilla" STRAWBERRY = "strawberry" CHOCOLATE = "chocolate" definition = IceCreamFlavour.__strawberry_definition__ assert definition.name == "IceCreamFlavour" assert definition.description is None assert definition.values[0].name == "VANILLA" assert definition.values[0].value == "vanilla" assert definition.values[1].name == "STRAWBERRY" assert definition.values[1].value == "strawberry" assert definition.values[2].name == "CHOCOLATE" assert definition.values[2].value == "chocolate" def test_can_pass_name_and_description(): @strawberry.enum(name="Flavour", description="example") class IceCreamFlavour(Enum): VANILLA = "vanilla" STRAWBERRY = "strawberry" CHOCOLATE = "chocolate" definition = IceCreamFlavour.__strawberry_definition__ assert definition.name == "Flavour" assert definition.description == "example" def test_can_use_enum_as_arguments(): @strawberry.enum class IceCreamFlavour(Enum): VANILLA = "vanilla" STRAWBERRY = "strawberry" CHOCOLATE = "chocolate" @strawberry.type class Query: @strawberry.field def flavour_available(self, flavour: IceCreamFlavour) -> bool: return flavour == IceCreamFlavour.STRAWBERRY field = Query.__strawberry_definition__.fields[0] assert isinstance(field.arguments[0].type, StrawberryEnumDefinition) @pytest.mark.raises_strawberry_exception( ObjectIsNotAnEnumError, match="strawberry.enum can only be used with subclasses of Enum. ", ) def test_raises_error_when_using_enum_with_a_not_enum_class(): @strawberry.enum class AClass: hello = "world" def test_can_deprecate_enum_values(): @strawberry.enum class IceCreamFlavour(Enum): VANILLA = strawberry.enum_value("vanilla") STRAWBERRY = strawberry.enum_value( "strawberry", deprecation_reason="We ran out" ) CHOCOLATE = "chocolate" definition = IceCreamFlavour.__strawberry_definition__ assert definition.values[0].name == "VANILLA" assert definition.values[0].value == "vanilla" assert definition.values[0].deprecation_reason is None assert definition.values[1].name == "STRAWBERRY" assert definition.values[1].value == "strawberry" assert definition.values[1].deprecation_reason == "We ran out" assert definition.values[2].name == "CHOCOLATE" assert definition.values[2].value == "chocolate" assert definition.values[2].deprecation_reason is None def test_can_describe_enum_values(): @strawberry.enum class IceCreamFlavour(Enum): VANILLA = strawberry.enum_value("vanilla") STRAWBERRY = strawberry.enum_value( "strawberry", description="Our favourite", ) CHOCOLATE = "chocolate" definition = IceCreamFlavour.__strawberry_definition__ assert definition.values[0].name == "VANILLA" assert definition.values[0].value == "vanilla" assert definition.values[0].description is None assert definition.values[1].name == "STRAWBERRY" assert definition.values[1].value == "strawberry" assert definition.values[1].description == "Our favourite" assert definition.values[2].name == "CHOCOLATE" assert definition.values[2].value == "chocolate" assert definition.values[2].description is None def test_can_use_enum_values(): @strawberry.enum class TestEnum(Enum): A = "A" B = strawberry.enum_value("B") C = strawberry.enum_value("Coconut", deprecation_reason="We ran out") assert TestEnum.B.value == "B" assert [x.value for x in TestEnum.__members__.values()] == [ "A", "B", "Coconut", ] def test_int_enums(): @strawberry.enum class TestEnum(IntEnum): A = 1 B = 2 C = 3 D = strawberry.enum_value(4, description="D") assert TestEnum.A.value == 1 assert TestEnum.B.value == 2 assert TestEnum.C.value == 3 assert TestEnum.D.value == 4 assert [x.value for x in TestEnum.__members__.values()] == [1, 2, 3, 4] def test_default_enum_implementation() -> None: class Foo(Enum): BAR = "bar" BAZ = "baz" @strawberry.type class Query: @strawberry.field def foo(self, foo: Foo) -> Foo: return foo schema = strawberry.Schema(Query) res = schema.execute_sync("{ foo(foo: BAR) }") assert not res.errors assert res.data assert res.data["foo"] == "BAR" def test_default_enum_reuse() -> None: class Foo(Enum): BAR = "bar" BAZ = "baz" @strawberry.type class SomeType: foo: Foo bar: Foo definition = get_object_definition(SomeType, strict=True) assert definition.fields[1].type is definition.fields[1].type def test_default_enum_with_enum_value() -> None: class Foo(Enum): BAR = "bar" BAZ = strawberry.enum_value("baz") @strawberry.type class Query: @strawberry.field def foo(self, foo: Foo) -> str: return foo.value schema = strawberry.Schema(Query) res = schema.execute_sync("{ foo(foo: BAZ) }") assert not res.errors assert res.data assert res.data["foo"] == "baz" strawberry-graphql-0.287.0/tests/exceptions/000077500000000000000000000000001511033167500210555ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/exceptions/__init__.py000066400000000000000000000000001511033167500231540ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/exceptions/classes/000077500000000000000000000000001511033167500225125ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/exceptions/classes/__init__.py000066400000000000000000000000001511033167500246110ustar00rootroot00000000000000test_exception_class_missing_optional_dependencies_error.py000066400000000000000000000036651511033167500366160ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/exceptions/classesimport pytest from strawberry.exceptions import MissingOptionalDependenciesError def test_missing_optional_dependencies_error(): with pytest.raises(MissingOptionalDependenciesError) as exc_info: raise MissingOptionalDependenciesError assert exc_info.value.message == "Some optional dependencies are missing" def test_missing_optional_dependencies_error_packages(): with pytest.raises(MissingOptionalDependenciesError) as exc_info: raise MissingOptionalDependenciesError(packages=["a", "b"]) assert ( exc_info.value.message == "Some optional dependencies are missing (hint: pip install a b)" ) def test_missing_optional_dependencies_error_empty_packages(): with pytest.raises(MissingOptionalDependenciesError) as exc_info: raise MissingOptionalDependenciesError(packages=[]) assert exc_info.value.message == "Some optional dependencies are missing" def test_missing_optional_dependencies_error_extras(): with pytest.raises(MissingOptionalDependenciesError) as exc_info: raise MissingOptionalDependenciesError(extras=["dev", "test"]) assert ( exc_info.value.message == "Some optional dependencies are missing (hint: pip install 'strawberry-graphql[dev,test]')" ) def test_missing_optional_dependencies_error_empty_extras(): with pytest.raises(MissingOptionalDependenciesError) as exc_info: raise MissingOptionalDependenciesError(extras=[]) assert exc_info.value.message == "Some optional dependencies are missing" def test_missing_optional_dependencies_error_packages_and_extras(): with pytest.raises(MissingOptionalDependenciesError) as exc_info: raise MissingOptionalDependenciesError( packages=["a", "b"], extras=["dev", "test"], ) assert ( exc_info.value.message == "Some optional dependencies are missing (hint: pip install a b 'strawberry-graphql[dev,test]')" ) strawberry-graphql-0.287.0/tests/exceptions/test_exception_handler.py000066400000000000000000000047751511033167500261760ustar00rootroot00000000000000import os import sys from strawberry.exceptions import MissingFieldAnnotationError from strawberry.exceptions.handler import ( reset_exception_handler, setup_exception_handler, strawberry_exception_handler, ) def test_exception_handler(mocker): print_mock = mocker.patch("rich.print", autospec=True) class Query: abc: int exception = MissingFieldAnnotationError("abc", Query) strawberry_exception_handler(MissingFieldAnnotationError, exception, None) assert print_mock.call_args == mocker.call(exception) def test_exception_handler_other_exceptions(mocker): print_mock = mocker.patch("rich.print", autospec=True) original_exception_mock = mocker.patch( "strawberry.exceptions.handler.sys.__excepthook__", autospec=True ) exception = ValueError("abc") strawberry_exception_handler(ValueError, exception, None) assert print_mock.called is False assert original_exception_mock.call_args == mocker.call(ValueError, exception, None) def test_exception_handler_uses_original_when_rich_is_not_installed(mocker): original_exception_mock = mocker.patch( "strawberry.exceptions.handler.sys.__excepthook__", autospec=True ) mocker.patch.dict("sys.modules", {"rich": None}) class Query: abc: int exception = MissingFieldAnnotationError("abc", Query) strawberry_exception_handler(MissingFieldAnnotationError, exception, None) assert original_exception_mock.call_args == mocker.call( MissingFieldAnnotationError, exception, None ) def test_exception_handler_uses_original_when_libcst_is_not_installed(mocker): original_exception_mock = mocker.patch( "strawberry.exceptions.handler.sys.__excepthook__", autospec=True ) mocker.patch.dict("sys.modules", {"libcst": None}) class Query: abc: int exception = MissingFieldAnnotationError("abc", Query) strawberry_exception_handler(MissingFieldAnnotationError, exception, None) assert original_exception_mock.call_args == mocker.call( MissingFieldAnnotationError, exception, None ) def test_setup_install_handler(mocker): reset_exception_handler() setup_exception_handler() assert sys.excepthook == strawberry_exception_handler def test_setup_does_not_install_handler_when_disabled_via_env(mocker): reset_exception_handler() mocker.patch.dict(os.environ, {"STRAWBERRY_DISABLE_RICH_ERRORS": "true"}) setup_exception_handler() assert sys.excepthook != strawberry_exception_handler strawberry-graphql-0.287.0/tests/exceptions/test_exception_source.py000066400000000000000000000016761511033167500260560ustar00rootroot00000000000000import sys from pathlib import Path import pytest from strawberry.exceptions.exception_source import ExceptionSource pytestmark = pytest.mark.skipif( sys.platform == "win32", reason="Test is meant to run on Unix systems" ) def test_returns_relative_path(mocker): mocker.patch.object(Path, "cwd", return_value="/home/user/project/") source = ExceptionSource( path=Path("/home/user/project/src/main.py"), code="", start_line=1, end_line=1, error_line=1, error_column=1, error_column_end=1, ) assert source.path_relative_to_cwd == Path("src/main.py") def test_returns_relative_path_when_is_already_relative(): source = ExceptionSource( path=Path("src/main.py"), code="", start_line=1, end_line=1, error_line=1, error_column=1, error_column_end=1, ) assert source.path_relative_to_cwd == Path("src/main.py") strawberry-graphql-0.287.0/tests/exceptions/test_threading_exception_handler.py000066400000000000000000000052371511033167500302150ustar00rootroot00000000000000import os import threading from strawberry.exceptions import MissingFieldAnnotationError from strawberry.exceptions.handler import ( reset_exception_handler, setup_exception_handler, strawberry_threading_exception_handler, ) def test_exception_handler(mocker): print_mock = mocker.patch("rich.print", autospec=True) class Query: abc: int exception = MissingFieldAnnotationError("abc", Query) strawberry_threading_exception_handler( (MissingFieldAnnotationError, exception, None, None) ) assert print_mock.call_args == mocker.call(exception) def test_exception_handler_other_exceptions(mocker): print_mock = mocker.patch("rich.print", autospec=True) original_exception_mock = mocker.patch( "strawberry.exceptions.handler.sys.__excepthook__", autospec=True ) exception = ValueError("abc") strawberry_threading_exception_handler((ValueError, exception, None, None)) assert print_mock.called is False assert original_exception_mock.call_args == mocker.call(ValueError, exception, None) def test_exception_handler_uses_original_when_rich_is_not_installed(mocker): original_exception_mock = mocker.patch( "strawberry.exceptions.handler.sys.__excepthook__", autospec=True ) mocker.patch.dict("sys.modules", {"rich": None}) class Query: abc: int exception = MissingFieldAnnotationError("abc", Query) strawberry_threading_exception_handler( (MissingFieldAnnotationError, exception, None, None) ) assert original_exception_mock.call_args == mocker.call( MissingFieldAnnotationError, exception, None ) def test_exception_handler_uses_original_when_libcst_is_not_installed(mocker): original_exception_mock = mocker.patch( "strawberry.exceptions.handler.sys.__excepthook__", autospec=True ) mocker.patch.dict("sys.modules", {"libcst": None}) class Query: abc: int exception = MissingFieldAnnotationError("abc", Query) strawberry_threading_exception_handler( (MissingFieldAnnotationError, exception, None, None) ) assert original_exception_mock.call_args == mocker.call( MissingFieldAnnotationError, exception, None ) def test_setup_install_handler(mocker): reset_exception_handler() setup_exception_handler() assert threading.excepthook == strawberry_threading_exception_handler def test_setup_does_not_install_handler_when_disabled_via_env(mocker): reset_exception_handler() mocker.patch.dict(os.environ, {"STRAWBERRY_DISABLE_RICH_ERRORS": "true"}) setup_exception_handler() assert threading.excepthook != strawberry_threading_exception_handler strawberry-graphql-0.287.0/tests/experimental/000077500000000000000000000000001511033167500213715ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/experimental/__init__.py000066400000000000000000000000001511033167500234700ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/experimental/pydantic/000077500000000000000000000000001511033167500232045ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/experimental/pydantic/__init__.py000066400000000000000000000000001511033167500253030ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/experimental/pydantic/schema/000077500000000000000000000000001511033167500244445ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/experimental/pydantic/schema/__init__.py000066400000000000000000000000001511033167500265430ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/experimental/pydantic/schema/test_1_and_2.py000066400000000000000000000041271511033167500272640ustar00rootroot00000000000000import sys import textwrap import pytest import strawberry from tests.experimental.pydantic.utils import needs_pydantic_v2 @pytest.mark.skipif( sys.version_info >= (3, 14), reason="Pydantic v1 is not compatible with Python 3.14+", ) @needs_pydantic_v2 def test_can_use_both_pydantic_1_and_2(): import pydantic from pydantic import v1 as pydantic_v1 class UserModel(pydantic.BaseModel): age: int name: str | None @strawberry.experimental.pydantic.type(UserModel) class User: age: strawberry.auto name: strawberry.auto class LegacyUserModel(pydantic_v1.BaseModel): age: int name: str | None int_field: pydantic.v1.NonNegativeInt = 1 @strawberry.experimental.pydantic.type(LegacyUserModel) class LegacyUser: age: strawberry.auto name: strawberry.auto int_field: strawberry.auto @strawberry.type class Query: @strawberry.field def user(self, id: strawberry.ID) -> User | LegacyUser: if id == "legacy": return LegacyUser(age=1, name="legacy") return User(age=1, name="ABC") schema = strawberry.Schema(query=Query) expected_schema = """ type LegacyUser { age: Int! name: String intField: Int! } type Query { user(id: ID!): UserLegacyUser! } type User { age: Int! name: String } union UserLegacyUser = User | LegacyUser """ assert str(schema) == textwrap.dedent(expected_schema).strip() query = """ query ($id: ID!) { user(id: $id) { __typename ... on User { name } ... on LegacyUser { name } } } """ result = schema.execute_sync(query, variable_values={"id": "new"}) assert not result.errors assert result.data == {"user": {"__typename": "User", "name": "ABC"}} result = schema.execute_sync(query, variable_values={"id": "legacy"}) assert not result.errors assert result.data == {"user": {"__typename": "LegacyUser", "name": "legacy"}} strawberry-graphql-0.287.0/tests/experimental/pydantic/schema/test_basic.py000066400000000000000000000313171511033167500271430ustar00rootroot00000000000000import textwrap from enum import Enum import pydantic import strawberry from tests.experimental.pydantic.utils import needs_pydantic_v1 def test_basic_type_field_list(): class UserModel(pydantic.BaseModel): age: int password: str | None @strawberry.experimental.pydantic.type(UserModel) class User: age: strawberry.auto password: strawberry.auto @strawberry.type class Query: @strawberry.field def user(self) -> User: return User(age=1, password="ABC") schema = strawberry.Schema(query=Query) expected_schema = """ type Query { user: User! } type User { age: Int! password: String } """ assert str(schema) == textwrap.dedent(expected_schema).strip() query = "{ user { age } }" result = schema.execute_sync(query) assert not result.errors assert result.data["user"]["age"] == 1 def test_all_fields(): class UserModel(pydantic.BaseModel): age: int password: str | None @strawberry.experimental.pydantic.type(UserModel, all_fields=True) class User: pass @strawberry.type class Query: @strawberry.field def user(self) -> User: return User(age=1, password="ABC") schema = strawberry.Schema(query=Query) expected_schema = """ type Query { user: User! } type User { age: Int! password: String } """ assert str(schema) == textwrap.dedent(expected_schema).strip() query = "{ user { age } }" result = schema.execute_sync(query) assert not result.errors assert result.data["user"]["age"] == 1 def test_auto_fields(): class UserModel(pydantic.BaseModel): age: int password: str | None other: float @strawberry.experimental.pydantic.type(UserModel) class User: age: strawberry.auto password: strawberry.auto @strawberry.type class Query: @strawberry.field def user(self) -> User: return User(age=1, password="ABC") schema = strawberry.Schema(query=Query) expected_schema = """ type Query { user: User! } type User { age: Int! password: String } """ assert str(schema) == textwrap.dedent(expected_schema).strip() query = "{ user { age } }" result = schema.execute_sync(query) assert not result.errors assert result.data["user"]["age"] == 1 def test_basic_alias_type(): class UserModel(pydantic.BaseModel): age_: int = pydantic.Field(..., alias="age") password: str | None @strawberry.experimental.pydantic.type(UserModel) class User: age_: strawberry.auto password: strawberry.auto @strawberry.type class Query: @strawberry.field def user(self) -> User: return User(age=1, password="ABC") schema = strawberry.Schema(query=Query) expected_schema = """ type Query { user: User! } type User { age: Int! password: String } """ assert str(schema) == textwrap.dedent(expected_schema).strip() def test_basic_type_with_list(): class UserModel(pydantic.BaseModel): age: int friend_names: list[str] @strawberry.experimental.pydantic.type(UserModel) class User: age: strawberry.auto friend_names: strawberry.auto @strawberry.type class Query: @strawberry.field def user(self) -> User: return User(age=1, friend_names=["A", "B"]) schema = strawberry.Schema(query=Query) query = "{ user { friendNames } }" result = schema.execute_sync(query) assert not result.errors assert result.data["user"]["friendNames"] == ["A", "B"] def test_basic_type_with_nested_model(): class Hobby(pydantic.BaseModel): name: str @strawberry.experimental.pydantic.type(Hobby) class HobbyType: name: strawberry.auto class User(pydantic.BaseModel): hobby: Hobby @strawberry.experimental.pydantic.type(User) class UserType: hobby: strawberry.auto @strawberry.type class Query: @strawberry.field def user(self) -> UserType: return UserType(hobby=HobbyType(name="Skii")) schema = strawberry.Schema(query=Query) query = "{ user { hobby { name } } }" result = schema.execute_sync(query) assert not result.errors assert result.data["user"]["hobby"]["name"] == "Skii" def test_basic_type_with_list_of_nested_model(): class Hobby(pydantic.BaseModel): name: str @strawberry.experimental.pydantic.type(Hobby) class HobbyType: name: strawberry.auto class User(pydantic.BaseModel): hobbies: list[Hobby] @strawberry.experimental.pydantic.type(User) class UserType: hobbies: strawberry.auto @strawberry.type class Query: @strawberry.field def user(self) -> UserType: return UserType( hobbies=[ HobbyType(name="Skii"), HobbyType(name="Cooking"), ] ) schema = strawberry.Schema(query=Query) query = "{ user { hobbies { name } } }" result = schema.execute_sync(query) assert not result.errors assert result.data["user"]["hobbies"] == [ {"name": "Skii"}, {"name": "Cooking"}, ] def test_basic_type_with_extended_fields(): class UserModel(pydantic.BaseModel): age: int @strawberry.experimental.pydantic.type(UserModel) class User: name: str age: strawberry.auto @strawberry.type class Query: @strawberry.field def user(self) -> User: return User(name="Marco", age=100) schema = strawberry.Schema(query=Query) expected_schema = """ type Query { user: User! } type User { name: String! age: Int! } """ assert str(schema) == textwrap.dedent(expected_schema).strip() query = "{ user { name age } }" result = schema.execute_sync(query) assert not result.errors assert result.data["user"]["name"] == "Marco" assert result.data["user"]["age"] == 100 def test_type_with_custom_resolver(): class UserModel(pydantic.BaseModel): age: int def get_age_in_months(root): return root.age * 12 @strawberry.experimental.pydantic.type(UserModel) class User: age_in_months: int = strawberry.field(resolver=get_age_in_months) age: strawberry.auto @strawberry.type class Query: @strawberry.field def user(self) -> User: return User(age=20) schema = strawberry.Schema(query=Query) query = "{ user { age ageInMonths } }" result = schema.execute_sync(query) assert not result.errors assert result.data["user"]["age"] == 20 assert result.data["user"]["ageInMonths"] == 240 def test_basic_type_with_union(): class BranchA(pydantic.BaseModel): field_a: str class BranchB(pydantic.BaseModel): field_b: int class User(pydantic.BaseModel): union_field: BranchA | BranchB @strawberry.experimental.pydantic.type(BranchA) class BranchAType: field_a: strawberry.auto @strawberry.experimental.pydantic.type(BranchB) class BranchBType: field_b: strawberry.auto @strawberry.experimental.pydantic.type(User) class UserType: union_field: strawberry.auto @strawberry.type class Query: @strawberry.field def user(self) -> UserType: return UserType(union_field=BranchBType(field_b=10)) schema = strawberry.Schema(query=Query) query = "{ user { unionField { ... on BranchBType { fieldB } } } }" result = schema.execute_sync(query) assert not result.errors assert result.data["user"]["unionField"]["fieldB"] == 10 def test_basic_type_with_union_pydantic_types(): class BranchA(pydantic.BaseModel): field_a: str class BranchB(pydantic.BaseModel): field_b: int class User(pydantic.BaseModel): union_field: BranchA | BranchB @strawberry.experimental.pydantic.type(BranchA) class BranchAType: field_a: strawberry.auto @strawberry.experimental.pydantic.type(BranchB) class BranchBType: field_b: strawberry.auto @strawberry.experimental.pydantic.type(User) class UserType: union_field: strawberry.auto @strawberry.type class Query: @strawberry.field def user(self) -> UserType: # note that BranchB is a pydantic type, not a strawberry type return UserType(union_field=BranchB(field_b=10)) schema = strawberry.Schema(query=Query) query = "{ user { unionField { ... on BranchBType { fieldB } } } }" result = schema.execute_sync(query) assert not result.errors assert result.data["user"]["unionField"]["fieldB"] == 10 def test_basic_type_with_enum(): @strawberry.enum class UserKind(Enum): user = 0 admin = 1 class User(pydantic.BaseModel): age: int kind: UserKind @strawberry.experimental.pydantic.type(User) class UserType: age: strawberry.auto kind: strawberry.auto @strawberry.type class Query: @strawberry.field def user(self) -> UserType: return UserType(age=10, kind=UserKind.admin) schema = strawberry.Schema(query=Query) query = "{ user { kind } }" result = schema.execute_sync(query) assert not result.errors assert result.data["user"]["kind"] == "admin" def test_basic_type_with_interface(): class Base(pydantic.BaseModel): base_field: str class BranchA(Base): field_a: str class BranchB(Base): field_b: int class User(pydantic.BaseModel): interface_field: Base @strawberry.experimental.pydantic.interface(Base) class BaseType: base_field: strawberry.auto @strawberry.experimental.pydantic.type(BranchA) class BranchAType(BaseType): field_a: strawberry.auto @strawberry.experimental.pydantic.type(BranchB) class BranchBType(BaseType): field_b: strawberry.auto @strawberry.experimental.pydantic.type(User) class UserType: interface_field: strawberry.auto @strawberry.type class Query: @strawberry.field def user(self) -> UserType: return UserType(interface_field=BranchBType(base_field="abc", field_b=10)) schema = strawberry.Schema(query=Query, types=[BranchAType, BranchBType]) query = "{ user { interfaceField { baseField, ... on BranchBType { fieldB } } } }" result = schema.execute_sync(query) assert not result.errors assert result.data["user"]["interfaceField"]["baseField"] == "abc" assert result.data["user"]["interfaceField"]["fieldB"] == 10 def test_basic_type_with_optional_and_default(): class UserModel(pydantic.BaseModel): age: int password: str | None = pydantic.Field(default="ABC") @strawberry.experimental.pydantic.type(UserModel, all_fields=True) class User: pass @strawberry.type class Query: @strawberry.field def user(self) -> User: return User(age=1) schema = strawberry.Schema(query=Query) expected_schema = """ type Query { user: User! } type User { age: Int! password: String } """ assert str(schema) == textwrap.dedent(expected_schema).strip() query = "{ user { age password } }" result = schema.execute_sync(query) assert not result.errors assert result.data["user"]["age"] == 1 assert result.data["user"]["password"] == "ABC" @strawberry.type class QueryNone: @strawberry.field def user(self) -> User: return User(age=1, password=None) schema = strawberry.Schema(query=QueryNone) query = "{ user { age password } }" result = schema.execute_sync(query) assert not result.errors assert result.data["user"]["age"] == 1 assert result.data["user"]["password"] is None @needs_pydantic_v1 def test_basic_type_with_constrained_list(): class FriendList(pydantic.ConstrainedList): min_items = 1 class UserModel(pydantic.BaseModel): age: int friend_names: FriendList[str] @strawberry.experimental.pydantic.type(UserModel) class User: age: strawberry.auto friend_names: strawberry.auto @strawberry.type class Query: @strawberry.field def user(self) -> User: return User(age=1, friend_names=["A", "B"]) schema = strawberry.Schema(query=Query) query = "{ user { friendNames } }" result = schema.execute_sync(query) assert not result.errors assert result.data["user"]["friendNames"] == ["A", "B"] strawberry-graphql-0.287.0/tests/experimental/pydantic/schema/test_computed.py000066400000000000000000000031221511033167500276730ustar00rootroot00000000000000import textwrap import pydantic import pytest from pydantic.version import VERSION as PYDANTIC_VERSION import strawberry IS_PYDANTIC_V2: bool = PYDANTIC_VERSION.startswith("2.") if IS_PYDANTIC_V2: from pydantic import computed_field @pytest.mark.skipif( not IS_PYDANTIC_V2, reason="Requires Pydantic v2 for computed_field" ) def test_computed_field(): class UserModel(pydantic.BaseModel): age: int @computed_field @property def next_age(self) -> int: return self.age + 1 @strawberry.experimental.pydantic.type( UserModel, all_fields=True, include_computed=True ) class User: pass @strawberry.experimental.pydantic.type(UserModel, all_fields=True) class UserNoComputed: pass @strawberry.type class Query: @strawberry.field def user(self) -> User: return User.from_pydantic(UserModel(age=1)) @strawberry.field def user_no_computed(self) -> UserNoComputed: return UserNoComputed.from_pydantic(UserModel(age=1)) schema = strawberry.Schema(query=Query) expected_schema = """ type Query { user: User! userNoComputed: UserNoComputed! } type User { age: Int! nextAge: Int! } type UserNoComputed { age: Int! } """ assert str(schema) == textwrap.dedent(expected_schema).strip() query = "{ user { age nextAge } }" result = schema.execute_sync(query) assert not result.errors assert result.data["user"]["age"] == 1 assert result.data["user"]["nextAge"] == 2 strawberry-graphql-0.287.0/tests/experimental/pydantic/schema/test_defaults.py000066400000000000000000000134111511033167500276640ustar00rootroot00000000000000import textwrap import pydantic import strawberry from strawberry.printer import print_schema from tests.conftest import skip_if_gql_32 from tests.experimental.pydantic.utils import needs_pydantic_v2 def test_field_type_default(): class User(pydantic.BaseModel): name: str = "James" nickname: str | None = "Jim" @strawberry.experimental.pydantic.type(User, all_fields=True) class PydanticUser: ... @strawberry.type class StrawberryUser: name: str = "James" @strawberry.type class Query: @strawberry.field def a(self) -> PydanticUser: return PydanticUser() @strawberry.field def b(self) -> StrawberryUser: return StrawberryUser() schema = strawberry.Schema(Query) # name should be required in both the PydanticUser and StrawberryUser expected = """ type PydanticUser { name: String! nickname: String } type Query { a: PydanticUser! b: StrawberryUser! } type StrawberryUser { name: String! } """ assert print_schema(schema) == textwrap.dedent(expected).strip() def test_pydantic_type_default_none(): class UserPydantic(pydantic.BaseModel): name: str | None = None @strawberry.experimental.pydantic.type(UserPydantic, all_fields=True) class User: ... @strawberry.type class Query: a: User = strawberry.field() schema = strawberry.Schema(Query) expected = """ type Query { a: User! } type User { name: String } """ assert print_schema(schema) == textwrap.dedent(expected).strip() def test_pydantic_type_no_default_but_optional(): class UserPydantic(pydantic.BaseModel): # pydantic automatically adds a default of None for Optional fields name: str | None @strawberry.experimental.pydantic.type(UserPydantic, all_fields=True) class User: ... @strawberry.type class Query: a: User = strawberry.field() schema = strawberry.Schema(Query) expected = """ type Query { a: User! } type User { name: String } """ assert print_schema(schema) == textwrap.dedent(expected).strip() def test_input_type_default(): class User(pydantic.BaseModel): name: str = "James" @strawberry.experimental.pydantic.type(User, all_fields=True, is_input=True) class PydanticUser: ... @strawberry.type(is_input=True) class StrawberryUser: name: str = "James" @strawberry.type class Query: @strawberry.field def a(self, user: PydanticUser) -> str: return user.name @strawberry.field def b(self, user: StrawberryUser) -> str: return user.name schema = strawberry.Schema(Query) # name should be required in both the PydanticUser and StrawberryUser expected = """ input PydanticUser { name: String! = "James" } type Query { a(user: PydanticUser!): String! b(user: StrawberryUser!): String! } input StrawberryUser { name: String! = "James" } """ assert print_schema(schema) == textwrap.dedent(expected).strip() @needs_pydantic_v2 def test_v2_explicit_default(): class User(pydantic.BaseModel): name: str | None @strawberry.experimental.pydantic.type(User, all_fields=True) class PydanticUser: ... @strawberry.type class Query: @strawberry.field def a(self) -> PydanticUser: raise NotImplementedError schema = strawberry.Schema(Query) # name should have no default expected = """ type PydanticUser { name: String } type Query { a: PydanticUser! } """ assert print_schema(schema) == textwrap.dedent(expected).strip() @skip_if_gql_32("formatting is different in gql 3.2") def test_v2_input_with_nonscalar_default(): class NonScalarType(pydantic.BaseModel): id: int = 10 nullable_field: int | None = None class Owning(pydantic.BaseModel): non_scalar_type: NonScalarType = NonScalarType() id: int = 10 @strawberry.experimental.pydantic.type( model=NonScalarType, all_fields=True, is_input=True ) class NonScalarTypeInput: ... @strawberry.experimental.pydantic.type(model=Owning, all_fields=True, is_input=True) class OwningInput: ... @strawberry.type class ExampleOutput: owning_id: int non_scalar_id: int non_scalar_nullable_field: int | None @strawberry.type class Query: @strawberry.field() def test(self, x: OwningInput) -> ExampleOutput: return ExampleOutput( owning_id=x.id, non_scalar_id=x.non_scalar_type.id, non_scalar_nullable_field=x.non_scalar_type.nullable_field, ) schema = strawberry.Schema(Query) expected = """ type ExampleOutput { owningId: Int! nonScalarId: Int! nonScalarNullableField: Int } input NonScalarTypeInput { id: Int! = 10 nullableField: Int = null } input OwningInput { nonScalarType: NonScalarTypeInput! = { id: 10 } id: Int! = 10 } type Query { test(x: OwningInput!): ExampleOutput! } """ assert print_schema(schema) == textwrap.dedent(expected).strip() query = """ query($input_data: OwningInput!) { test(x: $input_data) { owningId nonScalarId nonScalarNullableField } } """ result = schema.execute_sync( query, variable_values={"input_data": {"nonScalarType": {}}} ) assert not result.errors expected_result = { "owningId": 10, "nonScalarId": 10, "nonScalarNullableField": None, } assert result.data["test"] == expected_result strawberry-graphql-0.287.0/tests/experimental/pydantic/schema/test_federation.py000066400000000000000000000024331511033167500301770ustar00rootroot00000000000000from pydantic import BaseModel import strawberry from strawberry.federation.schema_directives import Key def test_fetch_entities_pydantic(): class ProductInDb(BaseModel): upc: str name: str # @strawberry.federation.type(keys=["upc"]) @strawberry.experimental.pydantic.type( model=ProductInDb, directives=[Key(fields="upc", resolvable=True)] ) class Product: upc: str name: str @classmethod def resolve_reference(cls, upc) -> "Product": return Product(upc=upc, name="") @strawberry.federation.type(extend=True) class Query: @strawberry.field def top_products(self, first: int) -> list[Product]: # pragma: no cover return [] schema = strawberry.federation.Schema(query=Query) query = """ query ($representations: [_Any!]!) { _entities(representations: $representations) { ... on Product { upc } } } """ result = schema.execute_sync( query, variable_values={ "representations": [{"__typename": "Product", "upc": "B00005N5PF"}] }, ) assert not result.errors assert result.data == {"_entities": [{"upc": "B00005N5PF"}]} strawberry-graphql-0.287.0/tests/experimental/pydantic/schema/test_forward_reference.py000066400000000000000000000016221511033167500315400ustar00rootroot00000000000000from __future__ import annotations import textwrap import pydantic import strawberry def test_auto_fields(): global User class UserModel(pydantic.BaseModel): age: int password: str | None other: float @strawberry.experimental.pydantic.type(UserModel) class User: age: strawberry.auto password: strawberry.auto @strawberry.type class Query: @strawberry.field def user(self) -> User: return User(age=1, password="ABC") schema = strawberry.Schema(query=Query) expected_schema = """ type Query { user: User! } type User { age: Int! password: String } """ assert str(schema) == textwrap.dedent(expected_schema).strip() query = "{ user { age } }" result = schema.execute_sync(query) assert not result.errors assert result.data["user"]["age"] == 1 strawberry-graphql-0.287.0/tests/experimental/pydantic/schema/test_mutation.py000066400000000000000000000135731511033167500277260ustar00rootroot00000000000000import pydantic import strawberry from strawberry.experimental.pydantic._compat import IS_PYDANTIC_V2 def test_mutation(): class User(pydantic.BaseModel): name: pydantic.constr(min_length=2) @strawberry.experimental.pydantic.input(User) class CreateUserInput: name: strawberry.auto @strawberry.experimental.pydantic.type(User) class UserType: name: strawberry.auto @strawberry.type class Query: h: str @strawberry.type class Mutation: @strawberry.mutation def create_user(self, input: CreateUserInput) -> UserType: return UserType(name=input.name) schema = strawberry.Schema(query=Query, mutation=Mutation) query = """ mutation { createUser(input: { name: "Patrick" }) { name } } """ result = schema.execute_sync(query) assert not result.errors assert result.data["createUser"]["name"] == "Patrick" def test_mutation_with_validation(): class User(pydantic.BaseModel): name: pydantic.constr(min_length=2) @strawberry.experimental.pydantic.input(User) class CreateUserInput: name: strawberry.auto @strawberry.experimental.pydantic.type(User) class UserType: name: strawberry.auto @strawberry.type class Query: h: str @strawberry.type class Mutation: @strawberry.mutation def create_user(self, input: CreateUserInput) -> UserType: data = input.to_pydantic() return UserType(name=data.name) schema = strawberry.Schema(query=Query, mutation=Mutation) query = """ mutation { createUser(input: { name: "P" }) { name } } """ result = schema.execute_sync(query) if IS_PYDANTIC_V2: assert result.errors[0].message.startswith( "1 validation error for User\n" "name\n" " String should have at least 2 characters [type=string_too_short, " "input_value='P', input_type=str]\n" ) else: assert result.errors[0].message == ( "1 validation error for User\nname\n ensure this value has at " "least 2 characters (type=value_error.any_str.min_length; limit_value=2)" ) def test_mutation_with_validation_of_nested_model(): class HobbyInputModel(pydantic.BaseModel): name: pydantic.constr(min_length=2) class CreateUserModel(pydantic.BaseModel): hobby: HobbyInputModel @strawberry.experimental.pydantic.input(HobbyInputModel) class HobbyInput: name: strawberry.auto @strawberry.experimental.pydantic.input(CreateUserModel) class CreateUserInput: hobby: strawberry.auto class UserModel(pydantic.BaseModel): name: str @strawberry.experimental.pydantic.type(UserModel) class UserType: name: strawberry.auto @strawberry.type class Query: h: str @strawberry.type class Mutation: @strawberry.mutation def create_user(self, input: CreateUserInput) -> UserType: data = input.to_pydantic() return UserType(name=data.name) schema = strawberry.Schema(query=Query, mutation=Mutation) query = """ mutation { createUser(input: { hobby: { name: "P" } }) { name } } """ result = schema.execute_sync(query) if IS_PYDANTIC_V2: assert result.errors[0].message.startswith( "1 validation error for HobbyInputModel\n" "name\n" " String should have at least 2 characters [type=string_too_short, " "input_value='P', input_type=str]\n" ) else: assert result.errors[0].message == ( "1 validation error for HobbyInputModel\nname\n" " ensure this value has at least 2 characters " "(type=value_error.any_str.min_length; limit_value=2)" ) def test_mutation_with_validation_and_error_type(): class User(pydantic.BaseModel): name: pydantic.constr(min_length=2) @strawberry.experimental.pydantic.input(User) class CreateUserInput: name: strawberry.auto @strawberry.experimental.pydantic.type(User) class UserType: name: strawberry.auto @strawberry.experimental.pydantic.error_type(User) class UserError: name: strawberry.auto @strawberry.type class Query: h: str @strawberry.type class Mutation: @strawberry.mutation def create_user(self, input: CreateUserInput) -> UserType | UserError: try: data = input.to_pydantic() except pydantic.ValidationError as e: args: dict[str, list[str]] = {} for error in e.errors(): field = error["loc"][0] # currently doesn't support nested errors field_errors = args.get(field, []) field_errors.append(error["msg"]) args[field] = field_errors return UserError(**args) else: return UserType(name=data.name) schema = strawberry.Schema(query=Query, mutation=Mutation) query = """ mutation { createUser(input: { name: "P" }) { ... on UserType { name } ... on UserError { nameErrors: name } } } """ result = schema.execute_sync(query) assert result.errors is None assert result.data["createUser"].get("name") is None if IS_PYDANTIC_V2: assert result.data["createUser"]["nameErrors"] == [ ("String should have at least 2 characters") ] else: assert result.data["createUser"]["nameErrors"] == [ ("ensure this value has at least 2 characters") ] strawberry-graphql-0.287.0/tests/experimental/pydantic/test_basic.py000066400000000000000000000644301511033167500257050ustar00rootroot00000000000000import dataclasses from enum import Enum from typing import Annotated, Any import pydantic import pytest import strawberry from strawberry.experimental.pydantic.exceptions import MissingFieldsListError from strawberry.schema_directive import Location from strawberry.types.base import ( StrawberryList, StrawberryObjectDefinition, StrawberryOptional, ) from strawberry.types.enum import StrawberryEnumDefinition from strawberry.types.union import StrawberryUnion def test_basic_type_field_list(): class User(pydantic.BaseModel): age: int password: str | None with pytest.deprecated_call(): @strawberry.experimental.pydantic.type(User, fields=["age", "password"]) class UserType: pass definition: StrawberryObjectDefinition = UserType.__strawberry_definition__ assert definition.name == "UserType" [field1, field2] = definition.fields assert field1.python_name == "age" assert field1.graphql_name is None assert field1.type is int assert field2.python_name == "password" assert field2.graphql_name is None assert isinstance(field2.type, StrawberryOptional) assert field2.type.of_type is str def test_basic_type_all_fields(): class User(pydantic.BaseModel): age: int password: str | None @strawberry.experimental.pydantic.type(User, all_fields=True) class UserType: pass definition: StrawberryObjectDefinition = UserType.__strawberry_definition__ assert definition.name == "UserType" [field1, field2] = definition.fields assert field1.python_name == "age" assert field1.graphql_name is None assert field1.type is int assert field2.python_name == "password" assert field2.graphql_name is None assert isinstance(field2.type, StrawberryOptional) assert field2.type.of_type is str @pytest.mark.filterwarnings("error") def test_basic_type_all_fields_warn(): class User(pydantic.BaseModel): age: int password: str | None with pytest.raises( UserWarning, match="Using all_fields overrides any explicitly defined fields", ): @strawberry.experimental.pydantic.type(User, all_fields=True) class UserType: age: strawberry.auto def test_basic_type_auto_fields(): class User(pydantic.BaseModel): age: int password: str | None other: float @strawberry.experimental.pydantic.type(User) class UserType: age: strawberry.auto password: strawberry.auto definition: StrawberryObjectDefinition = UserType.__strawberry_definition__ assert definition.name == "UserType" [field1, field2] = definition.fields assert field1.python_name == "age" assert field1.graphql_name is None assert field1.type is int assert field2.python_name == "password" assert field2.graphql_name is None assert isinstance(field2.type, StrawberryOptional) assert field2.type.of_type is str def test_auto_fields_other_sentinel(): class OtherSentinel: pass class User(pydantic.BaseModel): age: int password: str | None other: int @strawberry.experimental.pydantic.type(User) class UserType: age: strawberry.auto password: strawberry.auto other: OtherSentinel # this should be a private field, not an auto field definition: StrawberryObjectDefinition = UserType.__strawberry_definition__ assert definition.name == "UserType" [field1, field2, field3] = definition.fields assert field1.python_name == "age" assert field1.graphql_name is None assert field1.type is int assert field2.python_name == "password" assert field2.graphql_name is None assert isinstance(field2.type, StrawberryOptional) assert field2.type.of_type is str assert field3.python_name == "other" assert field3.graphql_name is None assert field3.type is OtherSentinel def test_referencing_other_models_fails_when_not_registered(): class Group(pydantic.BaseModel): name: str class User(pydantic.BaseModel): age: int password: str | None group: Group with pytest.raises( strawberry.experimental.pydantic.UnregisteredTypeException, match=r"Cannot find a Strawberry Type for (.*) did you forget to register it?", ): @strawberry.experimental.pydantic.type(User) class UserType: age: strawberry.auto password: strawberry.auto group: strawberry.auto def test_referencing_other_input_models_fails_when_not_registered(): class Group(pydantic.BaseModel): name: str class User(pydantic.BaseModel): age: int password: str | None group: Group @strawberry.experimental.pydantic.type(Group) class GroupType: name: strawberry.auto with pytest.raises( strawberry.experimental.pydantic.UnregisteredTypeException, match=r"Cannot find a Strawberry Type for (.*) did you forget to register it?", ): @strawberry.experimental.pydantic.input(User) class UserInputType: age: strawberry.auto password: strawberry.auto group: strawberry.auto def test_referencing_other_registered_models(): class Group(pydantic.BaseModel): name: str class User(pydantic.BaseModel): age: int group: Group @strawberry.experimental.pydantic.type(Group) class GroupType: name: strawberry.auto @strawberry.experimental.pydantic.type(User) class UserType: age: strawberry.auto group: strawberry.auto definition: StrawberryObjectDefinition = UserType.__strawberry_definition__ assert definition.name == "UserType" [field1, field2] = definition.fields assert field1.python_name == "age" assert field1.type is int assert field2.python_name == "group" assert field2.type is GroupType def test_list(): class User(pydantic.BaseModel): friend_names: list[str] @strawberry.experimental.pydantic.type(User) class UserType: friend_names: strawberry.auto definition: StrawberryObjectDefinition = UserType.__strawberry_definition__ assert definition.name == "UserType" [field] = definition.fields assert field.python_name == "friend_names" assert isinstance(field.type, StrawberryList) assert field.type.of_type is str def test_list_of_types(): class Friend(pydantic.BaseModel): name: str class User(pydantic.BaseModel): friends: list[Friend | None] | None @strawberry.experimental.pydantic.type(Friend) class FriendType: name: strawberry.auto @strawberry.experimental.pydantic.type(User) class UserType: friends: strawberry.auto definition: StrawberryObjectDefinition = UserType.__strawberry_definition__ assert definition.name == "UserType" [field] = definition.fields assert field.python_name == "friends" assert isinstance(field.type, StrawberryOptional) assert isinstance(field.type.of_type, StrawberryList) assert isinstance(field.type.of_type.of_type, StrawberryOptional) assert field.type.of_type.of_type.of_type is FriendType def test_basic_type_without_fields_throws_an_error(): class User(pydantic.BaseModel): age: int password: str | None with pytest.raises(MissingFieldsListError): @strawberry.experimental.pydantic.type(User) class UserType: pass def test_type_with_fields_coming_from_strawberry_and_pydantic(): class User(pydantic.BaseModel): age: int password: str | None @strawberry.experimental.pydantic.type(User) class UserType: name: str age: strawberry.auto password: strawberry.auto definition: StrawberryObjectDefinition = UserType.__strawberry_definition__ assert definition.name == "UserType" [field1, field2, field3] = definition.fields assert field1.python_name == "name" assert field1.type is str assert field2.python_name == "age" assert field2.type is int assert field3.python_name == "password" assert isinstance(field3.type, StrawberryOptional) assert field3.type.of_type is str def test_default_and_default_factory(): class User1(pydantic.BaseModel): friend: str | None = "friend_value" @strawberry.experimental.pydantic.type(User1) class UserType1: friend: strawberry.auto assert UserType1().friend == "friend_value" assert UserType1().to_pydantic().friend == "friend_value" class User2(pydantic.BaseModel): friend: str | None = None @strawberry.experimental.pydantic.type(User2) class UserType2: friend: strawberry.auto assert UserType2().friend is None assert UserType2().to_pydantic().friend is None # Test instantiation using default_factory class User3(pydantic.BaseModel): friend: str | None = pydantic.Field(default_factory=lambda: "friend_value") @strawberry.experimental.pydantic.type(User3) class UserType3: friend: strawberry.auto assert UserType3().friend == "friend_value" assert UserType3().to_pydantic().friend == "friend_value" class User4(pydantic.BaseModel): friend: str | None = pydantic.Field(default_factory=lambda: None) @strawberry.experimental.pydantic.type(User4) class UserType4: friend: strawberry.auto assert UserType4().friend is None assert UserType4().to_pydantic().friend is None def test_optional_and_default(): class UserModel(pydantic.BaseModel): age: int name: str = pydantic.Field("Michael", description="The user name") password: str | None = pydantic.Field(default="ABC") passwordtwo: str | None = None some_list: list[str] | None = pydantic.Field(default_factory=list) check: bool | None = False @strawberry.experimental.pydantic.type(UserModel, all_fields=True) class User: pass definition: StrawberryObjectDefinition = User.__strawberry_definition__ assert definition.name == "User" [ age_field, name_field, password_field, passwordtwo_field, some_list_field, check_field, ] = definition.fields assert age_field.python_name == "age" assert age_field.type is int assert name_field.python_name == "name" assert name_field.type is str assert password_field.python_name == "password" assert isinstance(password_field.type, StrawberryOptional) assert password_field.type.of_type is str assert passwordtwo_field.python_name == "passwordtwo" assert isinstance(passwordtwo_field.type, StrawberryOptional) assert passwordtwo_field.type.of_type is str assert some_list_field.python_name == "some_list" assert isinstance(some_list_field.type, StrawberryOptional) assert isinstance(some_list_field.type.of_type, StrawberryList) assert some_list_field.type.of_type.of_type is str assert check_field.python_name == "check" assert isinstance(check_field.type, StrawberryOptional) assert check_field.type.of_type is bool def test_type_with_fields_mutable_default(): empty_list = [] class User(pydantic.BaseModel): groups: list[str] friends: list[str] = empty_list @strawberry.experimental.pydantic.type(User) class UserType: groups: strawberry.auto friends: strawberry.auto definition: StrawberryObjectDefinition = UserType.__strawberry_definition__ assert definition.name == "UserType" [groups_field, friends_field] = definition.fields assert groups_field.default is dataclasses.MISSING assert groups_field.default_factory is dataclasses.MISSING assert friends_field.default is dataclasses.MISSING # check that we really made a copy assert friends_field.default_factory() is not empty_list assert UserType(groups=["groups"]).friends is not empty_list UserType(groups=["groups"]).friends.append("joe") assert empty_list == [] @pytest.mark.xfail( reason=( "passing default values when extending types from pydantic is not" "supported. https://github.com/strawberry-graphql/strawberry/issues/829" ) ) def test_type_with_fields_coming_from_strawberry_and_pydantic_with_default(): class User(pydantic.BaseModel): age: int password: str | None @strawberry.experimental.pydantic.type(User) class UserType: name: str = "Michael" age: strawberry.auto password: strawberry.auto definition: StrawberryObjectDefinition = UserType.__strawberry_definition__ assert definition.name == "UserType" [field1, field2, field3] = definition.fields assert field1.python_name == "age" assert field1.type is int assert field2.python_name == "password" assert isinstance(field2.type, StrawberryOptional) assert field2.type.of_type is str assert field3.python_name == "name" assert field3.type is str assert field3.default == "Michael" def test_type_with_nested_fields_coming_from_strawberry_and_pydantic(): @strawberry.type class Name: first_name: str last_name: str class User(pydantic.BaseModel): age: int password: str | None @strawberry.experimental.pydantic.type(User) class UserType: name: Name age: strawberry.auto password: strawberry.auto definition: StrawberryObjectDefinition = UserType.__strawberry_definition__ assert definition.name == "UserType" [field1, field2, field3] = definition.fields assert field1.python_name == "name" assert field1.type is Name assert field2.python_name == "age" assert field2.type is int assert field3.python_name == "password" assert isinstance(field3.type, StrawberryOptional) assert field3.type.of_type is str def test_type_with_aliased_pydantic_field(): class UserModel(pydantic.BaseModel): age_: int = pydantic.Field(..., alias="age") password: str | None @strawberry.experimental.pydantic.type(UserModel) class User: age_: strawberry.auto password: strawberry.auto definition: StrawberryObjectDefinition = User.__strawberry_definition__ assert definition.name == "User" [field1, field2] = definition.fields assert field1.python_name == "age_" assert field1.type is int assert field1.graphql_name == "age" assert field2.python_name == "password" assert isinstance(field2.type, StrawberryOptional) assert field2.type.of_type is str def test_union(): class BranchA(pydantic.BaseModel): field_a: str class BranchB(pydantic.BaseModel): field_b: int class User(pydantic.BaseModel): age: int union_field: BranchA | BranchB @strawberry.experimental.pydantic.type(BranchA) class BranchAType: field_a: strawberry.auto @strawberry.experimental.pydantic.type(BranchB) class BranchBType: field_b: strawberry.auto @strawberry.experimental.pydantic.type(User) class UserType: age: strawberry.auto union_field: strawberry.auto definition: StrawberryObjectDefinition = UserType.__strawberry_definition__ assert definition.name == "UserType" [field1, field2] = definition.fields assert field1.python_name == "age" assert field1.type is int assert field2.python_name == "union_field" assert isinstance(field2.type, StrawberryUnion) assert field2.type.types[0] is BranchAType assert field2.type.types[1] is BranchBType def test_enum(): @strawberry.enum class UserKind(Enum): user = 0 admin = 1 class User(pydantic.BaseModel): age: int kind: UserKind @strawberry.experimental.pydantic.type(User) class UserType: age: strawberry.auto kind: strawberry.auto definition: StrawberryObjectDefinition = UserType.__strawberry_definition__ assert definition.name == "UserType" [field1, field2] = definition.fields assert field1.python_name == "age" assert field1.type is int assert field2.python_name == "kind" assert isinstance(field2.type, StrawberryEnumDefinition) assert field2.type.wrapped_cls is UserKind def test_interface(): class Base(pydantic.BaseModel): base_field: str class BranchA(Base): field_a: str class BranchB(Base): field_b: int class User(pydantic.BaseModel): age: int interface_field: Base @strawberry.experimental.pydantic.interface(Base) class BaseType: base_field: strawberry.auto @strawberry.experimental.pydantic.type(BranchA) class BranchAType(BaseType): field_a: strawberry.auto @strawberry.experimental.pydantic.type(BranchB) class BranchBType(BaseType): field_b: strawberry.auto @strawberry.experimental.pydantic.type(User) class UserType: age: strawberry.auto interface_field: strawberry.auto definition: StrawberryObjectDefinition = UserType.__strawberry_definition__ assert definition.name == "UserType" [field1, field2] = definition.fields assert field1.python_name == "age" assert field1.type is int assert field2.python_name == "interface_field" assert field2.type is BaseType def test_both_output_and_input_type(): class Work(pydantic.BaseModel): time: float class User(pydantic.BaseModel): name: str # Note that pydantic v2 requires an explicit default of None for Optionals work: Work | None = None class Group(pydantic.BaseModel): users: list[User] # Test both definition orders @strawberry.experimental.pydantic.input(Work) class WorkInput: time: strawberry.auto @strawberry.experimental.pydantic.type(Work) class WorkOutput: time: strawberry.auto @strawberry.experimental.pydantic.type(User) class UserOutput: name: strawberry.auto work: strawberry.auto @strawberry.experimental.pydantic.input(User) class UserInput: name: strawberry.auto work: strawberry.auto @strawberry.experimental.pydantic.input(Group) class GroupInput: users: strawberry.auto @strawberry.experimental.pydantic.type(Group) class GroupOutput: users: strawberry.auto @strawberry.type class Query: groups: list[GroupOutput] @strawberry.type class Mutation: @strawberry.mutation def update_group(group: GroupInput) -> GroupOutput: pass # This triggers the exception from #1504 schema = strawberry.Schema(query=Query, mutation=Mutation) expected_schema = """ input GroupInput { users: [UserInput!]! } type GroupOutput { users: [UserOutput!]! } type Mutation { updateGroup(group: GroupInput!): GroupOutput! } type Query { groups: [GroupOutput!]! } input UserInput { name: String! work: WorkInput = null } type UserOutput { name: String! work: WorkOutput } input WorkInput { time: Float! } type WorkOutput { time: Float! }""" assert schema.as_str().strip() == expected_schema.strip() assert Group._strawberry_type == GroupOutput assert Group._strawberry_input_type == GroupInput assert User._strawberry_type == UserOutput assert User._strawberry_input_type == UserInput assert Work._strawberry_type == WorkOutput assert Work._strawberry_input_type == WorkInput def test_single_field_changed_type(): class User(pydantic.BaseModel): age: int @strawberry.experimental.pydantic.type(User) class UserType: age: str definition: StrawberryObjectDefinition = UserType.__strawberry_definition__ assert definition.name == "UserType" [field1] = definition.fields assert field1.python_name == "age" assert field1.graphql_name is None assert field1.type is str def test_type_with_aliased_pydantic_field_changed_type(): class UserModel(pydantic.BaseModel): age_: int = pydantic.Field(..., alias="age") password: str | None @strawberry.experimental.pydantic.type(UserModel) class User: age_: str password: strawberry.auto definition: StrawberryObjectDefinition = User.__strawberry_definition__ assert definition.name == "User" [field1, field2] = definition.fields assert field1.python_name == "age_" assert field1.type is str assert field1.graphql_name == "age" assert field2.python_name == "password" assert isinstance(field2.type, StrawberryOptional) assert field2.type.of_type is str def test_deprecated_fields(): class User(pydantic.BaseModel): age: int password: str | None other: float @strawberry.experimental.pydantic.type(User) class UserType: age: strawberry.auto = strawberry.field(deprecation_reason="Because") password: strawberry.auto definition: StrawberryObjectDefinition = UserType.__strawberry_definition__ assert definition.name == "UserType" [field1, field2] = definition.fields assert field1.python_name == "age" assert field1.graphql_name is None assert field1.type is int assert field1.deprecation_reason == "Because" assert field2.python_name == "password" assert field2.graphql_name is None assert isinstance(field2.type, StrawberryOptional) assert field2.type.of_type is str def test_permission_classes(): class IsAuthenticated(strawberry.BasePermission): message = "User is not authenticated" def has_permission( self, source: Any, info: strawberry.types.Info, **kwargs: Any ) -> bool: return False class User(pydantic.BaseModel): age: int password: str | None other: float @strawberry.experimental.pydantic.type(User) class UserType: age: strawberry.auto = strawberry.field(permission_classes=[IsAuthenticated]) password: strawberry.auto definition: StrawberryObjectDefinition = UserType.__strawberry_definition__ assert definition.name == "UserType" [field1, field2] = definition.fields assert field1.python_name == "age" assert field1.graphql_name is None assert field1.type is int assert field1.permission_classes == [IsAuthenticated] assert field2.python_name == "password" assert field2.graphql_name is None assert isinstance(field2.type, StrawberryOptional) assert field2.type.of_type is str def test_field_directives(): @strawberry.schema_directive(locations=[Location.FIELD_DEFINITION]) class Sensitive: reason: str class User(pydantic.BaseModel): age: int password: str | None other: float @strawberry.experimental.pydantic.type(User) class UserType: age: strawberry.auto = strawberry.field(directives=[Sensitive(reason="GDPR")]) password: strawberry.auto definition: StrawberryObjectDefinition = UserType.__strawberry_definition__ assert definition.name == "UserType" [field1, field2] = definition.fields assert field1.python_name == "age" assert field1.graphql_name is None assert field1.type is int assert field1.directives == [Sensitive(reason="GDPR")] assert field2.python_name == "password" assert field2.graphql_name is None assert isinstance(field2.type, StrawberryOptional) assert field2.type.of_type is str def test_alias_fields(): class User(pydantic.BaseModel): age: int @strawberry.experimental.pydantic.type(User) class UserType: age: strawberry.auto = strawberry.field(name="ageAlias") definition: StrawberryObjectDefinition = UserType.__strawberry_definition__ assert definition.name == "UserType" field1 = definition.fields[0] assert field1.python_name == "age" assert field1.graphql_name == "ageAlias" assert field1.type is int def test_alias_fields_with_use_pydantic_alias(): class User(pydantic.BaseModel): age: int state: str = pydantic.Field(alias="statePydantic") country: str = pydantic.Field(alias="countryPydantic") @strawberry.experimental.pydantic.type(User, use_pydantic_alias=True) class UserType: age: strawberry.auto = strawberry.field(name="ageAlias") state: strawberry.auto = strawberry.field(name="state") country: strawberry.auto definition: StrawberryObjectDefinition = UserType.__strawberry_definition__ assert definition.name == "UserType" [field1, field2, field3] = definition.fields assert field1.python_name == "age" assert field1.graphql_name == "ageAlias" assert field2.python_name == "state" assert field2.graphql_name == "state" assert field3.python_name == "country" assert field3.graphql_name == "countryPydantic" def test_field_metadata(): class User(pydantic.BaseModel): private: bool public: bool @strawberry.experimental.pydantic.type(User) class UserType: private: strawberry.auto = strawberry.field(metadata={"admin_only": True}) public: strawberry.auto definition: StrawberryObjectDefinition = UserType.__strawberry_definition__ assert definition.name == "UserType" [field1, field2] = definition.fields assert field1.python_name == "private" assert field1.metadata["admin_only"] assert field2.python_name == "public" assert not field2.metadata def test_annotated(): class User(pydantic.BaseModel): a: Annotated[int, "metadata"] @strawberry.experimental.pydantic.input(User, all_fields=True) class UserType: pass definition: StrawberryObjectDefinition = UserType.__strawberry_definition__ assert definition.name == "UserType" [field] = definition.fields assert field.python_name == "a" assert field.type is int def test_nested_annotated(): class User(pydantic.BaseModel): a: Annotated[int, "metadata"] | None b: list[Annotated[int, "metadata"]] | None @strawberry.experimental.pydantic.input(User, all_fields=True) class UserType: pass definition: StrawberryObjectDefinition = UserType.__strawberry_definition__ assert definition.name == "UserType" [field_a, field_b] = definition.fields assert field_a.python_name == "a" assert isinstance(field_a.type, StrawberryOptional) assert field_a.type.of_type is int assert field_b.python_name == "b" assert isinstance(field_b.type, StrawberryOptional) assert isinstance(field_b.type.of_type, StrawberryList) assert field_b.type.of_type.of_type is int strawberry-graphql-0.287.0/tests/experimental/pydantic/test_conversion.py000066400000000000000000001035371511033167500270130ustar00rootroot00000000000000import base64 import dataclasses import re from enum import Enum from typing import Any, NewType, TypeVar import pytest from pydantic import BaseModel, Field, ValidationError import strawberry from strawberry.experimental.pydantic._compat import ( IS_PYDANTIC_V2, CompatModelField, PydanticCompat, ) from strawberry.experimental.pydantic.exceptions import ( AutoFieldsNotInBaseModelError, BothDefaultAndDefaultFactoryDefinedError, ) from strawberry.experimental.pydantic.utils import get_default_factory_for_field from strawberry.types.base import ( StrawberryList, StrawberryObjectDefinition, StrawberryOptional, ) from tests.experimental.pydantic.utils import needs_pydantic_v1 if IS_PYDANTIC_V2: from pydantic import computed_field def test_can_use_type_standalone(): class User(BaseModel): age: int password: str | None @strawberry.experimental.pydantic.type(User) class UserType: age: strawberry.auto password: strawberry.auto user = UserType(age=1, password="abc") assert user.age == 1 assert user.password == "abc" def test_can_convert_pydantic_type_to_strawberry(): class User(BaseModel): age: int password: str | None @strawberry.experimental.pydantic.type(User) class UserType: age: strawberry.auto password: strawberry.auto origin_user = User(age=1, password="abc") user = UserType.from_pydantic(origin_user) assert user.age == 1 assert user.password == "abc" def test_cannot_convert_pydantic_type_to_strawberry_missing_field(): class User(BaseModel): age: int with pytest.raises( AutoFieldsNotInBaseModelError, match=re.escape( "UserType defines ['password'] with strawberry.auto." " Field(s) not present in User BaseModel." ), ): @strawberry.experimental.pydantic.type(User) class UserType: age: strawberry.auto password: strawberry.auto def test_cannot_convert_pydantic_type_to_strawberry_property_auto(): # auto inferring type of a property is not supported class User(BaseModel): age: int @property def password(self) -> str: return "hunter2" with pytest.raises( AutoFieldsNotInBaseModelError, match=re.escape( "UserType defines ['password'] with strawberry.auto." " Field(s) not present in User BaseModel." ), ): @strawberry.experimental.pydantic.type(User) class UserType: age: strawberry.auto password: strawberry.auto def test_can_convert_pydantic_type_to_strawberry_property(): class User(BaseModel): age: int @property def password(self) -> str: return "hunter2" @strawberry.experimental.pydantic.type(User) class UserType: age: strawberry.auto password: str origin_user = User(age=1) user = UserType.from_pydantic(origin_user) assert user.age == 1 assert user.password == "hunter2" def test_can_convert_alias_pydantic_field_to_strawberry(): class UserModel(BaseModel): age_: int = Field(..., alias="age") password: str | None @strawberry.experimental.pydantic.type(UserModel) class User: age_: strawberry.auto password: strawberry.auto origin_user = UserModel(age=1, password="abc") user = User.from_pydantic(origin_user) assert user.age_ == 1 assert user.password == "abc" def test_convert_alias_name(): class UserModel(BaseModel): age_: int = Field(..., alias="age") password: str | None @strawberry.experimental.pydantic.type( UserModel, all_fields=True, use_pydantic_alias=True ) class User: ... origin_user = UserModel(age=1, password="abc") user = User.from_pydantic(origin_user) assert user.age_ == 1 definition = User.__strawberry_definition__ assert definition.fields[0].graphql_name == "age" def test_do_not_convert_alias_name(): class UserModel(BaseModel): age_: int = Field(..., alias="age") password: str | None @strawberry.experimental.pydantic.type( UserModel, all_fields=True, use_pydantic_alias=False ) class User: ... origin_user = UserModel(age=1, password="abc") user = User.from_pydantic(origin_user) assert user.age_ == 1 definition = User.__strawberry_definition__ assert definition.fields[0].graphql_name is None def test_can_pass_pydantic_field_description_to_strawberry(): class UserModel(BaseModel): age: int password: str | None = Field(..., description="NOT 'password'.") @strawberry.experimental.pydantic.type(UserModel) class User: age: strawberry.auto password: strawberry.auto definition = User.__strawberry_definition__ assert definition.fields[0].python_name == "age" assert definition.fields[0].description is None assert definition.fields[1].python_name == "password" assert definition.fields[1].description == "NOT 'password'." def test_can_convert_falsy_values_to_strawberry(): class UserModel(BaseModel): age: int password: str @strawberry.experimental.pydantic.type(UserModel) class User: age: strawberry.auto password: strawberry.auto origin_user = UserModel(age=0, password="") user = User.from_pydantic(origin_user) assert user.age == 0 assert not user.password def test_can_convert_pydantic_type_to_strawberry_with_private_field(): class UserModel(BaseModel): age: int @strawberry.experimental.pydantic.type(model=UserModel) class User: age: strawberry.auto password: strawberry.Private[str] user = User(age=30, password="qwerty") assert user.age == 30 assert user.password == "qwerty" definition = User.__strawberry_definition__ assert len(definition.fields) == 1 assert definition.fields[0].python_name == "age" assert definition.fields[0].graphql_name is None assert definition.fields[0].type is int def test_can_convert_pydantic_type_with_nested_data_to_strawberry(): class WorkModel(BaseModel): name: str @strawberry.experimental.pydantic.type(WorkModel) class Work: name: strawberry.auto class UserModel(BaseModel): work: WorkModel @strawberry.experimental.pydantic.type(UserModel) class User: work: strawberry.auto origin_user = UserModel(work=WorkModel(name="Ice Cream inc")) user = User.from_pydantic(origin_user) assert user.work.name == "Ice Cream inc" def test_can_convert_pydantic_type_with_list_of_nested_data_to_strawberry(): class WorkModel(BaseModel): name: str @strawberry.experimental.pydantic.type(WorkModel) class Work: name: strawberry.auto class UserModel(BaseModel): work: list[WorkModel] @strawberry.experimental.pydantic.type(UserModel) class User: work: strawberry.auto origin_user = UserModel( work=[ WorkModel(name="Ice Cream inc"), WorkModel(name="Wall Street"), ] ) user = User.from_pydantic(origin_user) assert user.work == [Work(name="Ice Cream inc"), Work(name="Wall Street")] def test_can_convert_pydantic_type_with_list_of_nested_int_to_strawberry(): class UserModel(BaseModel): hours: list[int] @strawberry.experimental.pydantic.type(UserModel) class User: hours: strawberry.auto origin_user = UserModel( hours=[ 8, 9, 10, ] ) user = User.from_pydantic(origin_user) assert user.hours == [8, 9, 10] def test_can_convert_pydantic_type_with_matrix_list_of_nested_int_to_strawberry(): class UserModel(BaseModel): hours: list[list[int]] @strawberry.experimental.pydantic.type(UserModel) class User: hours: strawberry.auto origin_user = UserModel( hours=[ [8, 10], [9, 11], [10, 12], ] ) user = User.from_pydantic(origin_user) assert user.hours == [ [8, 10], [9, 11], [10, 12], ] def test_can_convert_pydantic_type_with_matrix_list_of_nested_model_to_strawberry(): class HourModel(BaseModel): hour: int @strawberry.experimental.pydantic.type(HourModel) class Hour: hour: strawberry.auto class UserModel(BaseModel): hours: list[list[HourModel]] @strawberry.experimental.pydantic.type(UserModel) class User: hours: strawberry.auto origin_user = UserModel( hours=[ [ HourModel(hour=1), HourModel(hour=2), ], [ HourModel(hour=3), HourModel(hour=4), ], [ HourModel(hour=5), HourModel(hour=6), ], ] ) user = User.from_pydantic(origin_user) assert user.hours == [ [ Hour(hour=1), Hour(hour=2), ], [ Hour(hour=3), Hour(hour=4), ], [ Hour(hour=5), Hour(hour=6), ], ] def test_can_convert_pydantic_type_to_strawberry_with_union(): class BranchA(BaseModel): field_a: str class BranchB(BaseModel): field_b: int class User(BaseModel): age: int union_field: BranchA | BranchB @strawberry.experimental.pydantic.type(BranchA) class BranchAType: field_a: strawberry.auto @strawberry.experimental.pydantic.type(BranchB) class BranchBType: field_b: strawberry.auto @strawberry.experimental.pydantic.type(User) class UserType: age: strawberry.auto union_field: strawberry.auto origin_user = User(age=1, union_field=BranchA(field_a="abc")) user = UserType.from_pydantic(origin_user) assert user.age == 1 assert isinstance(user.union_field, BranchAType) assert user.union_field.field_a == "abc" origin_user = User(age=1, union_field=BranchB(field_b=123)) user = UserType.from_pydantic(origin_user) assert user.age == 1 assert isinstance(user.union_field, BranchBType) assert user.union_field.field_b == 123 def test_can_convert_pydantic_type_to_strawberry_with_union_of_strawberry_types(): @strawberry.type class BranchA: field_a: str @strawberry.type class BranchB: field_b: int class User(BaseModel): age: int union_field: BranchA | BranchB @strawberry.experimental.pydantic.type(User) class UserType: age: strawberry.auto union_field: strawberry.auto origin_user = User(age=1, union_field=BranchA(field_a="abc")) user = UserType.from_pydantic(origin_user) assert user.age == 1 assert isinstance(user.union_field, BranchA) assert user.union_field.field_a == "abc" origin_user = User(age=1, union_field=BranchB(field_b=123)) user = UserType.from_pydantic(origin_user) assert user.age == 1 assert isinstance(user.union_field, BranchB) assert user.union_field.field_b == 123 def test_can_convert_pydantic_type_to_strawberry_with_union_nullable(): class BranchA(BaseModel): field_a: str class BranchB(BaseModel): field_b: int class User(BaseModel): age: int union_field: None | BranchA | BranchB @strawberry.experimental.pydantic.type(BranchA) class BranchAType: field_a: strawberry.auto @strawberry.experimental.pydantic.type(BranchB) class BranchBType: field_b: strawberry.auto @strawberry.experimental.pydantic.type(User) class UserType: age: strawberry.auto union_field: strawberry.auto origin_user = User(age=1, union_field=BranchA(field_a="abc")) user = UserType.from_pydantic(origin_user) assert user.age == 1 assert isinstance(user.union_field, BranchAType) assert user.union_field.field_a == "abc" origin_user = User(age=1, union_field=BranchB(field_b=123)) user = UserType.from_pydantic(origin_user) assert user.age == 1 assert isinstance(user.union_field, BranchBType) assert user.union_field.field_b == 123 origin_user = User(age=1, union_field=None) user = UserType.from_pydantic(origin_user) assert user.age == 1 assert user.union_field is None def test_can_convert_pydantic_type_to_strawberry_with_enum(): @strawberry.enum class UserKind(Enum): user = 0 admin = 1 class User(BaseModel): age: int kind: UserKind @strawberry.experimental.pydantic.type(User) class UserType: age: strawberry.auto kind: strawberry.auto origin_user = User(age=1, kind=UserKind.user) user = UserType.from_pydantic(origin_user) assert user.age == 1 assert user.kind == UserKind.user def test_can_convert_pydantic_type_to_strawberry_with_interface(): class Base(BaseModel): base_field: str class BranchA(Base): field_a: str class BranchB(Base): field_b: int class User(BaseModel): age: int interface_field: Base @strawberry.experimental.pydantic.interface(Base) class BaseType: base_field: strawberry.auto @strawberry.experimental.pydantic.type(BranchA) class BranchAType(BaseType): field_a: strawberry.auto @strawberry.experimental.pydantic.type(BranchB) class BranchBType(BaseType): field_b: strawberry.auto @strawberry.experimental.pydantic.type(User) class UserType: age: strawberry.auto interface_field: strawberry.auto origin_user = User(age=1, interface_field=BranchA(field_a="abc", base_field="def")) user = UserType.from_pydantic(origin_user) assert user.age == 1 assert isinstance(user.interface_field, BranchAType) assert user.interface_field.field_a == "abc" origin_user = User(age=1, interface_field=BranchB(field_b=123, base_field="def")) user = UserType.from_pydantic(origin_user) assert user.age == 1 assert isinstance(user.interface_field, BranchBType) assert user.interface_field.field_b == 123 def test_can_convert_pydantic_type_to_strawberry_with_additional_fields(): class UserModel(BaseModel): password: str | None @strawberry.experimental.pydantic.type(UserModel) class User: age: int password: strawberry.auto origin_user = UserModel(password="abc") user = User.from_pydantic(origin_user, extra={"age": 1}) assert user.age == 1 assert user.password == "abc" def test_can_convert_pydantic_type_to_strawberry_with_additional_nested_fields(): @strawberry.type class Work: name: str class UserModel(BaseModel): password: str | None @strawberry.experimental.pydantic.type(UserModel) class User: work: Work password: strawberry.auto origin_user = UserModel(password="abc") user = User.from_pydantic(origin_user, extra={"work": {"name": "Ice inc"}}) assert user.work.name == "Ice inc" assert user.password == "abc" def test_can_convert_pydantic_type_to_strawberry_with_additional_list_nested_fields(): @strawberry.type class Work: name: str class UserModel(BaseModel): password: str | None @strawberry.experimental.pydantic.type(UserModel) class User: work: list[Work] password: strawberry.auto origin_user = UserModel(password="abc") user = User.from_pydantic( origin_user, extra={ "work": [ {"name": "Software inc"}, {"name": "Homemade inc"}, ] }, ) assert user.work == [ Work(name="Software inc"), Work(name="Homemade inc"), ] assert user.password == "abc" def test_can_convert_pydantic_type_to_strawberry_with_missing_data_in_nested_type(): class WorkModel(BaseModel): name: str @strawberry.experimental.pydantic.type(WorkModel) class Work: year: int name: strawberry.auto class UserModel(BaseModel): work: list[WorkModel] @strawberry.experimental.pydantic.type(UserModel) class User: work: strawberry.auto origin_user = UserModel(work=[WorkModel(name="Software inc")]) user = User.from_pydantic( origin_user, extra={ "work": [ {"year": 2020}, ] }, ) assert user.work == [ Work(name="Software inc", year=2020), ] def test_can_convert_pydantic_type_to_strawberry_with_missing_index_data_nested_type(): class WorkModel(BaseModel): name: str @strawberry.experimental.pydantic.type(WorkModel) class Work: year: int name: strawberry.auto class UserModel(BaseModel): work: list[WorkModel | None] @strawberry.experimental.pydantic.type(UserModel) class User: work: strawberry.auto origin_user = UserModel( work=[ WorkModel(name="Software inc"), None, ] ) user = User.from_pydantic( origin_user, extra={ "work": [ {"year": 2020}, {"name": "Alternative", "year": 3030}, ] }, ) assert user.work == [ Work(name="Software inc", year=2020), # This was None in the UserModel Work(name="Alternative", year=3030), ] def test_can_convert_pydantic_type_to_strawberry_with_optional_list(): class WorkModel(BaseModel): name: str @strawberry.experimental.pydantic.type(WorkModel) class Work: name: strawberry.auto year: int class UserModel(BaseModel): work: WorkModel | None @strawberry.experimental.pydantic.type(UserModel) class User: work: strawberry.auto origin_user = UserModel(work=None) user = User.from_pydantic( origin_user, ) assert user.work is None def test_can_convert_pydantic_type_to_strawberry_with_optional_nested_value(): class UserModel(BaseModel): names: list[str] | None @strawberry.experimental.pydantic.type(UserModel) class User: names: strawberry.auto origin_user = UserModel(names=None) user = User.from_pydantic( origin_user, ) assert user.names is None def test_can_convert_input_types_to_pydantic(): class User(BaseModel): age: int password: str | None @strawberry.experimental.pydantic.input(User) class UserInput: age: strawberry.auto password: strawberry.auto data = UserInput(age=1, password=None) user = data.to_pydantic() assert user.age == 1 assert user.password is None def test_can_convert_input_types_to_pydantic_default_values(): class User(BaseModel): age: int password: str | None = None @strawberry.experimental.pydantic.input(User) class UserInput: age: strawberry.auto password: strawberry.auto data = UserInput(age=1) user = data.to_pydantic() assert user.age == 1 assert user.password is None def test_can_convert_input_types_to_pydantic_default_values_defaults_declared_first(): # test that we can declare a field with a default. before a field without a default class User(BaseModel): password: str | None = None age: int @strawberry.experimental.pydantic.input(User) class UserInput: password: strawberry.auto age: strawberry.auto data = UserInput(age=1) user = data.to_pydantic() assert user.age == 1 assert user.password is None definition: StrawberryObjectDefinition = UserInput.__strawberry_definition__ assert definition.name == "UserInput" [ password_field, age_field, ] = definition.fields assert age_field.python_name == "age" assert age_field.type is int assert password_field.python_name == "password" assert isinstance(password_field.type, StrawberryOptional) assert password_field.type.of_type is str def test_can_convert_pydantic_type_to_strawberry_newtype(): Password = NewType("Password", str) class User(BaseModel): age: int password: Password | None @strawberry.experimental.pydantic.type(User) class UserType: age: strawberry.auto password: strawberry.auto origin_user = User(age=1, password="abc") user = UserType.from_pydantic(origin_user) assert user.age == 1 assert user.password == "abc" def test_can_convert_pydantic_type_to_strawberry_newtype_list(): Password = NewType("Password", str) class User(BaseModel): age: int passwords: list[Password] @strawberry.experimental.pydantic.type(User) class UserType: age: strawberry.auto passwords: strawberry.auto origin_user = User(age=1, passwords=["hunter2"]) user = UserType.from_pydantic(origin_user) assert user.age == 1 assert user.passwords == ["hunter2"] def test_get_default_factory_for_field(): class User(BaseModel): pass compat = PydanticCompat.from_model(User) MISSING_TYPE = compat.PYDANTIC_MISSING_TYPE def _get_field( default: Any = MISSING_TYPE, default_factory: Any = MISSING_TYPE, ) -> CompatModelField: return CompatModelField( name="a", type_=str, outer_type_=str, default=default, default_factory=default_factory, alias="a", allow_none=False, description="", has_alias=False, required=True, _missing_type=MISSING_TYPE, is_v1=not IS_PYDANTIC_V2, ) field = _get_field() assert get_default_factory_for_field(field, compat) is dataclasses.MISSING def factory_func(): return "strawberry" field = _get_field(default_factory=factory_func) # should return the default_factory unchanged assert get_default_factory_for_field(field, compat) is factory_func mutable_default = [123, "strawberry"] field = _get_field(mutable_default) created_factory = get_default_factory_for_field(field, compat) # should return a factory that copies the default parameter assert created_factory() == mutable_default assert created_factory() is not mutable_default field = _get_field(default=mutable_default, default_factory=factory_func) with pytest.raises( BothDefaultAndDefaultFactoryDefinedError, match=r"Not allowed to specify both default and default_factory.", ): get_default_factory_for_field(field, compat) def test_convert_input_types_to_pydantic_default_and_default_factory(): # Pydantic should raise an error if the user specifies both default # and default_factory. this checks for a regression on their side if IS_PYDANTIC_V2: with pytest.raises( TypeError, match=("cannot specify both default and default_factory"), ): class User(BaseModel): password: str | None = Field(default=None, default_factory=lambda: None) else: with pytest.raises( ValueError, match=("cannot specify both default and default_factory"), ): class User(BaseModel): password: str | None = Field(default=None, default_factory=lambda: None) def test_can_convert_pydantic_type_to_strawberry_with_additional_field_resolvers(): def some_resolver() -> int: return 84 class UserModel(BaseModel): password: str | None new_age: int @strawberry.experimental.pydantic.type(UserModel) class User: password: strawberry.auto new_age: int = strawberry.field(resolver=some_resolver) @strawberry.field def age() -> int: return 42 origin_user = UserModel(password="abc", new_age=21) user = User.from_pydantic(origin_user) assert user.password == "abc" assert User.__strawberry_definition__.fields[0].name == "age" assert User.__strawberry_definition__.fields[0].base_resolver() == 42 assert User.__strawberry_definition__.fields[2].name == "new_age" assert User.__strawberry_definition__.fields[2].base_resolver() == 84 def test_can_convert_both_output_and_input_type(): class Work(BaseModel): time: float class User(BaseModel): name: str work: Work | None class Group(BaseModel): users: list[User] # Test both definition orders @strawberry.experimental.pydantic.input(Work) class WorkInput: time: strawberry.auto @strawberry.experimental.pydantic.type(Work) class WorkOutput: time: strawberry.auto @strawberry.experimental.pydantic.type(User) class UserOutput: name: strawberry.auto work: strawberry.auto @strawberry.experimental.pydantic.input(User) class UserInput: name: strawberry.auto work: strawberry.auto @strawberry.experimental.pydantic.input(Group) class GroupInput: users: strawberry.auto @strawberry.experimental.pydantic.type(Group) class GroupOutput: users: strawberry.auto origin_group = Group( users=[ User(name="Alice", work=Work(time=10.0)), User(name="Bob", work=Work(time=5.0)), ] ) group = GroupOutput.from_pydantic(origin_group) final_group = group.to_pydantic() assert origin_group == final_group group_input = GroupInput.from_pydantic(origin_group) final_group = group_input.to_pydantic() assert origin_group == final_group def test_custom_conversion_functions(): class User(BaseModel): age: int password: str | None @strawberry.experimental.pydantic.type(User) class UserType: age: str password: strawberry.auto @staticmethod def from_pydantic( instance: User, extra: dict[str, Any] | None = None ) -> "UserType": return UserType( age=str(instance.age), password=base64.b64encode(instance.password.encode()).decode() if instance.password else None, ) def to_pydantic(self) -> User: return User( age=int(self.age), password=base64.b64decode(self.password.encode()).decode() if self.password else None, ) user = User(age=1, password="abc") user_strawberry = UserType.from_pydantic(user) assert user_strawberry.age == "1" assert user_strawberry.password == "YWJj" user_pydantic = user_strawberry.to_pydantic() assert user == user_pydantic def test_nested_custom_conversion_functions(): class User(BaseModel): age: int password: str | None class Parent(BaseModel): user: User @strawberry.experimental.pydantic.type(User) class UserType: age: str password: strawberry.auto @staticmethod def from_pydantic( instance: User, extra: dict[str, Any] | None = None ) -> "UserType": return UserType( age=str(instance.age), password=base64.b64encode(instance.password.encode()).decode() if instance.password else None, ) def to_pydantic(self) -> User: return User( age=int(self.age), password=base64.b64decode(self.password.encode()).decode() if self.password else None, ) @strawberry.experimental.pydantic.type(Parent) class ParentType: user: strawberry.auto user = User(age=1, password="abc") parent = Parent(user=user) parent_strawberry = ParentType.from_pydantic(parent) assert parent_strawberry.user.age == "1" assert parent_strawberry.user.password == "YWJj" parent_pydantic = parent_strawberry.to_pydantic() assert parent == parent_pydantic def test_can_convert_input_types_to_pydantic_with_non_pydantic_dataclass(): @strawberry.type class Work: hours: int class User(BaseModel): age: int password: str | None work: Work @strawberry.experimental.pydantic.input(User) class UserInput: age: strawberry.auto password: strawberry.auto work: strawberry.auto data = UserInput(age=1, password=None, work=Work(hours=1)) user = data.to_pydantic() assert user.age == 1 assert user.password is None assert user.work.hours == 1 def test_can_convert_input_types_to_pydantic_with_dict(): class Work(BaseModel): hours: int class User(BaseModel): age: int password: str | None work: dict[str, Work] @strawberry.experimental.pydantic.input(Work) class WorkInput: hours: strawberry.auto @strawberry.experimental.pydantic.input(User) class UserInput: age: strawberry.auto password: strawberry.auto work: strawberry.auto data = UserInput(age=1, password=None, work={"Monday": Work(hours=1)}) user = data.to_pydantic() assert user.age == 1 assert user.password is None assert user.work["Monday"].hours == 1 def test_can_add_missing_arguments_to_pydantic(): class User(BaseModel): age: int password: str @strawberry.experimental.pydantic.type(User) class UserInput: age: strawberry.auto data = UserInput(age=1) user = data.to_pydantic(password="hunter2") assert user.age == 1 assert user.password == "hunter2" def test_raise_missing_arguments_to_pydantic(): class User(BaseModel): age: int password: str @strawberry.experimental.pydantic.type(User) class UserInput: age: strawberry.auto data = UserInput(age=1) with pytest.raises( ValidationError, match=("1 validation error for User"), ): data.to_pydantic() def test_can_convert_generic_alias_fields_to_strawberry(): class TestModel(BaseModel): list_1d: list[int] list_2d: list[list[int]] @strawberry.experimental.pydantic.type(TestModel) class Test: list_1d: strawberry.auto list_2d: strawberry.auto fields = Test.__strawberry_definition__.fields assert isinstance(fields[0].type, StrawberryList) assert isinstance(fields[1].type, StrawberryList) model = TestModel( list_1d=[1, 2, 3], list_2d=[[1, 2], [3]], ) test = Test.from_pydantic(model) assert test.list_1d == [1, 2, 3] assert test.list_2d == [[1, 2], [3]] def test_can_convert_optional_union_type_expression_fields_to_strawberry(): class TestModel(BaseModel): optional_list: list[int] | None optional_str: str | None @strawberry.experimental.pydantic.type(TestModel) class Test: optional_list: strawberry.auto optional_str: strawberry.auto fields = Test.__strawberry_definition__.fields assert isinstance(fields[0].type, StrawberryOptional) assert isinstance(fields[1].type, StrawberryOptional) model = TestModel( optional_list=[1, 2, 3], optional_str=None, ) test = Test.from_pydantic(model) assert test.optional_list == [1, 2, 3] assert test.optional_str is None @needs_pydantic_v1 def test_can_convert_pydantic_type_to_strawberry_with_constrained_list(): from pydantic import ConstrainedList class WorkModel(BaseModel): name: str class WorkList(ConstrainedList): min_items = 1 class UserModel(BaseModel): work: WorkList[WorkModel] @strawberry.experimental.pydantic.type(WorkModel) class Work: name: strawberry.auto @strawberry.experimental.pydantic.type(UserModel) class User: work: strawberry.auto origin_user = UserModel( work=[WorkModel(name="developer"), WorkModel(name="tester")] ) user = User.from_pydantic(origin_user) assert user == User(work=[Work(name="developer"), Work(name="tester")]) SI = TypeVar("SI", covariant=True) # pragma: no mutate class SpecialList(list[SI]): pass @needs_pydantic_v1 def test_can_convert_pydantic_type_to_strawberry_with_specialized_list(): class WorkModel(BaseModel): name: str class WorkList(SpecialList[SI]): min_items = 1 class UserModel(BaseModel): work: WorkList[WorkModel] @strawberry.experimental.pydantic.type(WorkModel) class Work: name: strawberry.auto @strawberry.experimental.pydantic.type(UserModel) class User: work: strawberry.auto origin_user = UserModel( work=[WorkModel(name="developer"), WorkModel(name="tester")] ) user = User.from_pydantic(origin_user) assert user == User(work=[Work(name="developer"), Work(name="tester")]) @pytest.mark.skipif( not IS_PYDANTIC_V2, reason="Requires Pydantic v2 for computed_field" ) def test_can_convert_pydantic_type_to_strawberry_computed_field(): """Test that computed fields on a pydantic type are not accessed unless queried.""" class UserModel(BaseModel): age: int @computed_field @property def name(self) -> str: raise Exception("`name` computed_field should not be accessed") @computed_field @property def location(self) -> str: return "earth" def get_name(root) -> str: return root._original_model.name def get_location(root) -> str: return root._original_model.location @strawberry.experimental.pydantic.type(UserModel) class User: age: int name: str = strawberry.field(resolver=get_name) location: str = strawberry.field(resolver=get_location) @strawberry.type class Query: @strawberry.field def user(self) -> User: return User.from_pydantic(UserModel(age=20)) schema = strawberry.Schema(query=Query) query = "{ user { age location } }" result = schema.execute_sync(query) assert not result.errors assert result.data["user"] == {"age": 20, "location": "earth"} strawberry-graphql-0.287.0/tests/experimental/pydantic/test_error_type.py000066400000000000000000000260641511033167500270170ustar00rootroot00000000000000import pydantic import pytest import strawberry from strawberry.experimental.pydantic.exceptions import MissingFieldsListError from strawberry.types.base import ( StrawberryList, StrawberryObjectDefinition, StrawberryOptional, ) def test_basic_error_type_fields(): class UserModel(pydantic.BaseModel): name: str age: int @strawberry.experimental.pydantic.error_type(UserModel) class UserError: name: strawberry.auto age: strawberry.auto definition: StrawberryObjectDefinition = UserError.__strawberry_definition__ assert definition.name == "UserError" [field1, field2] = definition.fields assert field1.python_name == "name" assert isinstance(field1.type, StrawberryOptional) assert isinstance(field1.type.of_type, StrawberryList) assert field1.type.of_type.of_type is str assert definition.fields[1].python_name == "age" assert isinstance(field2.type, StrawberryOptional) assert isinstance(field2.type.of_type, StrawberryList) assert field1.type.of_type.of_type is str def test_basic_error_type(): class UserModel(pydantic.BaseModel): name: str age: int @strawberry.experimental.pydantic.error_type(UserModel) class UserError: name: strawberry.auto age: strawberry.auto definition: StrawberryObjectDefinition = UserError.__strawberry_definition__ assert definition.name == "UserError" [field1, field2] = definition.fields assert field1.python_name == "name" assert isinstance(field1.type, StrawberryOptional) assert isinstance(field1.type.of_type, StrawberryList) assert field1.type.of_type.of_type is str assert definition.fields[1].python_name == "age" assert isinstance(field2.type, StrawberryOptional) assert isinstance(field2.type.of_type, StrawberryList) assert field1.type.of_type.of_type is str def test_basic_error_type_all_fields(): class UserModel(pydantic.BaseModel): name: str age: int @strawberry.experimental.pydantic.error_type(UserModel, all_fields=True) class UserError: pass definition: StrawberryObjectDefinition = UserError.__strawberry_definition__ assert definition.name == "UserError" [field1, field2] = definition.fields assert field1.python_name == "name" assert isinstance(field1.type, StrawberryOptional) assert isinstance(field1.type.of_type, StrawberryList) assert field1.type.of_type.of_type is str assert definition.fields[1].python_name == "age" assert isinstance(field2.type, StrawberryOptional) assert isinstance(field2.type.of_type, StrawberryList) assert field1.type.of_type.of_type is str @pytest.mark.filterwarnings("error") def test_basic_type_all_fields_warn(): class User(pydantic.BaseModel): age: int password: str | None with pytest.raises( UserWarning, match=("Using all_fields overrides any explicitly defined fields"), ): @strawberry.experimental.pydantic.error_type(User, all_fields=True) class UserError: age: strawberry.auto def test_basic_error_type_without_fields_throws_an_error(): class User(pydantic.BaseModel): age: int password: str | None with pytest.raises(MissingFieldsListError): @strawberry.experimental.pydantic.error_type(User) class UserError: pass def test_error_type_with_default_value(): class UserModel(pydantic.BaseModel): name: str = "foo" age: int @strawberry.experimental.pydantic.error_type(UserModel) class UserError: name: strawberry.auto age: strawberry.auto definition: StrawberryObjectDefinition = UserError.__strawberry_definition__ assert definition.name == "UserError" [field1, field2] = definition.fields assert field1.python_name == "name" assert isinstance(field1.type, StrawberryOptional) assert isinstance(field1.type.of_type, StrawberryList) assert field1.type.of_type.of_type is str assert field1.default is None assert field2.python_name == "age" assert isinstance(field2.type, StrawberryOptional) assert isinstance(field2.type.of_type, StrawberryList) assert field2.type.of_type.of_type is str assert field2.default is None def test_error_type_with_nested_model(): class FriendModel(pydantic.BaseModel): food: str class UserModel(pydantic.BaseModel): friend: FriendModel @strawberry.experimental.pydantic.error_type(FriendModel) class FriendError: food: strawberry.auto @strawberry.experimental.pydantic.error_type(UserModel) class UserError: friend: strawberry.auto definition: StrawberryObjectDefinition = UserError.__strawberry_definition__ assert definition.name == "UserError" [field] = definition.fields assert field.python_name == "friend" assert isinstance(field.type, StrawberryOptional) assert field.type.of_type is FriendError def test_error_type_with_list_nested_model(): class FriendModel(pydantic.BaseModel): food: str class UserModel(pydantic.BaseModel): friends: list[FriendModel] @strawberry.experimental.pydantic.error_type(FriendModel) class FriendError: food: strawberry.auto @strawberry.experimental.pydantic.error_type(UserModel) class UserError: friends: strawberry.auto definition: StrawberryObjectDefinition = UserError.__strawberry_definition__ assert definition.name == "UserError" [field] = definition.fields assert field.python_name == "friends" assert isinstance(field.type, StrawberryOptional) assert isinstance(field.type.of_type, StrawberryList) assert isinstance(field.type.of_type.of_type, StrawberryOptional) assert field.type.of_type.of_type.of_type is FriendError def test_error_type_with_list_of_scalar(): class UserModel(pydantic.BaseModel): friends: list[int] @strawberry.experimental.pydantic.error_type(UserModel) class UserError: friends: strawberry.auto definition: StrawberryObjectDefinition = UserError.__strawberry_definition__ assert definition.name == "UserError" [field] = definition.fields assert field.python_name == "friends" assert isinstance(field.type, StrawberryOptional) assert isinstance(field.type.of_type, StrawberryList) assert isinstance(field.type.of_type.of_type, StrawberryOptional) assert isinstance(field.type.of_type.of_type.of_type, StrawberryList) assert field.type.of_type.of_type.of_type.of_type is str def test_error_type_with_optional_field(): class UserModel(pydantic.BaseModel): age: int | None @strawberry.experimental.pydantic.error_type(UserModel) class UserError: age: strawberry.auto definition: StrawberryObjectDefinition = UserError.__strawberry_definition__ assert definition.name == "UserError" [field] = definition.fields assert field.python_name == "age" assert isinstance(field.type, StrawberryOptional) assert isinstance(field.type.of_type, StrawberryList) assert field.type.of_type.of_type is str def test_error_type_with_list_of_optional_scalar(): class UserModel(pydantic.BaseModel): age: list[int | None] @strawberry.experimental.pydantic.error_type(UserModel) class UserError: age: strawberry.auto definition: StrawberryObjectDefinition = UserError.__strawberry_definition__ assert definition.name == "UserError" [field] = definition.fields assert field.python_name == "age" assert isinstance(field.type, StrawberryOptional) assert isinstance(field.type.of_type, StrawberryList) assert isinstance(field.type.of_type.of_type, StrawberryOptional) assert isinstance(field.type.of_type.of_type.of_type, StrawberryList) assert field.type.of_type.of_type.of_type.of_type is str def test_error_type_with_optional_list_scalar(): class UserModel(pydantic.BaseModel): age: list[int] | None @strawberry.experimental.pydantic.error_type(UserModel) class UserError: age: strawberry.auto definition: StrawberryObjectDefinition = UserError.__strawberry_definition__ assert definition.name == "UserError" [field] = definition.fields assert field.python_name == "age" assert isinstance(field.type, StrawberryOptional) assert isinstance(field.type.of_type, StrawberryList) assert isinstance(field.type.of_type.of_type, StrawberryOptional) assert isinstance(field.type.of_type.of_type.of_type, StrawberryList) assert field.type.of_type.of_type.of_type.of_type is str def test_error_type_with_optional_list_of_optional_scalar(): class UserModel(pydantic.BaseModel): age: list[int | None] | None @strawberry.experimental.pydantic.error_type(UserModel) class UserError: age: strawberry.auto definition: StrawberryObjectDefinition = UserError.__strawberry_definition__ assert definition.name == "UserError" [field] = definition.fields assert field.python_name == "age" assert isinstance(field.type, StrawberryOptional) assert isinstance(field.type.of_type, StrawberryList) assert isinstance(field.type.of_type.of_type, StrawberryOptional) assert isinstance(field.type.of_type.of_type.of_type, StrawberryList) assert field.type.of_type.of_type.of_type.of_type is str def test_error_type_with_optional_list_of_nested_model(): class FriendModel(pydantic.BaseModel): name: str @strawberry.experimental.pydantic.error_type(FriendModel) class FriendError: name: strawberry.auto class UserModel(pydantic.BaseModel): friends: list[FriendModel] | None @strawberry.experimental.pydantic.error_type(UserModel) class UserError: friends: strawberry.auto definition: StrawberryObjectDefinition = UserError.__strawberry_definition__ assert definition.name == "UserError" [field] = definition.fields assert field.python_name == "friends" assert isinstance(field.type, StrawberryOptional) assert isinstance(field.type.of_type, StrawberryList) assert isinstance(field.type.of_type.of_type, StrawberryOptional) assert field.type.of_type.of_type.of_type is FriendError def test_error_type_with_matrix_list_of_scalar(): class UserModel(pydantic.BaseModel): age: list[list[int]] @strawberry.experimental.pydantic.error_type(UserModel) class UserError: age: strawberry.auto definition: StrawberryObjectDefinition = UserError.__strawberry_definition__ assert definition.name == "UserError" [field] = definition.fields assert field.python_name == "age" assert isinstance(field.type, StrawberryOptional) assert isinstance(field.type.of_type, StrawberryList) assert isinstance(field.type.of_type.of_type, StrawberryOptional) assert isinstance(field.type.of_type.of_type.of_type, StrawberryList) assert isinstance(field.type.of_type.of_type.of_type.of_type, StrawberryOptional) assert isinstance( field.type.of_type.of_type.of_type.of_type.of_type, StrawberryList ) assert field.type.of_type.of_type.of_type.of_type.of_type.of_type is str strawberry-graphql-0.287.0/tests/experimental/pydantic/test_fields.py000066400000000000000000000141661511033167500260730ustar00rootroot00000000000000import re from typing import Literal import pydantic import pytest from pydantic import BaseModel, ValidationError, conlist import strawberry from strawberry.experimental.pydantic._compat import IS_PYDANTIC_V1 from strawberry.types.base import StrawberryObjectDefinition, StrawberryOptional from tests.experimental.pydantic.utils import needs_pydantic_v1, needs_pydantic_v2 @pytest.mark.parametrize( ("pydantic_type", "field_type"), [ (pydantic.ConstrainedInt, int), (pydantic.PositiveInt, int), (pydantic.NegativeInt, int), (pydantic.StrictInt, int), (pydantic.StrictStr, str), (pydantic.ConstrainedStr, str), (pydantic.SecretStr, str), (pydantic.StrictBool, bool), (pydantic.ConstrainedBytes, bytes), (pydantic.SecretBytes, bytes), (pydantic.EmailStr, str), (pydantic.AnyUrl, str), (pydantic.AnyHttpUrl, str), (pydantic.HttpUrl, str), (pydantic.PostgresDsn, str), (pydantic.RedisDsn, str), ] if IS_PYDANTIC_V1 else [ (pydantic.PositiveInt, int), (pydantic.NegativeInt, int), (pydantic.StrictInt, int), (pydantic.StrictStr, str), (pydantic.SecretStr, str), (pydantic.StrictBool, bool), (pydantic.SecretBytes, bytes), (pydantic.EmailStr, str), (pydantic.AnyUrl, str), (pydantic.AnyHttpUrl, str), (pydantic.HttpUrl, str), (pydantic.PostgresDsn, str), (pydantic.RedisDsn, str), ], ) def test_types(pydantic_type, field_type): class Model(pydantic.BaseModel): field: pydantic_type @strawberry.experimental.pydantic.type(Model) class Type: field: strawberry.auto definition: StrawberryObjectDefinition = Type.__strawberry_definition__ assert definition.name == "Type" [field] = definition.fields assert field.python_name == "field" assert field.type is field_type @needs_pydantic_v1 @pytest.mark.parametrize( ("pydantic_type", "field_type"), [(pydantic.NoneStr, str)] if IS_PYDANTIC_V1 else [], ) def test_types_optional(pydantic_type, field_type): class Model(pydantic.BaseModel): field: pydantic_type @strawberry.experimental.pydantic.type(Model) class Type: field: strawberry.auto definition: StrawberryObjectDefinition = Type.__strawberry_definition__ assert definition.name == "Type" [field] = definition.fields assert field.python_name == "field" assert isinstance(field.type, StrawberryOptional) assert field.type.of_type is field_type @needs_pydantic_v2 def test_conint(): class Model(pydantic.BaseModel): field: pydantic.conint(lt=100) @strawberry.experimental.pydantic.type(Model) class Type: field: strawberry.auto definition: StrawberryObjectDefinition = Type.__strawberry_definition__ assert definition.name == "Type" [field] = definition.fields assert field.python_name == "field" assert field.type is int @needs_pydantic_v1 def test_confloat(): class Model(pydantic.BaseModel): field: pydantic.confloat(lt=100.5) @strawberry.experimental.pydantic.type(Model) class Type: field: strawberry.auto definition: StrawberryObjectDefinition = Type.__strawberry_definition__ assert definition.name == "Type" [field] = definition.fields assert field.python_name == "field" assert field.type is float @needs_pydantic_v1 def test_constr(): class Model(pydantic.BaseModel): field: pydantic.constr(max_length=100) @strawberry.experimental.pydantic.type(Model) class Type: field: strawberry.auto definition: StrawberryObjectDefinition = Type.__strawberry_definition__ assert definition.name == "Type" [field] = definition.fields assert field.python_name == "field" assert field.type is str @needs_pydantic_v1 def test_constrained_list(): class User(BaseModel): friends: conlist(str, min_items=1) @strawberry.experimental.pydantic.type(model=User, all_fields=True) class UserType: ... assert UserType.__strawberry_definition__.fields[0].name == "friends" assert ( UserType.__strawberry_definition__.fields[0].type_annotation.raw_annotation == list[str] ) data = UserType(friends=[]) with pytest.raises( ValidationError, match=re.escape( "ensure this value has at least 1 items " "(type=value_error.list.min_items; limit_value=1)", ), ): # validation errors should happen when converting to pydantic data.to_pydantic() @needs_pydantic_v1 def test_constrained_list_nested(): class User(BaseModel): friends: conlist(conlist(int, min_items=1), min_items=1) @strawberry.experimental.pydantic.type(model=User, all_fields=True) class UserType: ... assert UserType.__strawberry_definition__.fields[0].name == "friends" assert ( UserType.__strawberry_definition__.fields[0].type_annotation.raw_annotation == list[list[int]] ) @needs_pydantic_v1 @pytest.mark.parametrize( "pydantic_type", [ pydantic.StrBytes, pydantic.NoneStrBytes, pydantic.PyObject, pydantic.FilePath, pydantic.DirectoryPath, pydantic.Json, pydantic.PaymentCardNumber, pydantic.ByteSize, # pydantic.JsonWrapper, ] if IS_PYDANTIC_V1 else [], ) def test_unsupported_types(pydantic_type): class Model(pydantic.BaseModel): field: pydantic_type with pytest.raises( strawberry.experimental.pydantic.exceptions.UnsupportedTypeError ): @strawberry.experimental.pydantic.type(Model) class Type: field: strawberry.auto def test_literal_types(): class Model(pydantic.BaseModel): field: Literal["field"] @strawberry.experimental.pydantic.type(Model) class Type: field: strawberry.auto definition: StrawberryObjectDefinition = Type.__strawberry_definition__ assert definition.name == "Type" [field] = definition.fields assert field.python_name == "field" assert field.type == Literal["field"] strawberry-graphql-0.287.0/tests/experimental/pydantic/utils.py000066400000000000000000000004121511033167500247130ustar00rootroot00000000000000import pytest from strawberry.experimental.pydantic._compat import IS_PYDANTIC_V2 needs_pydantic_v2 = pytest.mark.skipif( not IS_PYDANTIC_V2, reason="requires Pydantic v2" ) needs_pydantic_v1 = pytest.mark.skipif(IS_PYDANTIC_V2, reason="requires Pydantic v1") strawberry-graphql-0.287.0/tests/extensions/000077500000000000000000000000001511033167500210735ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/extensions/__init__.py000066400000000000000000000000001511033167500231720ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/extensions/test_custom_objects_for_setting_attribute.py000066400000000000000000000072571511033167500322300ustar00rootroot00000000000000import pytest from strawberry.extensions.tracing.opentelemetry import OpenTelemetryExtension @pytest.fixture def otel_ext(): return OpenTelemetryExtension() class SimpleObject: def __init__(self, value): self.value = value def __str__(self): return f"SimpleObject({self.value})" class ComplexObject: def __init__(self, simple_object, value): self.simple_object = simple_object self.value = value def __str__(self): return f"ComplexObject({self.simple_object!s}, {self.value})" def test_convert_complex_number(otel_ext): value = 3 + 4j assert otel_ext.convert_to_allowed_types(value) == "(3+4j)" def test_convert_range(otel_ext): value = range(3) assert otel_ext.convert_to_allowed_types(value) == "0, 1, 2" def test_convert_bytearray(otel_ext): value = bytearray(b"hello world") assert otel_ext.convert_to_allowed_types(value) == b"hello world" def test_convert_memoryview(otel_ext): value = memoryview(b"hello world") assert otel_ext.convert_to_allowed_types(value) == b"hello world" def test_convert_set(otel_ext): value = {1, 2, 3, 4} converted_value = otel_ext.convert_to_allowed_types(value) assert set(converted_value.strip("{}").split(", ")) == {"1", "2", "3", "4"} def test_convert_frozenset(otel_ext): value = frozenset([1, 2, 3, 4]) converted_value = otel_ext.convert_to_allowed_types(value) assert set(converted_value.strip("{}").split(", ")) == {"1", "2", "3", "4"} def test_convert_complex_object_with_simple_object(otel_ext): simple_obj = SimpleObject(42) complex_obj = ComplexObject(simple_obj, 99) assert ( otel_ext.convert_to_allowed_types(complex_obj) == "ComplexObject(SimpleObject(42), 99)" ) def test_convert_dictionary(otel_ext): value = { "int": 1, "float": 3.14, "bool": True, "str": "hello", "list": [1, 2, 3], "tuple": (4, 5, 6), "simple_object": SimpleObject(42), } expected = ( "{int: 1, " "float: 3.14, " "bool: True, " "str: hello, " "list: 1, 2, 3, " "tuple: 4, 5, 6, " "simple_object: SimpleObject(42)}" ) assert otel_ext.convert_to_allowed_types(value) == expected def test_convert_bool(otel_ext): assert otel_ext.convert_to_allowed_types(True) is True assert otel_ext.convert_to_allowed_types(False) is False def test_convert_str(otel_ext): assert otel_ext.convert_to_allowed_types("hello") == "hello" def test_convert_bytes(otel_ext): assert otel_ext.convert_to_allowed_types(b"hello") == b"hello" def test_convert_int(otel_ext): assert otel_ext.convert_to_allowed_types(42) == 42 def test_convert_float(otel_ext): assert otel_ext.convert_to_allowed_types(3.14) == 3.14 def test_convert_simple_object(otel_ext): obj = SimpleObject(42) assert otel_ext.convert_to_allowed_types(obj) == "SimpleObject(42)" def test_convert_list_of_basic_types(otel_ext): value = [1, "hello", 3.14, True, False] assert otel_ext.convert_to_allowed_types(value) == "1, hello, 3.14, True, False" def test_convert_list_of_mixed_types(otel_ext): value = [1, "hello", 3.14, SimpleObject(42)] assert ( otel_ext.convert_to_allowed_types(value) == "1, hello, 3.14, SimpleObject(42)" ) def test_convert_tuple_of_basic_types(otel_ext): value = (1, "hello", 3.14, True, False) assert otel_ext.convert_to_allowed_types(value) == "1, hello, 3.14, True, False" def test_convert_tuple_of_mixed_types(otel_ext): value = (1, "hello", 3.14, SimpleObject(42)) assert ( otel_ext.convert_to_allowed_types(value) == "1, hello, 3.14, SimpleObject(42)" ) strawberry-graphql-0.287.0/tests/extensions/test_pyinstrument.py000066400000000000000000000023411511033167500252650ustar00rootroot00000000000000import tempfile import time from pathlib import Path import strawberry from strawberry.extensions import pyinstrument def function_called_by_us_a(): time.sleep(0.1) return function_called_by_us_b() def function_called_by_us_b(): time.sleep(0.1) return function_called_by_us_c() def function_called_by_us_c(): time.sleep(0.1) return 4 def test_basic_pyinstrument(): with tempfile.NamedTemporaryFile(delete=False) as report_file: report_file_path = Path(report_file.name) @strawberry.type class Query: @strawberry.field def the_field(self) -> int: return function_called_by_us_a() schema = strawberry.Schema( query=Query, extensions=[pyinstrument.PyInstrument(report_path=report_file_path)], ) # Query the schema result = schema.execute_sync("{ theField }") content = report_file_path.read_text("utf-8") assert not result.errors assert result.data assert result.data["theField"] == 4 assert "function_called_by_us_a" in content assert "function_called_by_us_b" in content assert "function_called_by_us_c" in content assert content.count('"sleep') == 3 strawberry-graphql-0.287.0/tests/extensions/tracing/000077500000000000000000000000001511033167500225225ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/extensions/tracing/__init__.py000066400000000000000000000000001511033167500246210ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/extensions/tracing/test_opentelemetry.py000066400000000000000000000021671511033167500270350ustar00rootroot00000000000000from unittest.mock import Mock from opentelemetry.trace import Span, TracerProvider from strawberry.extensions import LifecycleStep from strawberry.extensions.tracing.opentelemetry import ( OpenTelemetryExtension, OpenTelemetryExtensionSync, ) def test_span_holder_initialization(): extension = OpenTelemetryExtension() assert extension._span_holder == {} extension._span_holder[LifecycleStep.OPERATION] = Mock(spec=Span) extension = OpenTelemetryExtension() assert extension._span_holder == {} tracer_provider = Mock(spec=TracerProvider) extension = OpenTelemetryExtension(tracer_provider=tracer_provider) assert tracer_provider.get_tracer.called def test_span_holder_initialization_sync(): extension = OpenTelemetryExtensionSync() assert extension._span_holder == {} extension._span_holder[LifecycleStep.OPERATION] = Mock(spec=Span) extension = OpenTelemetryExtensionSync() assert extension._span_holder == {} tracer_provider = Mock(spec=TracerProvider) extension = OpenTelemetryExtension(tracer_provider=tracer_provider) assert tracer_provider.get_tracer.called strawberry-graphql-0.287.0/tests/fastapi/000077500000000000000000000000001511033167500203235ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/fastapi/__init__.py000066400000000000000000000000001511033167500224220ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/fastapi/app.py000066400000000000000000000017211511033167500214560ustar00rootroot00000000000000from typing import Any from fastapi import BackgroundTasks, Depends, FastAPI, Request, WebSocket from strawberry.fastapi import GraphQLRouter from tests.views.schema import schema def custom_context_dependency() -> str: return "Hi!" async def get_context( background_tasks: BackgroundTasks, request: Request = None, ws: WebSocket = None, custom_value=Depends(custom_context_dependency), ) -> dict[str, Any]: return { "custom_value": custom_value, "request": request or ws, "background_tasks": background_tasks, } async def get_root_value( request: Request = None, ws: WebSocket = None ) -> Request | WebSocket: return request or ws def create_app(schema=schema, **kwargs: Any) -> FastAPI: app = FastAPI() graphql_app = GraphQLRouter( schema, context_getter=get_context, root_value_getter=get_root_value, **kwargs ) app.include_router(graphql_app, prefix="/graphql") return app strawberry-graphql-0.287.0/tests/fastapi/test_async.py000066400000000000000000000014301511033167500230470ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING import pytest import strawberry if TYPE_CHECKING: from starlette.testclient import TestClient @pytest.fixture def test_client() -> TestClient: from starlette.testclient import TestClient from tests.fastapi.app import create_app @strawberry.type class Query: @strawberry.field async def hello(self, name: str | None = None) -> str: return f"Hello {name or 'world'}" async_schema = strawberry.Schema(Query) app = create_app(schema=async_schema) return TestClient(app) def test_simple_query(test_client: TestClient): response = test_client.post("/graphql", json={"query": "{ hello }"}) assert response.json() == {"data": {"hello": "Hello world"}} strawberry-graphql-0.287.0/tests/fastapi/test_context.py000066400000000000000000000253751511033167500234340ustar00rootroot00000000000000import asyncio from collections.abc import AsyncGenerator import pytest import strawberry from strawberry.exceptions import InvalidCustomContext from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL from strawberry.subscriptions.protocols.graphql_transport_ws import ( types as transport_ws_types, ) from strawberry.subscriptions.protocols.graphql_ws import types as ws_types def test_base_context(): from strawberry.fastapi import BaseContext base_context = BaseContext() assert base_context.request is None assert base_context.background_tasks is None assert base_context.response is None def test_with_explicit_class_context_getter(): from fastapi import Depends, FastAPI from fastapi.testclient import TestClient from strawberry.fastapi import BaseContext, GraphQLRouter @strawberry.type class Query: @strawberry.field def abc(self, info: strawberry.Info) -> str: assert info.context.request is not None assert info.context.strawberry == "explicitly rocks" assert info.context.connection_params is None return "abc" class CustomContext(BaseContext): def __init__(self, rocks: str): self.strawberry = rocks def custom_context_dependency() -> CustomContext: return CustomContext(rocks="explicitly rocks") def get_context(custom_context: CustomContext = Depends(custom_context_dependency)): return custom_context app = FastAPI() schema = strawberry.Schema(query=Query) graphql_app = GraphQLRouter(schema=schema, context_getter=get_context) app.include_router(graphql_app, prefix="/graphql") test_client = TestClient(app) response = test_client.post("/graphql", json={"query": "{ abc }"}) assert response.status_code == 200 assert response.json() == {"data": {"abc": "abc"}} def test_with_implicit_class_context_getter(): from fastapi import Depends, FastAPI from fastapi.testclient import TestClient from strawberry.fastapi import BaseContext, GraphQLRouter @strawberry.type class Query: @strawberry.field def abc(self, info: strawberry.Info) -> str: assert info.context.request is not None assert info.context.strawberry == "implicitly rocks" assert info.context.connection_params is None return "abc" class CustomContext(BaseContext): def __init__(self, rocks: str = "implicitly rocks"): super().__init__() self.strawberry = rocks def get_context(custom_context: CustomContext = Depends()): return custom_context app = FastAPI() schema = strawberry.Schema(query=Query) graphql_app = GraphQLRouter(schema=schema, context_getter=get_context) app.include_router(graphql_app, prefix="/graphql") test_client = TestClient(app) response = test_client.post("/graphql", json={"query": "{ abc }"}) assert response.status_code == 200 assert response.json() == {"data": {"abc": "abc"}} def test_with_dict_context_getter(): from fastapi import Depends, FastAPI from fastapi.testclient import TestClient from strawberry.fastapi import GraphQLRouter @strawberry.type class Query: @strawberry.field def abc(self, info: strawberry.Info) -> str: assert info.context.get("request") is not None assert "connection_params" not in info.context assert info.context.get("strawberry") == "rocks" return "abc" def custom_context_dependency() -> str: return "rocks" def get_context(value: str = Depends(custom_context_dependency)) -> dict[str, str]: return {"strawberry": value} app = FastAPI() schema = strawberry.Schema(query=Query) graphql_app = GraphQLRouter(schema=schema, context_getter=get_context) app.include_router(graphql_app, prefix="/graphql") test_client = TestClient(app) response = test_client.post("/graphql", json={"query": "{ abc }"}) assert response.status_code == 200 assert response.json() == {"data": {"abc": "abc"}} def test_without_context_getter(): from fastapi import FastAPI from fastapi.testclient import TestClient from strawberry.fastapi import GraphQLRouter @strawberry.type class Query: @strawberry.field def abc(self, info: strawberry.Info) -> str: assert info.context.get("request") is not None assert info.context.get("strawberry") is None return "abc" app = FastAPI() schema = strawberry.Schema(query=Query) graphql_app = GraphQLRouter(schema, context_getter=None) app.include_router(graphql_app, prefix="/graphql") test_client = TestClient(app) response = test_client.post("/graphql", json={"query": "{ abc }"}) assert response.status_code == 200 assert response.json() == {"data": {"abc": "abc"}} def test_with_invalid_context_getter(): from fastapi import Depends, FastAPI from fastapi.testclient import TestClient from strawberry.fastapi import GraphQLRouter @strawberry.type class Query: @strawberry.field def abc(self, info: strawberry.Info) -> str: assert info.context.get("request") is not None assert info.context.get("strawberry") is None return "abc" def custom_context_dependency() -> str: return "rocks" def get_context(value: str = Depends(custom_context_dependency)) -> str: return value app = FastAPI() schema = strawberry.Schema(query=Query) graphql_app = GraphQLRouter(schema=schema, context_getter=get_context) app.include_router(graphql_app, prefix="/graphql") test_client = TestClient(app) with pytest.raises( InvalidCustomContext, match=( "The custom context must be either a class " "that inherits from BaseContext or a dictionary" ), ): test_client.post("/graphql", json={"query": "{ abc }"}) def test_class_context_injects_connection_params_over_transport_ws(): from fastapi import Depends, FastAPI from fastapi.testclient import TestClient from strawberry.fastapi import BaseContext, GraphQLRouter @strawberry.type class Query: x: str = "hi" @strawberry.type class Subscription: @strawberry.subscription async def connection_params( self, info: strawberry.Info, delay: float = 0 ) -> AsyncGenerator[str, None]: assert info.context.request is not None await asyncio.sleep(delay) yield info.context.connection_params["strawberry"] class Context(BaseContext): strawberry: str def __init__(self): self.strawberry = "rocks" def get_context(context: Context = Depends()) -> Context: return context app = FastAPI() schema = strawberry.Schema(query=Query, subscription=Subscription) graphql_app = GraphQLRouter(schema=schema, context_getter=get_context) app.include_router(graphql_app, prefix="/graphql") test_client = TestClient(app) with test_client.websocket_connect( "/graphql", [GRAPHQL_TRANSPORT_WS_PROTOCOL] ) as ws: ws.send_json( transport_ws_types.ConnectionInitMessage( {"type": "connection_init", "payload": {"strawberry": "rocks"}} ) ) connection_ack_message: transport_ws_types.ConnectionInitMessage = ( ws.receive_json() ) assert connection_ack_message == {"type": "connection_ack"} ws.send_json( transport_ws_types.SubscribeMessage( { "id": "sub1", "type": "subscribe", "payload": {"query": "subscription { connectionParams }"}, } ) ) next_message: transport_ws_types.NextMessage = ws.receive_json() assert next_message == { "id": "sub1", "type": "next", "payload": {"data": {"connectionParams": "rocks"}}, } ws.send_json( transport_ws_types.CompleteMessage({"id": "sub1", "type": "complete"}) ) ws.close() def test_class_context_injects_connection_params_over_ws(): from fastapi import Depends, FastAPI from fastapi.testclient import TestClient from starlette.websockets import WebSocketDisconnect from strawberry.fastapi import BaseContext, GraphQLRouter @strawberry.type class Query: x: str = "hi" @strawberry.type class Subscription: @strawberry.subscription async def connection_params( self, info: strawberry.Info, delay: float = 0 ) -> AsyncGenerator[str, None]: assert info.context.request is not None await asyncio.sleep(delay) yield info.context.connection_params["strawberry"] class Context(BaseContext): strawberry: str def __init__(self): self.strawberry = "rocks" def get_context(context: Context = Depends()) -> Context: return context app = FastAPI() schema = strawberry.Schema(query=Query, subscription=Subscription) graphql_app = GraphQLRouter(schema=schema, context_getter=get_context) app.include_router(graphql_app, prefix="/graphql") test_client = TestClient(app) with test_client.websocket_connect("/graphql", [GRAPHQL_WS_PROTOCOL]) as ws: ws.send_json( ws_types.ConnectionInitMessage( { "type": "connection_init", "payload": {"strawberry": "rocks"}, } ) ) ws.send_json( ws_types.StartMessage( { "type": "start", "id": "demo", "payload": { "query": "subscription { connectionParams }", }, } ) ) connection_ack_message: ws_types.ConnectionAckMessage = ws.receive_json() assert connection_ack_message["type"] == "connection_ack" data_message: ws_types.DataMessage = ws.receive_json() assert data_message["type"] == "data" assert data_message["id"] == "demo" assert data_message["payload"]["data"] == {"connectionParams": "rocks"} ws.send_json(ws_types.StopMessage({"type": "stop", "id": "demo"})) complete_message: ws_types.CompleteMessage = ws.receive_json() assert complete_message["type"] == "complete" assert complete_message["id"] == "demo" ws.send_json( ws_types.ConnectionTerminateMessage({"type": "connection_terminate"}) ) # make sure the websocket is disconnected now with pytest.raises(WebSocketDisconnect): ws.receive_json() strawberry-graphql-0.287.0/tests/fastapi/test_openapi.py000066400000000000000000000026301511033167500233700ustar00rootroot00000000000000import strawberry @strawberry.type class Query: abc: str def test_enable_graphiql_view_and_allow_queries_via_get(): from fastapi import FastAPI from strawberry.fastapi import GraphQLRouter app = FastAPI() schema = strawberry.Schema(query=Query) graphql_app = GraphQLRouter[None, None](schema) app.include_router(graphql_app, prefix="/graphql") assert "get" in app.openapi()["paths"]["/graphql"] assert "post" in app.openapi()["paths"]["/graphql"] def test_disable_graphiql_view_and_allow_queries_via_get(): from fastapi import FastAPI from strawberry.fastapi import GraphQLRouter app = FastAPI() schema = strawberry.Schema(query=Query) graphql_app = GraphQLRouter[None, None]( schema, graphql_ide=None, allow_queries_via_get=False ) app.include_router(graphql_app, prefix="/graphql") assert "get" not in app.openapi()["paths"]["/graphql"] assert "post" in app.openapi()["paths"]["/graphql"] def test_graphql_router_with_tags(): from fastapi import FastAPI from strawberry.fastapi import GraphQLRouter app = FastAPI() schema = strawberry.Schema(query=Query) graphql_app = GraphQLRouter[None, None](schema, tags=["abc"]) app.include_router(graphql_app, prefix="/graphql") assert "abc" in app.openapi()["paths"]["/graphql"]["get"]["tags"] assert "abc" in app.openapi()["paths"]["/graphql"]["post"]["tags"] strawberry-graphql-0.287.0/tests/fastapi/test_router.py000066400000000000000000000034721511033167500232620ustar00rootroot00000000000000import pytest import strawberry def test_include_router_prefix(): from fastapi import FastAPI from starlette.testclient import TestClient from strawberry.fastapi import GraphQLRouter @strawberry.type class Query: @strawberry.field def abc(self) -> str: return "abc" app = FastAPI() schema = strawberry.Schema(query=Query) graphql_app = GraphQLRouter[None, None](schema) app.include_router(graphql_app, prefix="/graphql") test_client = TestClient(app) response = test_client.post("/graphql", json={"query": "{ abc }"}) assert response.status_code == 200 assert response.json() == {"data": {"abc": "abc"}} def test_graphql_router_path(): from fastapi import FastAPI from starlette.testclient import TestClient from strawberry.fastapi import GraphQLRouter @strawberry.type class Query: @strawberry.field def abc(self) -> str: return "abc" app = FastAPI() schema = strawberry.Schema(query=Query) graphql_app = GraphQLRouter[None, None](schema, path="/graphql") app.include_router(graphql_app) test_client = TestClient(app) response = test_client.post("/graphql", json={"query": "{ abc }"}) assert response.status_code == 200 assert response.json() == {"data": {"abc": "abc"}} def test_missing_path_and_prefix(): from fastapi import FastAPI from strawberry.fastapi import GraphQLRouter @strawberry.type class Query: @strawberry.field def abc(self) -> str: return "abc" app = FastAPI() schema = strawberry.Schema(query=Query) graphql_app = GraphQLRouter[None, None](schema) with pytest.raises(Exception) as exc: app.include_router(graphql_app) assert "Prefix and path cannot be both empty" in str(exc) strawberry-graphql-0.287.0/tests/federation/000077500000000000000000000000001511033167500210145ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/federation/__init__.py000066400000000000000000000000001511033167500231130ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/federation/printer/000077500000000000000000000000001511033167500224775ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/federation/printer/__init__.py000066400000000000000000000000001511033167500245760ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/federation/printer/test_additional_directives.py000066400000000000000000000053431511033167500304460ustar00rootroot00000000000000# type: ignore import textwrap import strawberry from strawberry.schema.config import StrawberryConfig from strawberry.schema_directive import Location def test_additional_schema_directives_printed_correctly_object(): @strawberry.schema_directive(locations=[Location.OBJECT]) class CacheControl: max_age: int @strawberry.federation.type( keys=["id"], shareable=True, extend=True, directives=[CacheControl(max_age=42)] ) class FederatedType: id: strawberry.ID @strawberry.type class Query: federatedType: FederatedType # noqa: N815 expected_type = """ directive @CacheControl(max_age: Int!) on OBJECT schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@key", "@shareable"]) { query: Query } extend type FederatedType @CacheControl(max_age: 42) @key(fields: "id") @shareable { id: ID! } type Query { _entities(representations: [_Any!]!): [_Entity]! _service: _Service! federatedType: FederatedType! } scalar _Any union _Entity = FederatedType type _Service { sdl: String! } """ schema = strawberry.federation.Schema( query=Query, config=StrawberryConfig(auto_camel_case=False) ) assert schema.as_str() == textwrap.dedent(expected_type).strip() def test_additional_schema_directives_printed_in_order_object(): @strawberry.schema_directive(locations=[Location.OBJECT]) class CacheControl0: max_age: int @strawberry.schema_directive(locations=[Location.OBJECT]) class CacheControl1: min_age: int @strawberry.federation.type( keys=["id"], shareable=True, extend=True, directives=[CacheControl0(max_age=42), CacheControl1(min_age=42)], ) class FederatedType: id: strawberry.ID @strawberry.type class Query: federatedType: FederatedType # noqa: N815 expected_type = """ directive @CacheControl0(max_age: Int!) on OBJECT directive @CacheControl1(min_age: Int!) on OBJECT schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@key", "@shareable"]) { query: Query } extend type FederatedType @CacheControl0(max_age: 42) @CacheControl1(min_age: 42) @key(fields: "id") @shareable { id: ID! } type Query { _entities(representations: [_Any!]!): [_Entity]! _service: _Service! federatedType: FederatedType! } scalar _Any union _Entity = FederatedType type _Service { sdl: String! } """ schema = strawberry.federation.Schema( query=Query, config=StrawberryConfig(auto_camel_case=False) ) assert schema.as_str() == textwrap.dedent(expected_type).strip() strawberry-graphql-0.287.0/tests/federation/printer/test_authenticated.py000066400000000000000000000054421511033167500267370ustar00rootroot00000000000000import textwrap from enum import Enum from typing import Annotated import strawberry def test_field_authenticated_printed_correctly(): @strawberry.federation.interface(authenticated=True) class SomeInterface: id: strawberry.ID @strawberry.federation.type(authenticated=True) class Product(SomeInterface): upc: str = strawberry.federation.field(authenticated=True) @strawberry.federation.type class Query: @strawberry.federation.field(authenticated=True) def top_products( self, first: Annotated[int, strawberry.federation.argument()] ) -> list[Product]: # pragma: no cover return [] schema = strawberry.federation.Schema(query=Query) expected = """ schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@authenticated"]) { query: Query } type Product implements SomeInterface @authenticated { id: ID! upc: String! @authenticated } type Query { _service: _Service! topProducts(first: Int!): [Product!]! @authenticated } interface SomeInterface @authenticated { id: ID! } scalar _Any type _Service { sdl: String! } """ assert schema.as_str() == textwrap.dedent(expected).strip() def test_field_authenticated_printed_correctly_on_scalar(): @strawberry.federation.scalar(authenticated=True) class SomeScalar(str): __slots__ = () @strawberry.federation.type class Query: hello: SomeScalar schema = strawberry.federation.Schema(query=Query) expected = """ schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@authenticated"]) { query: Query } type Query { _service: _Service! hello: SomeScalar! } scalar SomeScalar @authenticated scalar _Any type _Service { sdl: String! } """ assert schema.as_str() == textwrap.dedent(expected).strip() def test_field_authenticated_printed_correctly_on_enum(): @strawberry.federation.enum(authenticated=True) class SomeEnum(Enum): A = "A" @strawberry.federation.type class Query: hello: SomeEnum schema = strawberry.federation.Schema(query=Query) expected = """ schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@authenticated"]) { query: Query } type Query { _service: _Service! hello: SomeEnum! } enum SomeEnum @authenticated { A } scalar _Any type _Service { sdl: String! } """ assert schema.as_str() == textwrap.dedent(expected).strip() strawberry-graphql-0.287.0/tests/federation/printer/test_compose_directive.py000066400000000000000000000065741511033167500276270ustar00rootroot00000000000000import textwrap import strawberry from strawberry.schema_directive import Location def test_schema_directives_and_compose_schema(): @strawberry.federation.schema_directive( locations=[Location.OBJECT], name="cacheControl", compose=True, ) class CacheControl: max_age: int @strawberry.federation.schema_directive( locations=[Location.OBJECT], name="sensitive" ) class Sensitive: reason: str @strawberry.federation.type( keys=["id"], shareable=True, extend=True, directives=[CacheControl(max_age=42), Sensitive(reason="example")], ) class FederatedType: id: strawberry.ID @strawberry.type class Query: federatedType: FederatedType # noqa: N815 expected_type = """ directive @cacheControl(maxAge: Int!) on OBJECT directive @sensitive(reason: String!) on OBJECT schema @composeDirective(name: "@cacheControl") @link(url: "https://directives.strawberry.rocks/cacheControl/v0.1", import: ["@cacheControl"]) @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@composeDirective", "@key", "@shareable"]) { query: Query } extend type FederatedType @cacheControl(maxAge: 42) @sensitive(reason: "example") @key(fields: "id") @shareable { id: ID! } type Query { _entities(representations: [_Any!]!): [_Entity]! _service: _Service! federatedType: FederatedType! } scalar _Any union _Entity = FederatedType type _Service { sdl: String! } """ schema = strawberry.federation.Schema( query=Query, ) assert schema.as_str() == textwrap.dedent(expected_type).strip() def test_schema_directives_and_compose_schema_custom_import_url(): @strawberry.federation.schema_directive( locations=[Location.OBJECT], name="cacheControl", compose=True, import_url="https://f.strawberry.rocks/cacheControl/v1.0", ) class CacheControl: max_age: int @strawberry.federation.schema_directive( locations=[Location.OBJECT], name="sensitive" ) class Sensitive: reason: str @strawberry.federation.type( keys=["id"], shareable=True, extend=True, directives=[CacheControl(max_age=42), Sensitive(reason="example")], ) class FederatedType: id: strawberry.ID @strawberry.type class Query: federatedType: FederatedType # noqa: N815 expected_type = """ directive @cacheControl(maxAge: Int!) on OBJECT directive @sensitive(reason: String!) on OBJECT schema @composeDirective(name: "@cacheControl") @link(url: "https://f.strawberry.rocks/cacheControl/v1.0", import: ["@cacheControl"]) @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@composeDirective", "@key", "@shareable"]) { query: Query } extend type FederatedType @cacheControl(maxAge: 42) @sensitive(reason: "example") @key(fields: "id") @shareable { id: ID! } type Query { _entities(representations: [_Any!]!): [_Entity]! _service: _Service! federatedType: FederatedType! } scalar _Any union _Entity = FederatedType type _Service { sdl: String! } """ schema = strawberry.federation.Schema( query=Query, ) assert schema.as_str() == textwrap.dedent(expected_type).strip() strawberry-graphql-0.287.0/tests/federation/printer/test_entities.py000066400000000000000000000055001511033167500257340ustar00rootroot00000000000000# type: ignore import textwrap import strawberry def test_entities_type_when_no_type_has_keys(): global Review @strawberry.federation.type class User: username: str @strawberry.federation.type(extend=True) class Product: upc: str = strawberry.federation.field(external=True) reviews: list["Review"] @strawberry.federation.type class Review: body: str author: User product: Product @strawberry.federation.type class Query: @strawberry.field def top_products(self, first: int) -> list[Product]: # pragma: no cover return [] schema = strawberry.federation.Schema(query=Query) expected = """ schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@external"]) { query: Query } extend type Product { upc: String! @external reviews: [Review!]! } type Query { _service: _Service! topProducts(first: Int!): [Product!]! } type Review { body: String! author: User! product: Product! } type User { username: String! } scalar _Any type _Service { sdl: String! } """ assert schema.as_str() == textwrap.dedent(expected).strip() del Review def test_entities_type_when_one_type_has_keys(): global Review @strawberry.federation.type class User: username: str @strawberry.federation.type(keys=["upc"], extend=True) class Product: upc: str = strawberry.federation.field(external=True) reviews: list["Review"] @strawberry.federation.type class Review: body: str author: User product: Product @strawberry.federation.type class Query: @strawberry.field def top_products(self, first: int) -> list[Product]: # pragma: no cover return [] schema = strawberry.federation.Schema(query=Query) expected = """ schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@external", "@key"]) { query: Query } extend type Product @key(fields: "upc") { upc: String! @external reviews: [Review!]! } type Query { _entities(representations: [_Any!]!): [_Entity]! _service: _Service! topProducts(first: Int!): [Product!]! } type Review { body: String! author: User! product: Product! } type User { username: String! } scalar _Any union _Entity = Product type _Service { sdl: String! } """ assert schema.as_str() == textwrap.dedent(expected).strip() del Review strawberry-graphql-0.287.0/tests/federation/printer/test_inaccessible.py000066400000000000000000000141211511033167500265330ustar00rootroot00000000000000import textwrap from enum import Enum from typing import Annotated import strawberry def test_field_inaccessible_printed_correctly(): @strawberry.federation.interface(inaccessible=True) class AnInterface: id: strawberry.ID @strawberry.interface class SomeInterface: id: strawberry.ID a_field: str = strawberry.federation.field(inaccessible=True) @strawberry.federation.type(keys=["upc"], extend=True) class Product(SomeInterface): upc: str = strawberry.federation.field(external=True, inaccessible=True) @strawberry.federation.input(inaccessible=True) class AnInput: id: strawberry.ID = strawberry.federation.field(inaccessible=True) @strawberry.federation.type(inaccessible=True) class AnInaccessibleType: id: strawberry.ID @strawberry.federation.type class Query: @strawberry.field def top_products( self, first: Annotated[int, strawberry.federation.argument(inaccessible=True)], ) -> list[Product]: # pragma: no cover return [] schema = strawberry.federation.Schema( query=Query, types=[AnInterface, AnInput, AnInaccessibleType], ) expected = """ schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@external", "@inaccessible", "@key"]) { query: Query } type AnInaccessibleType @inaccessible { id: ID! } input AnInput @inaccessible { id: ID! @inaccessible } interface AnInterface @inaccessible { id: ID! } extend type Product implements SomeInterface @key(fields: "upc") { id: ID! aField: String! @inaccessible upc: String! @external @inaccessible } type Query { _entities(representations: [_Any!]!): [_Entity]! _service: _Service! topProducts(first: Int! @inaccessible): [Product!]! } interface SomeInterface { id: ID! aField: String! @inaccessible } scalar _Any union _Entity = Product type _Service { sdl: String! } """ assert schema.as_str() == textwrap.dedent(expected).strip() def test_inaccessible_on_mutation(): @strawberry.type class Query: hello: str @strawberry.type class Mutation: @strawberry.federation.mutation(inaccessible=True) def hello(self) -> str: # pragma: no cover return "Hello" schema = strawberry.federation.Schema( query=Query, mutation=Mutation, ) expected = """ schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@inaccessible"]) { query: Query mutation: Mutation } type Mutation { hello: String! @inaccessible } type Query { _service: _Service! hello: String! } scalar _Any type _Service { sdl: String! } """ assert schema.as_str() == textwrap.dedent(expected).strip() def test_inaccessible_on_scalar(): SomeScalar = strawberry.federation.scalar(str, name="SomeScalar", inaccessible=True) @strawberry.type class Query: hello: SomeScalar schema = strawberry.federation.Schema( query=Query, ) expected = """ schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@inaccessible"]) { query: Query } type Query { _service: _Service! hello: SomeScalar! } scalar SomeScalar @inaccessible scalar _Any type _Service { sdl: String! } """ assert schema.as_str() == textwrap.dedent(expected).strip() def test_inaccessible_on_enum(): @strawberry.federation.enum(inaccessible=True) class SomeEnum(Enum): A = "A" @strawberry.type class Query: hello: SomeEnum schema = strawberry.federation.Schema( query=Query, ) expected = """ schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@inaccessible"]) { query: Query } type Query { _service: _Service! hello: SomeEnum! } enum SomeEnum @inaccessible { A } scalar _Any type _Service { sdl: String! } """ assert schema.as_str() == textwrap.dedent(expected).strip() def test_inaccessible_on_enum_value(): @strawberry.enum class SomeEnum(Enum): A = strawberry.federation.enum_value("A", inaccessible=True) @strawberry.type class Query: hello: SomeEnum schema = strawberry.federation.Schema( query=Query, ) expected = """ schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@inaccessible"]) { query: Query } type Query { _service: _Service! hello: SomeEnum! } enum SomeEnum { A @inaccessible } scalar _Any type _Service { sdl: String! } """ assert schema.as_str() == textwrap.dedent(expected).strip() def test_field_tag_printed_correctly_on_union(): @strawberry.type class A: a: str @strawberry.type class B: b: str MyUnion = Annotated[A | B, strawberry.federation.union("Union", inaccessible=True)] @strawberry.federation.type class Query: hello: MyUnion schema = strawberry.federation.Schema(query=Query) expected = """ schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@inaccessible"]) { query: Query } type A { a: String! } type B { b: String! } type Query { _service: _Service! hello: Union! } union Union @inaccessible = A | B scalar _Any type _Service { sdl: String! } """ assert schema.as_str() == textwrap.dedent(expected).strip() strawberry-graphql-0.287.0/tests/federation/printer/test_interface.py000066400000000000000000000023511511033167500260510ustar00rootroot00000000000000import textwrap import strawberry def test_entities_extending_interface(): @strawberry.interface class SomeInterface: id: strawberry.ID @strawberry.federation.type(keys=["upc"], extend=True) class Product(SomeInterface): upc: str = strawberry.federation.field(external=True) @strawberry.federation.type class Query: @strawberry.field def top_products(self, first: int) -> list[Product]: # pragma: no cover return [] schema = strawberry.federation.Schema(query=Query) expected = """ schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@external", "@key"]) { query: Query } extend type Product implements SomeInterface @key(fields: "upc") { id: ID! upc: String! @external } type Query { _entities(representations: [_Any!]!): [_Entity]! _service: _Service! topProducts(first: Int!): [Product!]! } interface SomeInterface { id: ID! } scalar _Any union _Entity = Product type _Service { sdl: String! } """ assert schema.as_str() == textwrap.dedent(expected).strip() strawberry-graphql-0.287.0/tests/federation/printer/test_interface_object.py000066400000000000000000000014521511033167500274000ustar00rootroot00000000000000import textwrap import strawberry def test_interface_object(): @strawberry.federation.interface_object(keys=["id"]) class SomeInterface: id: strawberry.ID schema = strawberry.federation.Schema(types=[SomeInterface]) expected = """ schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@interfaceObject", "@key"]) { query: Query } type Query { _entities(representations: [_Any!]!): [_Entity]! _service: _Service! } type SomeInterface @key(fields: "id") @interfaceObject { id: ID! } scalar _Any union _Entity = SomeInterface type _Service { sdl: String! } """ assert schema.as_str() == textwrap.dedent(expected).strip() strawberry-graphql-0.287.0/tests/federation/printer/test_keys.py000066400000000000000000000031761511033167500250720ustar00rootroot00000000000000# type: ignore import textwrap import strawberry from strawberry.federation.schema_directives import Key def test_keys_federation_2(): global Review @strawberry.federation.type class User: username: str @strawberry.federation.type(keys=[Key(fields="upc", resolvable=True)], extend=True) class Product: upc: str = strawberry.federation.field(external=True) reviews: list["Review"] @strawberry.federation.type(keys=["body"]) class Review: body: str author: User product: Product @strawberry.federation.type class Query: @strawberry.field def top_products(self, first: int) -> list[Product]: # pragma: no cover return [] schema = strawberry.federation.Schema(query=Query) expected = """ schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@external", "@key"]) { query: Query } extend type Product @key(fields: "upc", resolvable: true) { upc: String! @external reviews: [Review!]! } type Query { _entities(representations: [_Any!]!): [_Entity]! _service: _Service! topProducts(first: Int!): [Product!]! } type Review @key(fields: "body") { body: String! author: User! product: Product! } type User { username: String! } scalar _Any union _Entity = Product | Review type _Service { sdl: String! } """ assert schema.as_str() == textwrap.dedent(expected).strip() del Review strawberry-graphql-0.287.0/tests/federation/printer/test_link.py000066400000000000000000000157471511033167500250630ustar00rootroot00000000000000import textwrap import strawberry from strawberry.federation.schema_directives import Link from tests.conftest import skip_if_gql_32 def test_link_directive(): @strawberry.type class Query: hello: str schema = strawberry.federation.Schema( query=Query, schema_directives=[ Link( url="https://specs.apollo.dev/link/v1.0", ) ], ) expected = """ schema @link(url: "https://specs.apollo.dev/link/v1.0") { query: Query } type Query { _service: _Service! hello: String! } scalar _Any type _Service { sdl: String! } """ assert schema.as_str() == textwrap.dedent(expected).strip() @skip_if_gql_32("formatting is different in gql 3.2") def test_link_directive_imports(): @strawberry.type class Query: hello: str schema = strawberry.federation.Schema( query=Query, schema_directives=[ Link( url="https://specs.apollo.dev/federation/v2.11", import_=[ "@key", "@requires", "@provides", "@external", {"name": "@tag", "as": "@mytag"}, "@extends", "@shareable", "@inaccessible", "@override", ], ) ], ) expected = """ schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: [ "@key" "@requires" "@provides" "@external" { name: "@tag", as: "@mytag" } "@extends" "@shareable" "@inaccessible" "@override" ]) { query: Query } type Query { _service: _Service! hello: String! } scalar _Any type _Service { sdl: String! } """ assert schema.as_str() == textwrap.dedent(expected).strip() def test_adds_link_directive_automatically(): @strawberry.federation.type(keys=["id"]) class User: id: strawberry.ID @strawberry.type class Query: user: User schema = strawberry.federation.Schema(query=Query) expected = """ schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@key"]) { query: Query } type Query { _entities(representations: [_Any!]!): [_Entity]! _service: _Service! user: User! } type User @key(fields: "id") { id: ID! } scalar _Any union _Entity = User type _Service { sdl: String! } """ assert schema.as_str() == textwrap.dedent(expected).strip() def test_adds_link_directive_from_interface(): @strawberry.federation.interface(keys=["id"]) class SomeInterface: id: strawberry.ID @strawberry.type class User: id: strawberry.ID @strawberry.type class Query: user: User schema = strawberry.federation.Schema(query=Query, types=[SomeInterface]) expected = """ schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@key"]) { query: Query } type Query { _service: _Service! user: User! } interface SomeInterface @key(fields: "id") { id: ID! } type User { id: ID! } scalar _Any type _Service { sdl: String! } """ assert schema.as_str() == textwrap.dedent(expected).strip() def test_adds_link_directive_from_input_types(): @strawberry.federation.input(inaccessible=True) class SomeInput: id: strawberry.ID @strawberry.type class User: id: strawberry.ID @strawberry.type class Query: user: User schema = strawberry.federation.Schema(query=Query, types=[SomeInput]) expected = """ schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@inaccessible"]) { query: Query } type Query { _service: _Service! user: User! } input SomeInput @inaccessible { id: ID! } type User { id: ID! } scalar _Any type _Service { sdl: String! } """ assert schema.as_str() == textwrap.dedent(expected).strip() def test_adds_link_directive_automatically_from_field(): @strawberry.federation.type(keys=["id"]) class User: id: strawberry.ID age: int = strawberry.federation.field(tags=["private"]) @strawberry.type class Query: user: User schema = strawberry.federation.Schema(query=Query) expected = """ schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@key", "@tag"]) { query: Query } type Query { _entities(representations: [_Any!]!): [_Entity]! _service: _Service! user: User! } type User @key(fields: "id") { id: ID! age: Int! @tag(name: "private") } scalar _Any union _Entity = User type _Service { sdl: String! } """ assert schema.as_str() == textwrap.dedent(expected).strip() def test_adds_directive_link_for_federation(): @strawberry.federation.type(keys=["id"]) class User: id: strawberry.ID @strawberry.type class Query: user: User schema = strawberry.federation.Schema(query=Query) expected = """ schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@key"]) { query: Query } type Query { _entities(representations: [_Any!]!): [_Entity]! _service: _Service! user: User! } type User @key(fields: "id") { id: ID! } scalar _Any union _Entity = User type _Service { sdl: String! } """ assert schema.as_str() == textwrap.dedent(expected).strip() def test_adds_link_directive_automatically_from_scalar(): # TODO: Federation scalar @strawberry.scalar class X: pass @strawberry.federation.type(keys=["id"]) class User: id: strawberry.ID age: X @strawberry.type class Query: user: User schema = strawberry.federation.Schema(query=Query) expected = """ schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@key"]) { query: Query } type Query { _entities(representations: [_Any!]!): [_Entity]! _service: _Service! user: User! } type User @key(fields: "id") { id: ID! age: X! } scalar X scalar _Any union _Entity = User type _Service { sdl: String! } """ assert schema.as_str() == textwrap.dedent(expected).strip() strawberry-graphql-0.287.0/tests/federation/printer/test_one_of.py000066400000000000000000000016471511033167500253650ustar00rootroot00000000000000import textwrap import strawberry def test_prints_one_of_directive(): @strawberry.federation.input(one_of=True, tags=["myTag", "anotherTag"]) class Input: a: str | None = strawberry.UNSET b: int | None = strawberry.UNSET @strawberry.federation.type class Query: hello: str schema = strawberry.federation.Schema(query=Query, types=[Input]) expected = """ directive @oneOf on INPUT_OBJECT schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@tag"]) { query: Query } input Input @tag(name: "myTag") @tag(name: "anotherTag") @oneOf { a: String b: Int } type Query { _service: _Service! hello: String! } scalar _Any type _Service { sdl: String! } """ assert schema.as_str() == textwrap.dedent(expected).strip() strawberry-graphql-0.287.0/tests/federation/printer/test_override.py000066400000000000000000000053621511033167500257350ustar00rootroot00000000000000# type: ignore import textwrap import strawberry from strawberry.federation.schema_directives import Override def test_field_override_printed_correctly(): @strawberry.interface class SomeInterface: id: strawberry.ID @strawberry.federation.type(keys=["upc"], extend=True) class Product(SomeInterface): upc: str = strawberry.federation.field(external=True, override="mySubGraph") @strawberry.federation.type class Query: @strawberry.field def top_products(self, first: int) -> list[Product]: # pragma: no cover return [] schema = strawberry.federation.Schema(query=Query) expected = """ schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@external", "@key", "@override"]) { query: Query } extend type Product implements SomeInterface @key(fields: "upc") { id: ID! upc: String! @external @override(from: "mySubGraph") } type Query { _entities(representations: [_Any!]!): [_Entity]! _service: _Service! topProducts(first: Int!): [Product!]! } interface SomeInterface { id: ID! } scalar _Any union _Entity = Product type _Service { sdl: String! } """ assert schema.as_str() == textwrap.dedent(expected).strip() def test_field_override_label_printed_correctly(): @strawberry.interface class SomeInterface: id: strawberry.ID @strawberry.federation.type(keys=["upc"], extend=True) class Product(SomeInterface): upc: str = strawberry.federation.field( external=True, override=Override(override_from="mySubGraph", label="percent(1)"), ) @strawberry.federation.type class Query: @strawberry.field def top_products(self, first: int) -> list[Product]: # pragma: no cover return [] schema = strawberry.federation.Schema(query=Query) expected = """ schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@external", "@key", "@override"]) { query: Query } extend type Product implements SomeInterface @key(fields: "upc") { id: ID! upc: String! @external @override(from: "mySubGraph", label: "percent(1)") } type Query { _entities(representations: [_Any!]!): [_Entity]! _service: _Service! topProducts(first: Int!): [Product!]! } interface SomeInterface { id: ID! } scalar _Any union _Entity = Product type _Service { sdl: String! } """ assert schema.as_str() == textwrap.dedent(expected).strip() strawberry-graphql-0.287.0/tests/federation/printer/test_policy.py000066400000000000000000000065421511033167500254160ustar00rootroot00000000000000import textwrap from enum import Enum from typing import Annotated import strawberry def test_field_policy_printed_correctly(): @strawberry.federation.interface( policy=[["client", "poweruser"], ["admin"], ["productowner"]] ) class SomeInterface: id: strawberry.ID @strawberry.federation.type( policy=[["client", "poweruser"], ["admin"], ["productowner"]] ) class Product(SomeInterface): upc: str = strawberry.federation.field(policy=[["productowner"]]) @strawberry.federation.type class Query: @strawberry.federation.field( policy=[["client", "poweruser"], ["admin"], ["productowner"]] ) def top_products( self, first: Annotated[int, strawberry.federation.argument()] ) -> list[Product]: # pragma: no cover return [] schema = strawberry.federation.Schema(query=Query) expected = """ schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@policy"]) { query: Query } type Product implements SomeInterface @policy(policies: [["client", "poweruser"], ["admin"], ["productowner"]]) { id: ID! upc: String! @policy(policies: [["productowner"]]) } type Query { _service: _Service! topProducts(first: Int!): [Product!]! @policy(policies: [["client", "poweruser"], ["admin"], ["productowner"]]) } interface SomeInterface @policy(policies: [["client", "poweruser"], ["admin"], ["productowner"]]) { id: ID! } scalar _Any type _Service { sdl: String! } """ assert schema.as_str() == textwrap.dedent(expected).strip() def test_field_policy_printed_correctly_on_scalar(): @strawberry.federation.scalar( policy=[["client", "poweruser"], ["admin"], ["productowner"]] ) class SomeScalar(str): __slots__ = () @strawberry.federation.type class Query: hello: SomeScalar schema = strawberry.federation.Schema(query=Query) expected = """ schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@policy"]) { query: Query } type Query { _service: _Service! hello: SomeScalar! } scalar SomeScalar @policy(policies: [["client", "poweruser"], ["admin"], ["productowner"]]) scalar _Any type _Service { sdl: String! } """ assert schema.as_str() == textwrap.dedent(expected).strip() def test_field_policy_printed_correctly_on_enum(): @strawberry.federation.enum( policy=[["client", "poweruser"], ["admin"], ["productowner"]] ) class SomeEnum(Enum): A = "A" @strawberry.federation.type class Query: hello: SomeEnum schema = strawberry.federation.Schema(query=Query) expected = """ schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@policy"]) { query: Query } type Query { _service: _Service! hello: SomeEnum! } enum SomeEnum @policy(policies: [["client", "poweruser"], ["admin"], ["productowner"]]) { A } scalar _Any type _Service { sdl: String! } """ assert schema.as_str() == textwrap.dedent(expected).strip() strawberry-graphql-0.287.0/tests/federation/printer/test_provides.py000066400000000000000000000070511511033167500257460ustar00rootroot00000000000000# type: ignore import textwrap import strawberry from strawberry.schema.config import StrawberryConfig def test_field_provides_are_printed_correctly_camel_case_on(): global Review @strawberry.federation.type class User: username: str @strawberry.federation.type(keys=["upc"], extend=True) class Product: upc: str = strawberry.federation.field(external=True) the_name: str = strawberry.federation.field(external=True) reviews: list["Review"] @strawberry.federation.type class Review: body: str author: User product: Product = strawberry.federation.field(provides=["name"]) @strawberry.federation.type class Query: @strawberry.field def top_products(self, first: int) -> list[Product]: # pragma: no cover return [] schema = strawberry.federation.Schema( query=Query, config=StrawberryConfig(auto_camel_case=True), ) expected = """ schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@external", "@key", "@provides"]) { query: Query } extend type Product @key(fields: "upc") { upc: String! @external theName: String! @external reviews: [Review!]! } type Query { _entities(representations: [_Any!]!): [_Entity]! _service: _Service! topProducts(first: Int!): [Product!]! } type Review { body: String! author: User! product: Product! @provides(fields: "name") } type User { username: String! } scalar _Any union _Entity = Product type _Service { sdl: String! } """ assert schema.as_str() == textwrap.dedent(expected).strip() del Review def test_field_provides_are_printed_correctly_camel_case_off(): global Review @strawberry.federation.type class User: username: str @strawberry.federation.type(keys=["upc"], extend=True) class Product: upc: str = strawberry.federation.field(external=True) the_name: str = strawberry.federation.field(external=True) reviews: list["Review"] @strawberry.federation.type class Review: body: str author: User product: Product = strawberry.federation.field(provides=["name"]) @strawberry.federation.type class Query: @strawberry.field def top_products(self, first: int) -> list[Product]: # pragma: no cover return [] schema = strawberry.federation.Schema( query=Query, config=StrawberryConfig(auto_camel_case=False), ) expected = """ schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@external", "@key", "@provides"]) { query: Query } extend type Product @key(fields: "upc") { upc: String! @external the_name: String! @external reviews: [Review!]! } type Query { _entities(representations: [_Any!]!): [_Entity]! _service: _Service! top_products(first: Int!): [Product!]! } type Review { body: String! author: User! product: Product! @provides(fields: "name") } type User { username: String! } scalar _Any union _Entity = Product type _Service { sdl: String! } """ assert schema.as_str() == textwrap.dedent(expected).strip() del Review strawberry-graphql-0.287.0/tests/federation/printer/test_requires.py000066400000000000000000000037261511033167500257570ustar00rootroot00000000000000# type: ignore import textwrap import strawberry def test_fields_requires_are_printed_correctly(): global Review @strawberry.federation.type class User: username: str @strawberry.federation.type(keys=["upc"], extend=True) class Product: upc: str = strawberry.federation.field(external=True) field1: str = strawberry.federation.field(external=True) field2: str = strawberry.federation.field(external=True) field3: str = strawberry.federation.field(external=True) @strawberry.federation.field(requires=["field1", "field2", "field3"]) def reviews(self) -> list["Review"]: return [] @strawberry.federation.type class Review: body: str author: User product: Product @strawberry.federation.type class Query: @strawberry.field def top_products(self, first: int) -> list[Product]: # pragma: no cover return [] schema = strawberry.federation.Schema(query=Query) expected = """ schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@external", "@key", "@requires"]) { query: Query } extend type Product @key(fields: "upc") { upc: String! @external field1: String! @external field2: String! @external field3: String! @external reviews: [Review!]! @requires(fields: "field1 field2 field3") } type Query { _entities(representations: [_Any!]!): [_Entity]! _service: _Service! topProducts(first: Int!): [Product!]! } type Review { body: String! author: User! product: Product! } type User { username: String! } scalar _Any union _Entity = Product type _Service { sdl: String! } """ assert schema.as_str() == textwrap.dedent(expected).strip() del Review strawberry-graphql-0.287.0/tests/federation/printer/test_requires_scopes.py000066400000000000000000000067571511033167500273420ustar00rootroot00000000000000import textwrap from enum import Enum from typing import Annotated import strawberry def test_field_requires_scopes_printed_correctly(): @strawberry.federation.interface( requires_scopes=[["client", "poweruser"], ["admin"], ["productowner"]] ) class SomeInterface: id: strawberry.ID @strawberry.federation.type( requires_scopes=[["client", "poweruser"], ["admin"], ["productowner"]] ) class Product(SomeInterface): upc: str = strawberry.federation.field(requires_scopes=[["productowner"]]) @strawberry.federation.type class Query: @strawberry.federation.field( requires_scopes=[["client", "poweruser"], ["admin"], ["productowner"]] ) def top_products( self, first: Annotated[int, strawberry.federation.argument()] ) -> list[Product]: # pragma: no cover return [] schema = strawberry.federation.Schema(query=Query) expected = """ schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@requiresScopes"]) { query: Query } type Product implements SomeInterface @requiresScopes(scopes: [["client", "poweruser"], ["admin"], ["productowner"]]) { id: ID! upc: String! @requiresScopes(scopes: [["productowner"]]) } type Query { _service: _Service! topProducts(first: Int!): [Product!]! @requiresScopes(scopes: [["client", "poweruser"], ["admin"], ["productowner"]]) } interface SomeInterface @requiresScopes(scopes: [["client", "poweruser"], ["admin"], ["productowner"]]) { id: ID! } scalar _Any type _Service { sdl: String! } """ assert schema.as_str() == textwrap.dedent(expected).strip() def test_field_requires_scopes_printed_correctly_on_scalar(): @strawberry.federation.scalar( requires_scopes=[["client", "poweruser"], ["admin"], ["productowner"]] ) class SomeScalar(str): __slots__ = () @strawberry.federation.type class Query: hello: SomeScalar schema = strawberry.federation.Schema(query=Query) expected = """ schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@requiresScopes"]) { query: Query } type Query { _service: _Service! hello: SomeScalar! } scalar SomeScalar @requiresScopes(scopes: [["client", "poweruser"], ["admin"], ["productowner"]]) scalar _Any type _Service { sdl: String! } """ assert schema.as_str() == textwrap.dedent(expected).strip() def test_field_requires_scopes_printed_correctly_on_enum(): @strawberry.federation.enum( requires_scopes=[["client", "poweruser"], ["admin"], ["productowner"]] ) class SomeEnum(Enum): A = "A" @strawberry.federation.type class Query: hello: SomeEnum schema = strawberry.federation.Schema(query=Query) expected = """ schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@requiresScopes"]) { query: Query } type Query { _service: _Service! hello: SomeEnum! } enum SomeEnum @requiresScopes(scopes: [["client", "poweruser"], ["admin"], ["productowner"]]) { A } scalar _Any type _Service { sdl: String! } """ assert schema.as_str() == textwrap.dedent(expected).strip() strawberry-graphql-0.287.0/tests/federation/printer/test_shareable.py000066400000000000000000000025021511033167500260350ustar00rootroot00000000000000# type: ignore import textwrap import strawberry def test_field_shareable_printed_correctly(): @strawberry.interface class SomeInterface: id: strawberry.ID @strawberry.federation.type(keys=["upc"], extend=True, shareable=True) class Product(SomeInterface): upc: str = strawberry.federation.field(external=True, shareable=True) @strawberry.federation.type class Query: @strawberry.field def top_products(self, first: int) -> list[Product]: # pragma: no cover return [] schema = strawberry.federation.Schema(query=Query) expected = """ schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@external", "@key", "@shareable"]) { query: Query } extend type Product implements SomeInterface @key(fields: "upc") @shareable { id: ID! upc: String! @external @shareable } type Query { _entities(representations: [_Any!]!): [_Entity]! _service: _Service! topProducts(first: Int!): [Product!]! } interface SomeInterface { id: ID! } scalar _Any union _Entity = Product type _Service { sdl: String! } """ assert schema.as_str() == textwrap.dedent(expected).strip() strawberry-graphql-0.287.0/tests/federation/printer/test_tag.py000066400000000000000000000130131511033167500246610ustar00rootroot00000000000000import textwrap from enum import Enum from typing import Annotated import strawberry def test_field_tag_printed_correctly(): @strawberry.federation.interface(tags=["myTag", "anotherTag"]) class SomeInterface: id: strawberry.ID @strawberry.federation.type(tags=["myTag", "anotherTag"]) class Product(SomeInterface): upc: str = strawberry.federation.field( external=True, tags=["myTag", "anotherTag"] ) @strawberry.federation.type class Query: @strawberry.field def top_products( self, first: Annotated[int, strawberry.federation.argument(tags=["myTag"])] ) -> list[Product]: # pragma: no cover return [] schema = strawberry.federation.Schema(query=Query) expected = """ schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@external", "@tag"]) { query: Query } type Product implements SomeInterface @tag(name: "myTag") @tag(name: "anotherTag") { id: ID! upc: String! @external @tag(name: "myTag") @tag(name: "anotherTag") } type Query { _service: _Service! topProducts(first: Int! @tag(name: "myTag")): [Product!]! } interface SomeInterface @tag(name: "myTag") @tag(name: "anotherTag") { id: ID! } scalar _Any type _Service { sdl: String! } """ assert schema.as_str() == textwrap.dedent(expected).strip() def test_field_tag_printed_correctly_on_scalar(): @strawberry.federation.scalar(tags=["myTag", "anotherTag"]) class SomeScalar(str): __slots__ = () @strawberry.federation.type class Query: hello: SomeScalar schema = strawberry.federation.Schema(query=Query) expected = """ schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@tag"]) { query: Query } type Query { _service: _Service! hello: SomeScalar! } scalar SomeScalar @tag(name: "myTag") @tag(name: "anotherTag") scalar _Any type _Service { sdl: String! } """ assert schema.as_str() == textwrap.dedent(expected).strip() def test_field_tag_printed_correctly_on_enum(): @strawberry.federation.enum(tags=["myTag", "anotherTag"]) class SomeEnum(Enum): A = "A" @strawberry.federation.type class Query: hello: SomeEnum schema = strawberry.federation.Schema(query=Query) expected = """ schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@tag"]) { query: Query } type Query { _service: _Service! hello: SomeEnum! } enum SomeEnum @tag(name: "myTag") @tag(name: "anotherTag") { A } scalar _Any type _Service { sdl: String! } """ assert schema.as_str() == textwrap.dedent(expected).strip() def test_field_tag_printed_correctly_on_enum_value(): @strawberry.enum class SomeEnum(Enum): A = strawberry.federation.enum_value("A", tags=["myTag", "anotherTag"]) @strawberry.federation.type class Query: hello: SomeEnum schema = strawberry.federation.Schema(query=Query) expected = """ schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@tag"]) { query: Query } type Query { _service: _Service! hello: SomeEnum! } enum SomeEnum { A @tag(name: "myTag") @tag(name: "anotherTag") } scalar _Any type _Service { sdl: String! } """ assert schema.as_str() == textwrap.dedent(expected).strip() def test_field_tag_printed_correctly_on_union(): @strawberry.type class A: a: str @strawberry.type class B: b: str MyUnion = Annotated[ A | B, strawberry.federation.union("Union", tags=["myTag", "anotherTag"]) ] @strawberry.federation.type class Query: hello: MyUnion schema = strawberry.federation.Schema(query=Query) expected = """ schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@tag"]) { query: Query } type A { a: String! } type B { b: String! } type Query { _service: _Service! hello: Union! } union Union @tag(name: "myTag") @tag(name: "anotherTag") = A | B scalar _Any type _Service { sdl: String! } """ assert schema.as_str() == textwrap.dedent(expected).strip() def test_tag_printed_correctly_on_inputs(): @strawberry.federation.input(tags=["myTag", "anotherTag"]) class Input: a: str = strawberry.federation.field(tags=["myTag", "anotherTag"]) @strawberry.federation.type class Query: hello: str schema = strawberry.federation.Schema(query=Query, types=[Input]) expected = """ schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@tag"]) { query: Query } input Input @tag(name: "myTag") @tag(name: "anotherTag") { a: String! @tag(name: "myTag") @tag(name: "anotherTag") } type Query { _service: _Service! hello: String! } scalar _Any type _Service { sdl: String! } """ assert schema.as_str() == textwrap.dedent(expected).strip() strawberry-graphql-0.287.0/tests/federation/test_entities.py000066400000000000000000000312251511033167500242540ustar00rootroot00000000000000from graphql import located_error import strawberry from strawberry.types import Info def test_fetch_entities(): @strawberry.federation.type(keys=["upc"]) class Product: upc: str @classmethod def resolve_reference(cls, upc) -> "Product": return Product(upc=upc) @strawberry.federation.type(extend=True) class Query: @strawberry.field def top_products(self, first: int) -> list[Product]: # pragma: no cover return [] schema = strawberry.federation.Schema(query=Query) query = """ query ($representations: [_Any!]!) { _entities(representations: $representations) { ... on Product { upc } } } """ result = schema.execute_sync( query, variable_values={ "representations": [{"__typename": "Product", "upc": "B00005N5PF"}] }, ) assert not result.errors assert result.data == {"_entities": [{"upc": "B00005N5PF"}]} def test_info_param_in_resolve_reference(): @strawberry.federation.type(keys=["upc"]) class Product: upc: str debug_field_name: str @classmethod def resolve_reference(cls, info: strawberry.Info, upc: str) -> "Product": return Product(upc=upc, debug_field_name=info.field_name) @strawberry.federation.type(extend=True) class Query: @strawberry.field def top_products(self, first: int) -> list[Product]: # pragma: no cover return [] schema = strawberry.federation.Schema(query=Query) query = """ query ($representations: [_Any!]!) { _entities(representations: $representations) { ... on Product { upc debugFieldName } } } """ result = schema.execute_sync( query, variable_values={ "representations": [{"__typename": "Product", "upc": "B00005N5PF"}] }, ) assert not result.errors assert result.data == { "_entities": [ { "upc": "B00005N5PF", # _entities is the field that's called by federation "debugFieldName": "_entities", } ] } def test_does_not_need_custom_resolve_reference_for_basic_things(): @strawberry.federation.type(keys=["upc"]) class Product: upc: str @strawberry.federation.type(extend=True) class Query: @strawberry.field def top_products(self, first: int) -> list[Product]: # pragma: no cover return [] schema = strawberry.federation.Schema(query=Query) query = """ query ($representations: [_Any!]!) { _entities(representations: $representations) { ... on Product { upc } } } """ result = schema.execute_sync( query, variable_values={ "representations": [{"__typename": "Product", "upc": "B00005N5PF"}] }, ) assert not result.errors assert result.data == {"_entities": [{"upc": "B00005N5PF"}]} def test_does_not_need_custom_resolve_reference_nested(): @strawberry.federation.type(keys=["id"]) class Something: id: str @strawberry.federation.type(keys=["upc"]) class Product: upc: str something: Something @strawberry.federation.type(extend=True) class Query: @strawberry.field def top_products(self, first: int) -> list[Product]: # pragma: no cover return [] schema = strawberry.federation.Schema(query=Query) query = """ query ($representations: [_Any!]!) { _entities(representations: $representations) { ... on Product { upc something { id } } } } """ result = schema.execute_sync( query, variable_values={ "representations": [ {"__typename": "Product", "upc": "B00005N5PF", "something": {"id": "1"}} ] }, ) assert not result.errors assert result.data == { "_entities": [{"upc": "B00005N5PF", "something": {"id": "1"}}] } def test_fails_properly_when_wrong_key_is_passed(): @strawberry.type class Something: id: str @strawberry.federation.type(keys=["upc"]) class Product: upc: str something: Something @strawberry.federation.type(extend=True) class Query: @strawberry.field def top_products(self, first: int) -> list[Product]: # pragma: no cover return [] schema = strawberry.federation.Schema(query=Query) query = """ query ($representations: [_Any!]!) { _entities(representations: $representations) { ... on Product { upc something { id } } } } """ result = schema.execute_sync( query, variable_values={ "representations": [{"__typename": "Product", "not_upc": "B00005N5PF"}] }, ) assert result.errors assert result.errors[0].message == "Unable to resolve reference for Product" def test_fails_properly_when_wrong_data_is_passed(): @strawberry.federation.type(keys=["id"]) class Something: id: str @strawberry.federation.type(keys=["upc"]) class Product: upc: str something: Something @strawberry.federation.type(extend=True) class Query: @strawberry.field def top_products(self, first: int) -> list[Product]: # pragma: no cover return [] schema = strawberry.federation.Schema(query=Query) query = """ query ($representations: [_Any!]!) { _entities(representations: $representations) { ... on Product { upc something { id } } } } """ result = schema.execute_sync( query, variable_values={ "representations": [ { "__typename": "Product", "upc": "B00005N5PF", "not_something": {"id": "1"}, } ] }, ) assert result.errors assert result.errors[0].message == "Unable to resolve reference for Product" def test_propagates_original_error_message_with_auto_graphql_error_metadata(): @strawberry.federation.type(keys=["id"]) class Product: id: strawberry.ID @classmethod def resolve_reference(cls, id: strawberry.ID) -> "Product": raise Exception("Foo bar") @strawberry.federation.type(extend=True) class Query: @strawberry.field def mock(self) -> Product | None: return None schema = strawberry.federation.Schema(query=Query) query = """ query ($representations: [_Any!]!) { _entities(representations: $representations) { ... on Product { id } } } """ result = schema.execute_sync( query, variable_values={ "representations": [ { "__typename": "Product", "id": "B00005N5PF", } ] }, ) assert len(result.errors) == 1 error = result.errors[0].formatted assert error["message"] == "Foo bar" assert error["path"] == ["_entities", 0] assert error["locations"] == [{"column": 13, "line": 3}] assert "extensions" not in error def test_propagates_custom_type_error_message_with_auto_graphql_error_metadata(): class MyTypeError(TypeError): pass @strawberry.federation.type(keys=["id"]) class Product: id: strawberry.ID @classmethod def resolve_reference(cls, id: strawberry.ID) -> "Product": raise MyTypeError("Foo bar") @strawberry.federation.type(extend=True) class Query: @strawberry.field def mock(self) -> Product | None: return None schema = strawberry.federation.Schema(query=Query) query = """ query ($representations: [_Any!]!) { _entities(representations: $representations) { ... on Product { id } } } """ result = schema.execute_sync( query, variable_values={ "representations": [ { "__typename": "Product", "id": "B00005N5PF", } ] }, ) assert len(result.errors) == 1 error = result.errors[0].formatted assert error["message"] == "Foo bar" assert error["path"] == ["_entities", 0] assert error["locations"] == [{"column": 13, "line": 3}] assert "extensions" not in error def test_propagates_original_error_message_and_graphql_error_metadata(): @strawberry.federation.type(keys=["id"]) class Product: id: strawberry.ID @classmethod def resolve_reference(cls, info: Info, id: strawberry.ID) -> "Product": exception = Exception("Foo bar") exception.extensions = {"baz": "qux"} raise located_error( exception, nodes=info._raw_info.field_nodes[0], path=["_entities_override", 0], ) @strawberry.federation.type(extend=True) class Query: @strawberry.field def mock(self) -> Product | None: return None schema = strawberry.federation.Schema(query=Query) query = """ query ($representations: [_Any!]!) { _entities(representations: $representations) { ... on Product { id } } } """ result = schema.execute_sync( query, variable_values={ "representations": [ { "__typename": "Product", "id": "B00005N5PF", } ] }, ) assert len(result.errors) == 1 error = result.errors[0].formatted assert error["message"] == "Foo bar" assert error["path"] == ["_entities_override", 0] assert error["locations"] == [{"column": 13, "line": 3}] assert error["extensions"] == {"baz": "qux"} async def test_can_use_async_resolve_reference(): @strawberry.federation.type(keys=["upc"]) class Product: upc: str @classmethod async def resolve_reference(cls, upc: str) -> "Product": return Product(upc=upc) @strawberry.federation.type(extend=True) class Query: @strawberry.field def top_products(self, first: int) -> list[Product]: # pragma: no cover return [] schema = strawberry.federation.Schema(query=Query) query = """ query ($representations: [_Any!]!) { _entities(representations: $representations) { ... on Product { upc } } } """ result = await schema.execute( query, variable_values={ "representations": [{"__typename": "Product", "upc": "B00005N5PF"}] }, ) assert not result.errors assert result.data == {"_entities": [{"upc": "B00005N5PF"}]} async def test_can_use_async_resolve_reference_multiple_representations(): @strawberry.federation.type(keys=["upc"]) class Product: upc: str @classmethod async def resolve_reference(cls, upc: str) -> "Product": return Product(upc=upc) @strawberry.federation.type(extend=True) class Query: @strawberry.field def top_products(self, first: int) -> list[Product]: # pragma: no cover return [] schema = strawberry.federation.Schema(query=Query) query = """ query ($representations: [_Any!]!) { _entities(representations: $representations) { ... on Product { upc } } } """ result = await schema.execute( query, variable_values={ "representations": [ {"__typename": "Product", "upc": "B00005N5PF"}, {"__typename": "Product", "upc": "B00005N5PG"}, ] }, ) assert not result.errors assert result.data == {"_entities": [{"upc": "B00005N5PF"}, {"upc": "B00005N5PG"}]} strawberry-graphql-0.287.0/tests/federation/test_schema.py000066400000000000000000000174721511033167500237000ustar00rootroot00000000000000import textwrap import warnings from typing import Generic, TypeVar import pytest import strawberry def test_entities_type_when_no_type_has_keys(): @strawberry.federation.type() class Product: upc: str name: str | None price: int | None weight: int | None @strawberry.federation.type(extend=True) class Query: @strawberry.field def top_products(self, first: int) -> list[Product]: # pragma: no cover return [] schema = strawberry.federation.Schema(query=Query) expected_sdl = textwrap.dedent(""" type Product { upc: String! name: String price: Int weight: Int } extend type Query { _service: _Service! topProducts(first: Int!): [Product!]! } scalar _Any type _Service { sdl: String! } """).strip() assert str(schema) == expected_sdl query = """ query { __type(name: "_Entity") { kind possibleTypes { name } } } """ result = schema.execute_sync(query) assert not result.errors assert result.data == {"__type": None} def test_entities_type(): @strawberry.federation.type(keys=["upc"]) class Product: upc: str name: str | None price: int | None weight: int | None @strawberry.federation.type(extend=True) class Query: @strawberry.field def top_products(self, first: int) -> list[Product]: # pragma: no cover return [] schema = strawberry.federation.Schema(query=Query) expected_sdl = textwrap.dedent(""" schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@key"]) { query: Query } type Product @key(fields: "upc") { upc: String! name: String price: Int weight: Int } extend type Query { _entities(representations: [_Any!]!): [_Entity]! _service: _Service! topProducts(first: Int!): [Product!]! } scalar _Any union _Entity = Product type _Service { sdl: String! } """).strip() assert str(schema) == expected_sdl query = """ query { __type(name: "_Entity") { kind possibleTypes { name } } } """ result = schema.execute_sync(query) assert not result.errors assert result.data == { "__type": {"kind": "UNION", "possibleTypes": [{"name": "Product"}]} } def test_additional_scalars(): @strawberry.federation.type(keys=["upc"]) class Example: upc: str @strawberry.federation.type(extend=True) class Query: @strawberry.field def top_products(self, first: int) -> list[Example]: # pragma: no cover return [] schema = strawberry.federation.Schema(query=Query) query = """ query { __type(name: "_Any") { kind } } """ result = schema.execute_sync(query) assert not result.errors assert result.data == {"__type": {"kind": "SCALAR"}} def test_service(): @strawberry.federation.type class Product: upc: str @strawberry.federation.type(extend=True) class Query: @strawberry.field def top_products(self, first: int) -> list[Product]: # pragma: no cover return [] schema = strawberry.federation.Schema(query=Query) query = """ query { _service { sdl } } """ result = schema.execute_sync(query) assert not result.errors sdl = """ type Product { upc: String! } extend type Query { _service: _Service! topProducts(first: Int!): [Product!]! } scalar _Any type _Service { sdl: String! } """ assert result.data == {"_service": {"sdl": textwrap.dedent(sdl).strip()}} def test_using_generics(): T = TypeVar("T") @strawberry.federation.type class Product: upc: str @strawberry.type class ListOfProducts(Generic[T]): products: list[T] @strawberry.federation.type(extend=True) class Query: @strawberry.field def top_products( self, first: int ) -> ListOfProducts[Product]: # pragma: no cover return ListOfProducts(products=[]) schema = strawberry.federation.Schema(query=Query) query = """ query { _service { sdl } } """ result = schema.execute_sync(query) assert not result.errors sdl = """ type Product { upc: String! } type ProductListOfProducts { products: [Product!]! } extend type Query { _service: _Service! topProducts(first: Int!): ProductListOfProducts! } scalar _Any type _Service { sdl: String! } """ assert result.data == {"_service": {"sdl": textwrap.dedent(sdl).strip()}} def test_input_types(): @strawberry.federation.input(inaccessible=True) class ExampleInput: upc: str @strawberry.federation.type(extend=True) class Query: @strawberry.field def top_products(self, example: ExampleInput) -> list[str]: # pragma: no cover return [] schema = strawberry.federation.Schema(query=Query) query = """ query { __type(name: "ExampleInput") { kind } } """ result = schema.execute_sync(query) assert not result.errors assert result.data == {"__type": {"kind": "INPUT_OBJECT"}} def test_can_create_schema_without_query(): @strawberry.federation.type() class Product: upc: str name: str | None price: int | None weight: int | None schema = strawberry.federation.Schema(types=[Product]) assert ( str(schema) == textwrap.dedent( """ type Product { upc: String! name: String price: Int weight: Int } type Query { _service: _Service! } scalar _Any type _Service { sdl: String! } """ ).strip() ) def test_federation_schema_warning(): @strawberry.federation.type(keys=["upc"]) class ProductFed: upc: str name: str | None price: int | None weight: int | None with pytest.warns(UserWarning) as record: # noqa: PT030 strawberry.Schema( query=ProductFed, ) assert ( "Federation directive found in schema. " "Use `strawberry.federation.Schema` instead of `strawberry.Schema`." in [str(r.message) for r in record] ) def test_does_not_warn_when_using_federation_schema(): @strawberry.federation.type(keys=["upc"]) class ProductFed: upc: str name: str | None price: int | None weight: int | None @strawberry.type class Query: @strawberry.field def top_products(self, first: int) -> list[ProductFed]: # pragma: no cover return [] with warnings.catch_warnings(record=True) as w: warnings.filterwarnings( "ignore", category=DeprecationWarning, message=r"'.*' is deprecated and slated for removal in Python 3\.\d+", ) strawberry.federation.Schema( query=Query, ) assert len(w) == 0 strawberry-graphql-0.287.0/tests/federation/test_types.py000066400000000000000000000015041511033167500235710ustar00rootroot00000000000000import strawberry def test_type(): @strawberry.federation.type(keys=["id"]) class Location: id: strawberry.ID assert Location(id=strawberry.ID("1")).id == "1" def test_type_and_override(): @strawberry.federation.type(keys=["id"]) class Location: id: strawberry.ID address: str = strawberry.federation.field(override="start") location = Location(id=strawberry.ID("1"), address="ABC") assert location.id == "1" assert location.address == "ABC" def test_type_and_override_with_resolver(): @strawberry.federation.type(keys=["id"]) class Location: id: strawberry.ID address: str = strawberry.federation.field( override="start", resolver=lambda: "ABC" ) location = Location(id=strawberry.ID("1")) assert location.id == "1" strawberry-graphql-0.287.0/tests/federation/test_version_validation.py000066400000000000000000000116701511033167500263310ustar00rootroot00000000000000import pytest import strawberry from strawberry.federation.schema_directives import Context, Cost, ListSize, Policy @pytest.mark.parametrize( ("directive_name", "minimum_version", "valid_version", "invalid_version"), [ ("policy", "2.6", "2.6", "2.5"), ("context", "2.8", "2.8", "2.7"), ("cost", "2.9", "2.9", "2.8"), ("listSize", "2.9", "2.9", "2.8"), ], ) def test_directive_version_validation( directive_name: str, minimum_version: str, valid_version: str, invalid_version: str, ): """Test that directives validate their minimum federation version requirement""" # Create type with directive based on directive name if directive_name == "policy": @strawberry.federation.type class ProductPolicy: upc: str name: str = strawberry.federation.field( directives=[Policy(policies=[["admin"]])] ) @strawberry.type class QueryPolicy: product: ProductPolicy query_type = QueryPolicy elif directive_name == "context": @strawberry.federation.type(directives=[Context(name="userContext")]) class UserContext: id: strawberry.ID name: str @strawberry.type class QueryContext: user: UserContext query_type = QueryContext elif directive_name == "cost": @strawberry.federation.type class ProductCost: upc: str name: str = strawberry.federation.field(directives=[Cost(weight=10)]) @strawberry.type class QueryCost: product: ProductCost query_type = QueryCost else: # listSize @strawberry.federation.type class ProductListSize: upc: str friends: list[str] = strawberry.federation.field( directives=[ ListSize( assumed_size=100, slicing_arguments=None, sized_fields=None, ) ] ) @strawberry.type class QueryListSize: product: ProductListSize query_type = QueryListSize # Should work with valid version schema = strawberry.federation.Schema( query=query_type, federation_version=valid_version, # type: ignore ) assert schema is not None # Should fail with invalid version escaped_version = minimum_version.replace(".", r"\.") expected_error = ( f"Directive @{directive_name} requires federation version " f"v{escaped_version} or higher" ) with pytest.raises(ValueError, match=expected_error): strawberry.federation.Schema( query=query_type, federation_version=invalid_version, # type: ignore ) def test_directive_version_validation_multiple_directives(): """Test validation with multiple directives having different version requirements""" @strawberry.federation.type(directives=[Context(name="ctx")]) class Product: upc: str name: str = strawberry.federation.field( directives=[Policy(policies=[["admin"]]), Cost(weight=5)] ) @strawberry.type class Query: product: Product # Should work with v2.9+ (highest requirement among @context v2.8, @policy v2.6, @cost v2.9) schema = strawberry.federation.Schema(query=Query, federation_version="2.9") assert schema is not None # Should fail with v2.8 (cost requires v2.9) with pytest.raises( ValueError, match=r"Directive @cost requires federation version v2\.9 or higher" ): strawberry.federation.Schema(query=Query, federation_version="2.8") # Should fail with v2.7 (context requires v2.8) with pytest.raises( ValueError, match=r"Directive @context requires federation version v2\.8 or higher", ): strawberry.federation.Schema(query=Query, federation_version="2.7") def test_older_directives_work_with_any_version(): """Test that older directives work with any federation version""" @strawberry.federation.type(keys=["id"]) class Product: id: strawberry.ID name: str = strawberry.federation.field(external=True) @strawberry.type class Query: product: Product # Should work with any version since @key and @external are from v2.0 for version in ["2.0", "2.5", "2.7", "2.9", "2.11"]: schema = strawberry.federation.Schema(query=Query, federation_version=version) # type: ignore assert schema is not None def test_default_version_uses_latest(): """Test that default federation version uses latest""" @strawberry.federation.type class Product: upc: str @strawberry.type class Query: product: Product # Default should use latest version schema = strawberry.federation.Schema(query=Query) assert schema.federation_version == (2, 11) # Latest version strawberry-graphql-0.287.0/tests/fields/000077500000000000000000000000001511033167500201425ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/fields/__init__.py000066400000000000000000000000001511033167500222410ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/fields/test_arguments.py000066400000000000000000000332571511033167500235720ustar00rootroot00000000000000from typing import Annotated import pytest import strawberry from strawberry import UNSET from strawberry.exceptions import ( InvalidArgumentTypeError, MultipleStrawberryArgumentsError, ) from strawberry.types.base import StrawberryList, StrawberryOptional def test_basic_arguments(): @strawberry.type class Query: @strawberry.field def name( self, argument: str, optional_argument: str | None ) -> str: # pragma: no cover return "Name" definition = Query.__strawberry_definition__ assert definition.name == "Query" [argument1, argument2] = definition.fields[0].arguments assert argument1.python_name == "argument" assert argument1.graphql_name is None assert argument1.type is str assert argument2.python_name == "optional_argument" assert argument2.graphql_name is None assert isinstance(argument2.type, StrawberryOptional) assert argument2.type.of_type is str def test_input_type_as_argument(): @strawberry.input class Input: name: str @strawberry.type class Query: @strawberry.field def name( self, input: Input, optional_input: Input | None ) -> str: # pragma: no cover return input.name definition = Query.__strawberry_definition__ assert definition.name == "Query" [argument1, argument2] = definition.fields[0].arguments assert argument1.python_name == "input" assert argument1.graphql_name is None assert argument1.type is Input assert argument2.python_name == "optional_input" assert argument2.graphql_name is None assert isinstance(argument2.type, StrawberryOptional) assert argument2.type.of_type is Input def test_arguments_lists(): @strawberry.input class Input: name: str @strawberry.type class Query: @strawberry.field def names(self, inputs: list[Input]) -> list[str]: # pragma: no cover return [input.name for input in inputs] definition = Query.__strawberry_definition__ assert definition.name == "Query" [argument] = definition.fields[0].arguments assert argument.python_name == "inputs" assert argument.graphql_name is None assert isinstance(argument.type, StrawberryList) assert argument.type.of_type is Input def test_arguments_lists_of_optionals(): @strawberry.input class Input: name: str @strawberry.type class Query: @strawberry.field def names(self, inputs: list[Input | None]) -> list[str]: # pragma: no cover return [input_.name for input_ in inputs if input_ is not None] definition = Query.__strawberry_definition__ assert definition.name == "Query" [argument] = definition.fields[0].arguments assert argument.python_name == "inputs" assert argument.graphql_name is None assert isinstance(argument.type, StrawberryList) assert isinstance(argument.type.of_type, StrawberryOptional) assert argument.type.of_type.of_type is Input def test_basic_arguments_on_resolver(): def name_resolver( # pragma: no cover id: strawberry.ID, argument: str, optional_argument: str | None ) -> str: return "Name" @strawberry.type class Query: name: str = strawberry.field(resolver=name_resolver) definition = Query.__strawberry_definition__ assert definition.name == "Query" [argument1, argument2, argument3] = definition.fields[0].arguments assert argument1.python_name == "id" assert argument1.type is strawberry.ID assert argument2.python_name == "argument" assert argument2.type is str assert argument3.python_name == "optional_argument" assert isinstance(argument3.type, StrawberryOptional) assert argument3.type.of_type is str def test_arguments_when_extending_a_type(): def name_resolver( id: strawberry.ID, argument: str, optional_argument: str | None ) -> str: # pragma: no cover return "Name" @strawberry.type class NameQuery: name: str = strawberry.field(resolver=name_resolver) @strawberry.type class Query(NameQuery): pass definition = Query.__strawberry_definition__ assert definition.name == "Query" assert len(definition.fields) == 1 [argument1, argument2, argument3] = definition.fields[0].arguments assert argument1.python_name == "id" assert argument1.type is strawberry.ID assert argument2.python_name == "argument" assert argument2.type is str assert argument3.python_name == "optional_argument" assert isinstance(argument3.type, StrawberryOptional) assert argument3.type.of_type is str def test_arguments_when_extending_multiple_types(): def name_resolver(id: strawberry.ID) -> str: # pragma: no cover return "Name" def name_2_resolver(id: strawberry.ID) -> str: # pragma: no cover return "Name 2" @strawberry.type class NameQuery: name: str = strawberry.field(permission_classes=[], resolver=name_resolver) @strawberry.type class ExampleQuery: name_2: str = strawberry.field(permission_classes=[], resolver=name_2_resolver) @strawberry.type class RootQuery(NameQuery, ExampleQuery): pass definition = RootQuery.__strawberry_definition__ assert definition.name == "RootQuery" assert len(definition.fields) == 2 [argument1] = definition.fields[0].arguments assert argument1.python_name == "id" assert argument1.graphql_name is None assert argument1.type is strawberry.ID [argument2] = definition.fields[1].arguments assert argument2.python_name == "id" assert argument2.graphql_name is None assert argument2.type is strawberry.ID def test_argument_with_default_value_none(): @strawberry.type class Query: @strawberry.field def name(self, argument: str | None = None) -> str: # pragma: no cover return "Name" definition = Query.__strawberry_definition__ assert definition.name == "Query" [argument] = definition.fields[0].arguments assert argument.python_name == "argument" assert argument.graphql_name is None assert argument.default is None assert argument.description is None assert isinstance(argument.type, StrawberryOptional) assert argument.type.of_type is str def test_argument_with_default_value_undefined(): @strawberry.type class Query: @strawberry.field def name(self, argument: str | None) -> str: # pragma: no cover return "Name" definition = Query.__strawberry_definition__ assert definition.name == "Query" [argument] = definition.fields[0].arguments assert argument.python_name == "argument" assert argument.graphql_name is None assert argument.default is UNSET assert argument.description is None assert isinstance(argument.type, StrawberryOptional) assert argument.type.of_type is str def test_annotated_argument_on_resolver(): @strawberry.type class Query: @strawberry.field def name( # type: ignore argument: Annotated[ str, strawberry.argument(description="This is a description"), ], ) -> str: # pragma: no cover return "Name" definition = Query.__strawberry_definition__ assert definition.name == "Query" [argument] = definition.fields[0].arguments assert argument.python_name == "argument" assert argument.graphql_name is None assert argument.description == "This is a description" assert argument.type is str def test_annotated_optional_arguments_on_resolver(): @strawberry.type class Query: @strawberry.field def name( # type: ignore argument: Annotated[ str | None, strawberry.argument(description="This is a description"), ], ) -> str: # pragma: no cover return "Name" definition = Query.__strawberry_definition__ assert definition.name == "Query" [argument] = definition.fields[0].arguments assert argument.python_name == "argument" assert argument.graphql_name is None assert argument.description == "This is a description" assert isinstance(argument.type, StrawberryOptional) assert argument.type.of_type is str def test_annotated_argument_with_default_value(): @strawberry.type class Query: @strawberry.field def name( self, argument: Annotated[ str, strawberry.argument(description="This is a description"), ] = "Patrick", ) -> str: # pragma: no cover return "Name" definition = Query.__strawberry_definition__ assert definition.name == "Query" [argument] = definition.fields[0].arguments assert argument.python_name == "argument" assert argument.graphql_name is None assert argument.description == "This is a description" assert argument.type is str assert argument.default == "Patrick" def test_annotated_argument_with_rename(): @strawberry.type class Query: @strawberry.field def name( self, arg: Annotated[ str, strawberry.argument(name="argument"), ] = "Patrick", ) -> str: # pragma: no cover return "Name" definition = Query.__strawberry_definition__ assert definition.name == "Query" assert len(definition.fields[0].arguments) == 1 argument = definition.fields[0].arguments[0] assert argument.python_name == "arg" assert argument.graphql_name == "argument" assert argument.type is str assert argument.description is None assert argument.default == "Patrick" @pytest.mark.xfail(reason="Can't get field name from argument") def test_multiple_annotated_arguments_exception(): with pytest.raises(MultipleStrawberryArgumentsError) as error: @strawberry.field def name( argument: Annotated[ str, strawberry.argument(description="This is a description"), strawberry.argument(description="Another description"), ], ) -> str: # pragma: no cover return "Name" assert str(error.value) == ( "Annotation for argument `argument` " "on field `name` cannot have multiple " "`strawberry.argument`s" ) def test_annotated_with_other_information(): @strawberry.type class Query: @strawberry.field def name( self, argument: Annotated[str, "Some other info"] ) -> str: # pragma: no cover return "Name" definition = Query.__strawberry_definition__ assert definition.name == "Query" [argument] = definition.fields[0].arguments assert argument.python_name == "argument" assert argument.graphql_name is None assert argument.description is None assert argument.type is str def test_annotated_python_39(): from typing import Annotated @strawberry.type class Query: @strawberry.field def name( self, argument: Annotated[ str, strawberry.argument(description="This is a description"), ], ) -> str: # pragma: no cover return "Name" definition = Query.__strawberry_definition__ assert definition.name == "Query" [argument] = definition.fields[0].arguments assert argument.python_name == "argument" assert argument.graphql_name is None assert argument.type is str assert argument.description == "This is a description" assert argument.type is str @pytest.mark.raises_strawberry_exception( InvalidArgumentTypeError, 'Argument "word" on field "add_word" cannot be of type "Union"', ) def test_union_as_an_argument_type(): @strawberry.type class Noun: text: str @strawberry.type class Verb: text: str Word = Annotated[Noun | Verb, strawberry.argument("Word")] @strawberry.field def add_word(word: Word) -> bool: return True @pytest.mark.raises_strawberry_exception( InvalidArgumentTypeError, 'Argument "adjective" on field "add_adjective" cannot be of type "Interface"', ) def test_interface_as_an_argument_type(): @strawberry.interface class Adjective: text: str @strawberry.field def add_adjective(adjective: Adjective) -> bool: return True @pytest.mark.raises_strawberry_exception( InvalidArgumentTypeError, ( 'Argument "adjective" on field "add_adjective_resolver" cannot be ' 'of type "Interface"' ), ) def test_resolver_with_invalid_field_argument_type(): @strawberry.interface class Adjective: text: str def add_adjective_resolver(adjective: Adjective) -> bool: # pragma: no cover return True @strawberry.type class Mutation: add_adjective: bool = strawberry.field(resolver=add_adjective_resolver) def test_unset_deprecation_warning(): with pytest.deprecated_call(): from strawberry.types.arguments import UNSET # noqa: F401 with pytest.deprecated_call(): from strawberry.types.arguments import is_unset # noqa: F401 def test_deprecated_unset(): warning = "`is_unset` is deprecated use `value is UNSET` instead" with pytest.deprecated_call(match=warning): from strawberry.types.unset import is_unset with pytest.deprecated_call(match=warning): assert is_unset(UNSET) with pytest.deprecated_call(match=warning): assert not is_unset(None) with pytest.deprecated_call(match=warning): assert not is_unset(False) with pytest.deprecated_call(match=warning): assert not is_unset("hello world") strawberry-graphql-0.287.0/tests/fields/test_field_basics.py000066400000000000000000000016631511033167500241700ustar00rootroot00000000000000import strawberry def test_type_add_type_definition_with_fields(): @strawberry.type class Query: name: str age: int definition = Query.__strawberry_definition__ assert definition.name == "Query" assert len(definition.fields) == 2 assert definition.fields[0].python_name == "name" assert definition.fields[0].type is str assert definition.fields[1].python_name == "age" assert definition.fields[1].type is int def test_passing_nothing_to_fields(): @strawberry.type class Query: name: str = strawberry.field() age: int = strawberry.field() definition = Query.__strawberry_definition__ assert definition.name == "Query" assert len(definition.fields) == 2 assert definition.fields[0].python_name == "name" assert definition.fields[0].type is str assert definition.fields[1].python_name == "age" assert definition.fields[1].type is int strawberry-graphql-0.287.0/tests/fields/test_field_defaults.py000066400000000000000000000042301511033167500245240ustar00rootroot00000000000000import pytest import strawberry from strawberry.exceptions import ( FieldWithResolverAndDefaultFactoryError, FieldWithResolverAndDefaultValueError, InvalidDefaultFactoryError, ) from strawberry.types.field import StrawberryField def test_field_with_default(): @strawberry.type class Query: the_field: int = strawberry.field(default=3) instance = Query() assert instance.the_field == 3 def test_field_with_resolver_and_default(): with pytest.raises(FieldWithResolverAndDefaultValueError): @strawberry.type class Query: @strawberry.field(default="potato") def fruit(self) -> str: return "tomato" def test_field_with_default_factory(): @strawberry.type class Query: the_int: int = strawberry.field(default_factory=lambda: 3) instance = Query() [int_field] = Query.__strawberry_definition__.fields assert instance.the_int == 3 assert int_field.default_value == 3 def test_field_default_extensions_value_set(): field = StrawberryField(python_name="test", default="test") assert field.extensions == [] def test_field_default_factory_executed_each_time(): @strawberry.type class Query: the_list: list[str] = strawberry.field(default_factory=list) assert Query().the_list == Query().the_list assert Query().the_list is not Query().the_list def test_field_with_separate_resolver_default(): def fruit_resolver() -> str: # pragma: no cover return "banana" with pytest.raises(FieldWithResolverAndDefaultValueError): @strawberry.type class Query: weapon: str = strawberry.field( default="strawberry", resolver=fruit_resolver ) def test_field_with_resolver_and_default_factory(): with pytest.raises(FieldWithResolverAndDefaultFactoryError): @strawberry.type class Query: @strawberry.field(default_factory=lambda: "steel") def metal(self) -> str: return "iron" def test_invalid_default_factory(): with pytest.raises(InvalidDefaultFactoryError): strawberry.field(default_factory=round) strawberry-graphql-0.287.0/tests/fields/test_field_descriptions.py000066400000000000000000000003121511033167500254200ustar00rootroot00000000000000import strawberry def test_field_descriptions(): description = "this description is super cool" field = strawberry.field(description=description) assert field.description == description strawberry-graphql-0.287.0/tests/fields/test_field_exceptions.py000066400000000000000000000040001511033167500250710ustar00rootroot00000000000000import textwrap from typing import Any import pytest import strawberry from strawberry.annotation import StrawberryAnnotation from strawberry.exceptions import ( FieldWithResolverAndDefaultFactoryError, FieldWithResolverAndDefaultValueError, ) from strawberry.extensions.field_extension import FieldExtension from strawberry.types.field import StrawberryField def test_field_with_resolver_default(): with pytest.raises(FieldWithResolverAndDefaultValueError): @strawberry.type class Query: @strawberry.field(default="potato") def fruit(self) -> str: return "tomato" def test_field_with_separate_resolver_default(): def fruit_resolver() -> str: # pragma: no cover return "strawberry" with pytest.raises(FieldWithResolverAndDefaultValueError): @strawberry.type class Query: weapon: str = strawberry.field(default="banana", resolver=fruit_resolver) def test_field_with_resolver_default_factory(): with pytest.raises(FieldWithResolverAndDefaultFactoryError): @strawberry.type class Query: @strawberry.field(default_factory=lambda: "steel") def metal(self) -> str: return "iron" def test_extension_changing_field_return_value(): """Ensure that field extensions can change the field's return type.""" class ChangeReturnTypeExtension(FieldExtension): def apply(self, field: StrawberryField) -> None: field.type_annotation = StrawberryAnnotation.from_annotation(int) def resolve(self, next_, source, info, **kwargs: Any): return next_(source, info, **kwargs) @strawberry.type class Query: @strawberry.field(extensions=[ChangeReturnTypeExtension()]) def test_changing_return_type(self) -> bool: ... schema = strawberry.Schema(query=Query) expected = """\ type Query { testChangingReturnType: Int! } """ assert str(schema) == textwrap.dedent(expected).strip() strawberry-graphql-0.287.0/tests/fields/test_field_names.py000066400000000000000000000020021511033167500240130ustar00rootroot00000000000000import strawberry def test_field_name_standard(): standard_field = strawberry.field() assert standard_field.python_name is None assert standard_field.graphql_name is None def test_field_name_standard_on_schema(): @strawberry.type() class Query: normal_field: int [field] = Query.__strawberry_definition__.fields assert field.python_name == "normal_field" assert field.graphql_name is None def test_field_name_override(): field_name = "override" standard_field = strawberry.field(name=field_name) assert standard_field.python_name is None # Set once field is added to a Schema assert standard_field.graphql_name == field_name def test_field_name_override_with_schema(): field_name = "override_name" @strawberry.type() class Query: override_field: bool = strawberry.field(name=field_name) [field] = Query.__strawberry_definition__.fields assert field.python_name == "override_field" assert field.graphql_name == field_name strawberry-graphql-0.287.0/tests/fields/test_permissions.py000066400000000000000000000043041511033167500241270ustar00rootroot00000000000000from typing import Any import pytest from asgiref.sync import sync_to_async import strawberry from strawberry.permission import BasePermission def test_permission_classes_basic_fields(): class IsAuthenticated(BasePermission): message = "User is not authenticated" def has_permission( self, source: Any, info: strawberry.Info, **kwargs: Any ) -> bool: return False @strawberry.type class Query: user: str = strawberry.field(permission_classes=[IsAuthenticated]) definition = Query.__strawberry_definition__ assert definition.name == "Query" assert len(definition.fields) == 1 assert definition.fields[0].python_name == "user" assert definition.fields[0].graphql_name is None assert definition.fields[0].permission_classes == [IsAuthenticated] def test_permission_classes(): class IsAuthenticated(BasePermission): message = "User is not authenticated" def has_permission( self, source: Any, info: strawberry.Info, **kwargs: Any ) -> bool: return False @strawberry.type class Query: @strawberry.field(permission_classes=[IsAuthenticated]) def user(self) -> str: return "patrick" definition = Query.__strawberry_definition__ assert definition.name == "Query" assert len(definition.fields) == 1 assert definition.fields[0].python_name == "user" assert definition.fields[0].graphql_name is None assert definition.fields[0].permission_classes == [IsAuthenticated] @pytest.mark.asyncio async def test_permission_classes_async(): class IsAuthenticated(BasePermission): message = "User is not authenticated" @sync_to_async def has_permission( self, source: Any, info: strawberry.Info, **kwargs: Any ) -> bool: return True @sync_to_async def resolver() -> str: return "patrick" @strawberry.type class Query: user: str = strawberry.field( resolver=resolver, permission_classes=[IsAuthenticated] ) schema = strawberry.Schema(Query) result = await schema.execute("query { user }") assert not result.errors strawberry-graphql-0.287.0/tests/fields/test_resolvers.py000066400000000000000000000354361511033167500236120ustar00rootroot00000000000000import dataclasses import textwrap import types from typing import Any, ClassVar, no_type_check import pytest import strawberry from strawberry.exceptions import ( MissingArgumentsAnnotationsError, MissingFieldAnnotationError, MissingReturnAnnotationError, ) from strawberry.parent import Parent from strawberry.scalars import JSON from strawberry.types.fields.resolver import ( Signature, StrawberryResolver, UncallableResolverError, ) def test_resolver_as_argument(): def get_name(self) -> str: return "Name" @strawberry.type class Query: name: str = strawberry.field(resolver=get_name) definition = Query.__strawberry_definition__ assert definition.name == "Query" assert len(definition.fields) == 1 assert definition.fields[0].python_name == "name" assert definition.fields[0].graphql_name is None assert definition.fields[0].type is str assert definition.fields[0].base_resolver.wrapped_func == get_name def test_resolver_fields(): @strawberry.type class Query: @strawberry.field def name(self) -> str: return "Name" definition = Query.__strawberry_definition__ assert definition.name == "Query" assert len(definition.fields) == 1 assert definition.fields[0].python_name == "name" assert definition.fields[0].graphql_name is None assert definition.fields[0].type is str assert definition.fields[0].base_resolver(None) == Query().name() def test_staticmethod_resolver_fields(): @strawberry.type class Query: @strawberry.field @staticmethod def name() -> str: return "Name" definition = Query.__strawberry_definition__ assert definition.name == "Query" assert len(definition.fields) == 1 assert definition.fields[0].python_name == "name" assert definition.fields[0].graphql_name is None assert definition.fields[0].type is str assert definition.fields[0].base_resolver() == Query.name() assert Query.name() == "Name" assert Query().name() == "Name" def test_classmethod_resolver_fields(): @strawberry.type class Query: my_val: ClassVar[str] = "thingy" @strawberry.field @classmethod def val(cls) -> str: return cls.my_val definition = Query.__strawberry_definition__ assert definition.name == "Query" assert len(definition.fields) == 1 assert definition.fields[0].python_name == "val" assert definition.fields[0].graphql_name is None assert definition.fields[0].type is str assert definition.fields[0].base_resolver() == Query.val() assert Query.val() == "thingy" assert Query().val() == "thingy" @pytest.mark.raises_strawberry_exception( MissingReturnAnnotationError, match='Return annotation missing for field "hello", did you forget to add it?', ) def test_raises_error_when_return_annotation_missing(): @strawberry.type class Query: @strawberry.field def hello(self): return "I'm a resolver" @pytest.mark.raises_strawberry_exception( MissingReturnAnnotationError, match='Return annotation missing for field "hello", did you forget to add it?', ) def test_raises_error_when_return_annotation_missing_async_function(): @strawberry.type class Query: @strawberry.field async def hello(self): return "I'm a resolver" @pytest.mark.raises_strawberry_exception( MissingReturnAnnotationError, match='Return annotation missing for field "goodbye", did you forget to add it?', ) def test_raises_error_when_return_annotation_missing_resolver(): @strawberry.type class Query2: def adios(self): return -1 goodbye = strawberry.field(resolver=adios) @pytest.mark.raises_strawberry_exception( MissingArgumentsAnnotationsError, match=( 'Missing annotation for argument "query" in field "hello", ' "did you forget to add it?" ), ) def test_raises_error_when_argument_annotation_missing(): @strawberry.field def hello(self, query) -> str: return "I'm a resolver" @pytest.mark.raises_strawberry_exception( MissingArgumentsAnnotationsError, match=( 'Missing annotation for arguments "query" and "limit" ' 'in field "hello", did you forget to add it?' ), ) def test_raises_error_when_argument_annotation_missing_multiple_fields(): @strawberry.field def hello(self, query, limit) -> str: return "I'm a resolver" @pytest.mark.raises_strawberry_exception( MissingArgumentsAnnotationsError, match=( 'Missing annotation for argument "query" ' 'in field "hello", did you forget to add it?' ), ) def test_raises_error_when_argument_annotation_missing_multiple_lines(): @strawberry.field def hello( self, query, ) -> str: return "I'm a resolver" @pytest.mark.raises_strawberry_exception( MissingArgumentsAnnotationsError, match=( 'Missing annotation for argument "query" ' 'in field "hello", did you forget to add it?' ), ) def test_raises_error_when_argument_annotation_missing_default_value(): @strawberry.field def hello( self, query="this is a default value", ) -> str: return "I'm a resolver" @pytest.mark.raises_strawberry_exception( MissingFieldAnnotationError, match=( 'Unable to determine the type of field "missing". ' "Either annotate it directly, or provide a typed resolver " "using @strawberry.field." ), ) def test_raises_error_when_missing_annotation_and_resolver(): @strawberry.type class Query: missing = strawberry.field(name="annotation") @pytest.mark.raises_strawberry_exception( MissingFieldAnnotationError, match=( 'Unable to determine the type of field "missing". Either annotate it ' "directly, or provide a typed resolver using @strawberry.field." ), ) def test_raises_error_when_missing_type(): """Test to make sure that if somehow a non-StrawberryField field is added to the cls without annotations it raises an exception. This would occur if someone manually uses dataclasses.field """ @strawberry.type class Query: missing = dataclasses.field() @pytest.mark.raises_strawberry_exception( MissingFieldAnnotationError, match=( 'Unable to determine the type of field "missing". Either annotate it ' "directly, or provide a typed resolver using @strawberry.field." ), ) def test_raises_error_when_missing_type_on_dynamic_class(): # this test if for making sure the code that finds the exception source # doesn't crash with dynamic code namespace = {"missing": dataclasses.field()} strawberry.type(types.new_class("Query", (), {}, lambda ns: ns.update(namespace))) @pytest.mark.raises_strawberry_exception( MissingFieldAnnotationError, match=( 'Unable to determine the type of field "banana". Either annotate it ' "directly, or provide a typed resolver using @strawberry.field." ), ) def test_raises_error_when_missing_type_on_longish_class(): @strawberry.type class Query: field_1: str = strawberry.field(name="field_1") field_2: str = strawberry.field(name="field_2") field_3: str = strawberry.field(name="field_3") field_4: str = strawberry.field(name="field_4") field_5: str = strawberry.field(name="field_5") field_6: str = strawberry.field(name="field_6") field_7: str = strawberry.field(name="field_7") field_8: str = strawberry.field(name="field_8") field_9: str = strawberry.field(name="field_9") banana = strawberry.field(name="banana") field_10: str = strawberry.field(name="field_10") field_11: str = strawberry.field(name="field_11") field_12: str = strawberry.field(name="field_12") field_13: str = strawberry.field(name="field_13") field_14: str = strawberry.field(name="field_14") field_15: str = strawberry.field(name="field_15") field_16: str = strawberry.field(name="field_16") field_17: str = strawberry.field(name="field_17") field_18: str = strawberry.field(name="field_18") field_19: str = strawberry.field(name="field_19") def test_raises_error_calling_uncallable_resolver(): @classmethod # type: ignore def class_func(cls) -> int: ... # Note that class_func is a raw classmethod object because it has not been bound # to a class at this point resolver = StrawberryResolver(class_func) with pytest.raises( UncallableResolverError, match=r"Attempted to call resolver (.*) with uncallable function (.*)", ): resolver() def test_can_reuse_resolver(): def get_name(self) -> str: return "Name" @strawberry.type class Query: name: str = strawberry.field(resolver=get_name) name_2: str = strawberry.field(resolver=get_name) definition = Query.__strawberry_definition__ assert definition.name == "Query" assert len(definition.fields) == 2 assert definition.fields[0].python_name == "name" assert definition.fields[0].graphql_name is None assert definition.fields[0].python_name == "name" assert definition.fields[0].type is str assert definition.fields[0].base_resolver.wrapped_func == get_name assert definition.fields[1].python_name == "name_2" assert definition.fields[1].graphql_name is None assert definition.fields[1].python_name == "name_2" assert definition.fields[1].type is str assert definition.fields[1].base_resolver.wrapped_func == get_name def test_eq_resolvers(): def get_name(self) -> str: return "Name" @strawberry.type class Query: a: int name: str = strawberry.field(resolver=get_name) name_2: str = strawberry.field(resolver=get_name) assert Query(a=1) == Query(a=1) assert Query(a=1) != Query(a=2) def test_eq_fields(): @strawberry.type class Query: a: int name: str = strawberry.field(name="name") assert Query(a=1, name="name") == Query(a=1, name="name") assert Query(a=1, name="name") != Query(a=1, name="not a name") def test_with_resolver_fields(): @strawberry.type class Query: a: int @strawberry.field def name(self) -> str: return "A" assert Query(a=1) == Query(a=1) assert Query(a=1) != Query(a=2) def root_and_info( root, foo: str, bar: float, info: str, strawberry_info: strawberry.Info, ) -> str: raise AssertionError("Unreachable code.") def self_and_info( self, foo: str, bar: float, info: str, strawberry_info: strawberry.Info, ) -> str: raise AssertionError("Unreachable code.") def parent_and_info( parent: Parent[str], foo: str, bar: float, info: str, strawberry_info: strawberry.Info, ) -> str: raise AssertionError("Unreachable code.") @pytest.mark.parametrize( "resolver_func", [ pytest.param(self_and_info), pytest.param(root_and_info), pytest.param(parent_and_info), ], ) def test_resolver_annotations(resolver_func): """Ensure only non-reserved annotations are returned.""" resolver = StrawberryResolver(resolver_func) expected_annotations = {"foo": str, "bar": float, "info": str, "return": str} assert resolver.annotations == expected_annotations # Sanity-check to ensure StrawberryArguments return the same annotations assert { **{ arg.python_name: arg.type_annotation.resolve() # type: ignore for arg in resolver.arguments }, "return": str, } @no_type_check def test_resolver_with_unhashable_default(): @strawberry.type class Query: @strawberry.field def field(self, x: list[str] = ["foo"], y: JSON = {"foo": 42}) -> str: # noqa: B006 return f"{x} {y}" schema = strawberry.Schema(Query) result = schema.execute_sync("query { field }") assert result.data == {"field": "['foo'] {'foo': 42}"} assert not result.errors @no_type_check def test_parameter_hash_collision(): """Ensure support for hashable defaults does not introduce collision.""" def foo(x: str = "foo"): pass def bar(x: str = "bar"): pass foo_signature = Signature.from_callable(foo, follow_wrapped=True) bar_signature = Signature.from_callable(bar, follow_wrapped=True) foo_param = foo_signature.parameters["x"] bar_param = bar_signature.parameters["x"] # Ensure __eq__ still functions properly assert foo_param != bar_param # Ensure collision does not occur in hash-map and hash-tables. Colisions are # prevented by Python invoking __eq__ when two items have the same hash. parameters_map = { foo_param: "foo", bar_param: "bar", } parameters_set = {foo_param, bar_param} assert len(parameters_map) == 2 assert len(parameters_set) == 2 def test_annotation_using_parent_annotation(): @strawberry.type class FruitType: name: str @strawberry.field @staticmethod def name_from_parent(parent: strawberry.Parent[Any]) -> str: return f"Using 'parent': {parent.name}" @strawberry.type class Query: @strawberry.field @staticmethod def fruit() -> FruitType: return FruitType(name="Strawberry") schema = strawberry.Schema(query=Query) expected = """\ type FruitType { name: String! nameFromParent: String! } type Query { fruit: FruitType! } """ assert textwrap.dedent(str(schema)) == textwrap.dedent(expected).strip() result = schema.execute_sync("query { fruit { name nameFromParent } }") assert result.data == { "fruit": { "name": "Strawberry", "nameFromParent": "Using 'parent': Strawberry", } } def test_annotation_using_parent_annotation_but_named_root(): @strawberry.type class FruitType: name: str @strawberry.field @staticmethod def name_from_parent(root: strawberry.Parent[Any]) -> str: return f"Using 'root': {root.name}" @strawberry.type class Query: @strawberry.field @staticmethod def fruit() -> FruitType: return FruitType(name="Strawberry") schema = strawberry.Schema(query=Query) expected = """\ type FruitType { name: String! nameFromParent: String! } type Query { fruit: FruitType! } """ assert textwrap.dedent(str(schema)) == textwrap.dedent(expected).strip() result = schema.execute_sync("query { fruit { name nameFromParent } }") assert result.data == { "fruit": { "name": "Strawberry", "nameFromParent": "Using 'root': Strawberry", } } strawberry-graphql-0.287.0/tests/file_uploads/000077500000000000000000000000001511033167500213425ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/file_uploads/__init__.py000066400000000000000000000000001511033167500234410ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/file_uploads/test_utils.py000066400000000000000000000106601511033167500241160ustar00rootroot00000000000000from io import BytesIO from strawberry.file_uploads.utils import replace_placeholders_with_files def test_does_deep_copy(): operations = { "query": "mutation($file: Upload!) { upload_file(file: $file) { id } }", "variables": {"file": None}, } files_map = {} files = {} result = replace_placeholders_with_files(operations, files_map, files) assert result == operations assert result is not operations def test_empty_files_map(): operations = { "query": "mutation($files: [Upload!]!) { upload_files(files: $files) { id } }", "variables": {"files": [None, None]}, } files_map = {} files = {"0": BytesIO(), "1": BytesIO()} result = replace_placeholders_with_files(operations, files_map, files) assert result == operations def test_empty_operations_paths(): operations = { "query": "mutation($files: [Upload!]!) { upload_files(files: $files) { id } }", "variables": {"files": [None, None]}, } files_map = {"0": [], "1": []} files = {"0": BytesIO(), "1": BytesIO()} result = replace_placeholders_with_files(operations, files_map, files) assert result == operations def test_single_file_in_single_location(): operations = { "query": "mutation($file: Upload!) { upload_file(file: $file) { id } }", "variables": {"file": None}, } files_map = {"0": ["variables.file"]} file0 = BytesIO() files = {"0": file0} result = replace_placeholders_with_files(operations, files_map, files) assert result["query"] == operations["query"] assert result["variables"]["file"] == file0 def test_single_file_in_multiple_locations(): operations = { "query": "mutation($a: Upload!, $b: Upload!) { pair(a: $a, b: $a) { id } }", "variables": {"a": None, "b": None}, } files_map = {"0": ["variables.a", "variables.b"]} file0 = BytesIO() files = {"0": file0} result = replace_placeholders_with_files(operations, files_map, files) assert result["query"] == operations["query"] assert result["variables"]["a"] == file0 assert result["variables"]["b"] == file0 def test_file_list(): operations = { "query": "mutation($files: [Upload!]!) { upload_files(files: $files) { id } }", "variables": {"files": [None, None]}, } files_map = {"0": ["variables.files.0"], "1": ["variables.files.1"]} file0 = BytesIO() file1 = BytesIO() files = {"0": file0, "1": file1} result = replace_placeholders_with_files(operations, files_map, files) assert result["query"] == operations["query"] assert result["variables"]["files"][0] == file0 assert result["variables"]["files"][1] == file1 def test_single_file_reuse_in_list(): operations = { "query": "mutation($a: [Upload!]!, $b: Upload!) { mixed(a: $a, b: $b) { id } }", "variables": {"a": [None, None], "b": None}, } files_map = {"0": ["variables.a.0"], "1": ["variables.a.1", "variables.b"]} file0 = BytesIO() file1 = BytesIO() files = {"0": file0, "1": file1} result = replace_placeholders_with_files(operations, files_map, files) assert result["query"] == operations["query"] assert result["variables"]["a"][0] == file0 assert result["variables"]["a"][1] == file1 assert result["variables"]["b"] == file1 def test_using_single_file_multiple_times_in_same_list(): operations = { "query": "mutation($files: [Upload!]!) { upload_files(files: $files) { id } }", "variables": {"files": [None, None]}, } files_map = {"0": ["variables.files.0", "variables.files.1"]} file0 = BytesIO() files = {"0": file0} result = replace_placeholders_with_files(operations, files_map, files) assert result["query"] == operations["query"] assert result["variables"]["files"][0] == file0 assert result["variables"]["files"][1] == file0 def test_deep_nesting(): operations = { "query": "mutation($list: [ComplexInput!]!) { mutate(list: $list) { id } }", "variables": {"a": [{"files": [None, None]}]}, } files_map = {"0": ["variables.a.0.files.0"], "1": ["variables.a.0.files.1"]} file0 = BytesIO() file1 = BytesIO() files = {"0": file0, "1": file1} result = replace_placeholders_with_files(operations, files_map, files) assert result["query"] == operations["query"] assert result["variables"]["a"][0]["files"][0] == file0 assert result["variables"]["a"][0]["files"][1] == file1 strawberry-graphql-0.287.0/tests/fixtures/000077500000000000000000000000001511033167500205455ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/fixtures/__init__.py000066400000000000000000000000001511033167500226440ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/fixtures/sample_package/000077500000000000000000000000001511033167500235015ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/fixtures/sample_package/__init__.py000066400000000000000000000000001511033167500256000ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/fixtures/sample_package/sample_module.py000066400000000000000000000017451511033167500267100ustar00rootroot00000000000000from enum import Enum from typing import Annotated, NewType import strawberry ExampleScalar = strawberry.scalar( NewType("ExampleScalar", object), serialize=lambda v: v, parse_value=lambda v: v, ) @strawberry.type class A: name: str @strawberry.type class B: a: A UnionExample = Annotated[A | B, strawberry.union("UnionExample")] class SampleClass: def __init__(self, schema): self.schema = schema @strawberry.enum class Role(Enum): ADMIN = "ADMIN" USER = "USER" @strawberry.type class User: name: str age: int role: Role example_scalar: ExampleScalar union_example: UnionExample inline_union: Annotated[A | B, strawberry.union("InlineUnion")] @strawberry.type class Query: @strawberry.field def user(self) -> User: return User(name="Patrick", age=100) def create_schema(): return strawberry.Schema(query=Query) schema = create_schema() sample_instance = SampleClass(schema) not_a_schema = 42 strawberry-graphql-0.287.0/tests/http/000077500000000000000000000000001511033167500176535ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/http/__init__.py000066400000000000000000000000001511033167500217520ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/http/clients/000077500000000000000000000000001511033167500213145ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/http/clients/__init__.py000066400000000000000000000000001511033167500234130ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/http/clients/aiohttp.py000066400000000000000000000165501511033167500233450ustar00rootroot00000000000000from __future__ import annotations import contextlib import json from collections.abc import AsyncGenerator, Mapping, Sequence from datetime import timedelta from io import BytesIO from typing import Any, Literal from aiohttp import web from aiohttp.client_ws import ClientWebSocketResponse from aiohttp.http_websocket import WSMsgType from aiohttp.test_utils import TestClient, TestServer from strawberry.aiohttp.views import GraphQLView as BaseGraphQLView from strawberry.http import GraphQLHTTPResponse from strawberry.http.ides import GraphQL_IDE from strawberry.schema import Schema from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL from strawberry.types import ExecutionResult from tests.http.context import get_context from tests.views.schema import Query from tests.websockets.views import OnWSConnectMixin from .base import ( JSON, DebuggableGraphQLTransportWSHandler, DebuggableGraphQLWSHandler, HttpClient, Message, Response, ResultOverrideFunction, WebSocketClient, ) class GraphQLView(OnWSConnectMixin, BaseGraphQLView[dict[str, object], object]): result_override: ResultOverrideFunction = None graphql_transport_ws_handler_class = DebuggableGraphQLTransportWSHandler graphql_ws_handler_class = DebuggableGraphQLWSHandler async def get_context( self, request: web.Request, response: web.Response | web.WebSocketResponse ) -> dict[str, object]: context = await super().get_context(request, response) return get_context(context) async def get_root_value(self, request: web.Request) -> Query: await super().get_root_value(request) # for coverage return Query() async def process_result( self, request: web.Request, result: ExecutionResult ) -> GraphQLHTTPResponse: if self.result_override: return self.result_override(result) return await super().process_result(request, result) class AioHttpClient(HttpClient): def __init__( self, schema: Schema, graphiql: bool | None = None, graphql_ide: GraphQL_IDE | None = "graphiql", allow_queries_via_get: bool = True, keep_alive: bool = False, keep_alive_interval: float = 1, subscription_protocols: Sequence[str] = ( GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL, ), connection_init_wait_timeout: timedelta = timedelta(minutes=1), result_override: ResultOverrideFunction = None, multipart_uploads_enabled: bool = False, ): view = GraphQLView( schema=schema, graphiql=graphiql, graphql_ide=graphql_ide, allow_queries_via_get=allow_queries_via_get, keep_alive=keep_alive, keep_alive_interval=keep_alive_interval, subscription_protocols=subscription_protocols, connection_init_wait_timeout=connection_init_wait_timeout, multipart_uploads_enabled=multipart_uploads_enabled, ) view.result_override = result_override self.app = web.Application() self.app.router.add_route("*", "/graphql", view) async def _graphql_request( self, method: Literal["get", "post"], query: str | None = None, operation_name: str | None = None, variables: dict[str, object] | None = None, files: dict[str, BytesIO] | None = None, headers: dict[str, str] | None = None, extensions: dict[str, Any] | None = None, **kwargs: Any, ) -> Response: async with TestClient(TestServer(self.app)) as client: body = self._build_body( query=query, operation_name=operation_name, variables=variables, files=files, method=method, extensions=extensions, ) if body and files: body.update(files) if method == "get": kwargs["params"] = body else: kwargs["data"] = body if files else json.dumps(body) response = await getattr(client, method)( "/graphql", headers=self._get_headers(method=method, headers=headers, files=files), **kwargs, ) return Response( status_code=response.status, data=(await response.text()).encode(), headers=response.headers, ) async def request( self, url: str, method: Literal["head", "get", "post", "patch", "put", "delete"], headers: dict[str, str] | None = None, ) -> Response: async with TestClient(TestServer(self.app)) as client: response = await getattr(client, method)(url, headers=headers) return Response( status_code=response.status, data=(await response.text()).encode(), headers=response.headers, ) async def get( self, url: str, headers: dict[str, str] | None = None, ) -> Response: return await self.request(url, "get", headers=headers) async def post( self, url: str, data: bytes | None = None, json: JSON | None = None, headers: dict[str, str] | None = None, ) -> Response: async with TestClient(TestServer(self.app)) as client: response = await client.post( "/graphql", headers=headers, data=data, json=json ) return Response( status_code=response.status, data=(await response.text()).encode(), headers=dict(response.headers), ) @contextlib.asynccontextmanager async def ws_connect( self, url: str, *, protocols: list[str], ) -> AsyncGenerator[WebSocketClient, None]: async with ( TestClient(TestServer(self.app)) as client, client.ws_connect(url, protocols=protocols) as ws, ): yield AioWebSocketClient(ws) class AioWebSocketClient(WebSocketClient): def __init__(self, ws: ClientWebSocketResponse): self.ws = ws self._reason: str | None = None async def send_text(self, payload: str) -> None: await self.ws.send_str(payload) async def send_json(self, payload: Mapping[str, object]) -> None: await self.ws.send_json(payload) async def send_bytes(self, payload: bytes) -> None: await self.ws.send_bytes(payload) async def receive(self, timeout: float | None = None) -> Message: m = await self.ws.receive(timeout) self._reason = m.extra return Message(type=m.type, data=m.data, extra=m.extra) async def receive_json(self, timeout: float | None = None) -> object: m = await self.ws.receive(timeout) assert m.type == WSMsgType.TEXT return json.loads(m.data) async def close(self) -> None: await self.ws.close() @property def accepted_subprotocol(self) -> str | None: return self.ws.protocol @property def closed(self) -> bool: return self.ws.closed @property def close_code(self) -> int: assert self.ws.close_code is not None return self.ws.close_code @property def close_reason(self) -> str | None: return self._reason strawberry-graphql-0.287.0/tests/http/clients/asgi.py000066400000000000000000000170251511033167500226160ustar00rootroot00000000000000from __future__ import annotations import contextlib import json from collections.abc import AsyncGenerator, Mapping, Sequence from datetime import timedelta from io import BytesIO from typing import Any, Literal from starlette.requests import Request from starlette.responses import Response as StarletteResponse from starlette.testclient import TestClient, WebSocketTestSession from starlette.websockets import WebSocket from strawberry.asgi import GraphQL as BaseGraphQLView from strawberry.http import GraphQLHTTPResponse from strawberry.http.ides import GraphQL_IDE from strawberry.schema import Schema from strawberry.subscriptions import ( GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL, ) from strawberry.types import ExecutionResult from tests.http.context import get_context from tests.views.schema import Query from tests.websockets.views import OnWSConnectMixin from .base import ( JSON, DebuggableGraphQLTransportWSHandler, DebuggableGraphQLWSHandler, HttpClient, Message, Response, ResultOverrideFunction, WebSocketClient, ) class GraphQLView(OnWSConnectMixin, BaseGraphQLView[dict[str, object], object]): result_override: ResultOverrideFunction = None graphql_transport_ws_handler_class = DebuggableGraphQLTransportWSHandler graphql_ws_handler_class = DebuggableGraphQLWSHandler async def get_root_value(self, request: WebSocket | Request) -> Query: return Query() async def get_context( self, request: Request | WebSocket, response: StarletteResponse | WebSocket, ) -> dict[str, object]: context = await super().get_context(request, response) return get_context(context) async def process_result( self, request: Request, result: ExecutionResult ) -> GraphQLHTTPResponse: if self.result_override: return self.result_override(result) return await super().process_result(request, result) class AsgiHttpClient(HttpClient): def __init__( self, schema: Schema, graphiql: bool | None = None, graphql_ide: GraphQL_IDE | None = "graphiql", allow_queries_via_get: bool = True, keep_alive: bool = False, keep_alive_interval: float = 1, subscription_protocols: Sequence[str] = ( GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL, ), connection_init_wait_timeout: timedelta = timedelta(minutes=1), result_override: ResultOverrideFunction = None, multipart_uploads_enabled: bool = False, ): view = GraphQLView( schema, graphiql=graphiql, graphql_ide=graphql_ide, allow_queries_via_get=allow_queries_via_get, keep_alive=keep_alive, keep_alive_interval=keep_alive_interval, subscription_protocols=subscription_protocols, connection_init_wait_timeout=connection_init_wait_timeout, multipart_uploads_enabled=multipart_uploads_enabled, ) view.result_override = result_override self.client = TestClient(view) async def _graphql_request( self, method: Literal["get", "post"], query: str | None = None, operation_name: str | None = None, variables: dict[str, object] | None = None, files: dict[str, BytesIO] | None = None, headers: dict[str, str] | None = None, extensions: dict[str, Any] | None = None, **kwargs: Any, ) -> Response: body = self._build_body( query=query, operation_name=operation_name, variables=variables, files=files, method=method, extensions=extensions, ) if method == "get": kwargs["params"] = body elif body: if files: kwargs["data"] = body else: kwargs["content"] = json.dumps(body) if files is not None: kwargs["files"] = files response = getattr(self.client, method)( "/graphql", headers=self._get_headers(method=method, headers=headers, files=files), **kwargs, ) return Response( status_code=response.status_code, data=response.content, headers=response.headers, ) async def request( self, url: str, method: Literal["get", "post", "patch", "put", "delete"], headers: dict[str, str] | None = None, ) -> Response: response = getattr(self.client, method)(url, headers=headers) return Response( status_code=response.status_code, data=response.content, headers=response.headers, ) async def get( self, url: str, headers: dict[str, str] | None = None, ) -> Response: return await self.request(url, "get", headers=headers) async def post( self, url: str, data: bytes | None = None, json: JSON | None = None, headers: dict[str, str] | None = None, ) -> Response: response = self.client.post(url, headers=headers, content=data, json=json) return Response( status_code=response.status_code, data=response.content, headers=dict(response.headers), ) @contextlib.asynccontextmanager async def ws_connect( self, url: str, *, protocols: list[str], ) -> AsyncGenerator[WebSocketClient, None]: with self.client.websocket_connect(url, protocols) as ws: yield AsgiWebSocketClient(ws) class AsgiWebSocketClient(WebSocketClient): def __init__(self, ws: WebSocketTestSession): self.ws = ws self._closed: bool = False self._close_code: int | None = None self._close_reason: str | None = None async def send_text(self, payload: str) -> None: self.ws.send_text(payload) async def send_json(self, payload: Mapping[str, object]) -> None: self.ws.send_json(payload) async def send_bytes(self, payload: bytes) -> None: self.ws.send_bytes(payload) async def receive(self, timeout: float | None = None) -> Message: if self._closed: # if close was received via exception, fake it so that recv works return Message( type="websocket.close", data=self._close_code, extra=self._close_reason ) m = self.ws.receive() if m["type"] == "websocket.close": self._closed = True self._close_code = m["code"] self._close_reason = m["reason"] return Message(type=m["type"], data=m["code"], extra=m["reason"]) if m["type"] == "websocket.send": return Message(type=m["type"], data=m["text"]) return Message(type=m["type"], data=m["data"], extra=m["extra"]) async def receive_json(self, timeout: float | None = None) -> Any: m = self.ws.receive() assert m["type"] == "websocket.send" assert "text" in m return json.loads(m["text"]) async def close(self) -> None: self.ws.close() self._closed = True @property def accepted_subprotocol(self) -> str | None: return self.ws.accepted_subprotocol @property def closed(self) -> bool: return self._closed @property def close_code(self) -> int: assert self._close_code is not None return self._close_code @property def close_reason(self) -> str | None: return self._close_reason strawberry-graphql-0.287.0/tests/http/clients/async_django.py000066400000000000000000000055051511033167500243320ustar00rootroot00000000000000from __future__ import annotations from collections.abc import AsyncIterable from django.core.exceptions import BadRequest, SuspiciousOperation from django.http import Http404, HttpRequest, HttpResponse, StreamingHttpResponse from strawberry.django.views import AsyncGraphQLView as BaseAsyncGraphQLView from strawberry.http import GraphQLHTTPResponse from strawberry.http.ides import GraphQL_IDE from strawberry.schema import Schema from strawberry.types import ExecutionResult from tests.http.context import get_context from tests.views.schema import Query from .base import Response, ResultOverrideFunction from .django import DjangoHttpClient class AsyncGraphQLView(BaseAsyncGraphQLView[dict[str, object], object]): result_override: ResultOverrideFunction = None async def get_root_value(self, request: HttpRequest) -> Query: await super().get_root_value(request) # for coverage return Query() async def get_context( self, request: HttpRequest, response: HttpResponse ) -> dict[str, object]: context = {"request": request, "response": response} return get_context(context) async def process_result( self, request: HttpRequest, result: ExecutionResult ) -> GraphQLHTTPResponse: if self.result_override: return self.result_override(result) return await super().process_result(request, result) class AsyncDjangoHttpClient(DjangoHttpClient): def __init__( self, schema: Schema, graphiql: bool | None = None, graphql_ide: GraphQL_IDE | None = "graphiql", allow_queries_via_get: bool = True, result_override: ResultOverrideFunction = None, multipart_uploads_enabled: bool = False, ): self.view = AsyncGraphQLView.as_view( schema=schema, graphiql=graphiql, graphql_ide=graphql_ide, allow_queries_via_get=allow_queries_via_get, result_override=result_override, multipart_uploads_enabled=multipart_uploads_enabled, ) async def _do_request(self, request: HttpRequest) -> Response: try: response = await self.view(request) except Http404: return Response(status_code=404, data=b"Not found", headers={}) except (BadRequest, SuspiciousOperation) as e: return Response( status_code=400, data=e.args[0].encode(), headers={}, ) data = ( response.streaming_content if isinstance(response, StreamingHttpResponse) and isinstance(response.streaming_content, AsyncIterable) else response.content ) return Response( status_code=response.status_code, data=data, headers=dict(response.headers), ) strawberry-graphql-0.287.0/tests/http/clients/async_flask.py000066400000000000000000000045421511033167500241700ustar00rootroot00000000000000from __future__ import annotations from typing import Any from flask import Flask from flask import Request as FlaskRequest from flask import Response as FlaskResponse from strawberry.flask.views import AsyncGraphQLView as BaseAsyncGraphQLView from strawberry.http import GraphQLHTTPResponse from strawberry.http.ides import GraphQL_IDE from strawberry.schema import Schema from strawberry.types import ExecutionResult from tests.http.context import get_context from tests.views.schema import Query from .base import ResultOverrideFunction from .flask import FlaskHttpClient class GraphQLView(BaseAsyncGraphQLView[dict[str, object], object]): methods = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"] result_override: ResultOverrideFunction = None def __init__(self, *args: Any, **kwargs: Any): self.result_override = kwargs.pop("result_override") super().__init__(*args, **kwargs) async def get_root_value(self, request: FlaskRequest) -> Query: await super().get_root_value(request) # for coverage return Query() async def get_context( self, request: FlaskRequest, response: FlaskResponse ) -> dict[str, object]: context = await super().get_context(request, response) return get_context(context) async def process_result( self, request: FlaskRequest, result: ExecutionResult ) -> GraphQLHTTPResponse: if self.result_override: return self.result_override(result) return await super().process_result(request, result) class AsyncFlaskHttpClient(FlaskHttpClient): def __init__( self, schema: Schema, graphiql: bool | None = None, graphql_ide: GraphQL_IDE | None = "graphiql", allow_queries_via_get: bool = True, result_override: ResultOverrideFunction = None, multipart_uploads_enabled: bool = False, ): self.app = Flask(__name__) self.app.debug = True view = GraphQLView.as_view( "graphql_view", schema=schema, graphiql=graphiql, graphql_ide=graphql_ide, allow_queries_via_get=allow_queries_via_get, result_override=result_override, multipart_uploads_enabled=multipart_uploads_enabled, ) self.app.add_url_rule( "/graphql", view_func=view, ) strawberry-graphql-0.287.0/tests/http/clients/base.py000066400000000000000000000265301511033167500226060ustar00rootroot00000000000000import abc import contextlib import json import logging from collections.abc import AsyncGenerator, AsyncIterable, Callable, Mapping, Sequence from dataclasses import dataclass from datetime import timedelta from functools import cached_property from io import BytesIO from typing import Any, Literal, Optional, Union from strawberry.http import GraphQLHTTPResponse from strawberry.http.ides import GraphQL_IDE from strawberry.schema import Schema from strawberry.subscriptions.protocols.graphql_transport_ws.handlers import ( BaseGraphQLTransportWSHandler, ) from strawberry.subscriptions.protocols.graphql_transport_ws.types import ( Message as GraphQLTransportWSMessage, ) from strawberry.subscriptions.protocols.graphql_ws.handlers import BaseGraphQLWSHandler from strawberry.subscriptions.protocols.graphql_ws.types import OperationMessage from strawberry.types import ExecutionResult logger = logging.getLogger("strawberry.test.http_client") JSON = Union[dict[str, "JSON"], list["JSON"], str, int, float, bool, None] ResultOverrideFunction = Optional[Callable[[ExecutionResult], GraphQLHTTPResponse]] @dataclass class Response: status_code: int data: bytes | AsyncIterable[bytes] def __init__( self, status_code: int, data: bytes | AsyncIterable[bytes], *, headers: dict[str, str] | None = None, ) -> None: self.status_code = status_code self.data = data self._headers = headers or {} @cached_property def headers(self) -> Mapping[str, str]: return {k.lower(): v for k, v in self._headers.items()} @property def is_multipart(self) -> bool: return self.headers.get("content-type", "").startswith("multipart/mixed") @property def text(self) -> str: assert isinstance(self.data, bytes) return self.data.decode() @property def json(self) -> JSON: assert isinstance(self.data, bytes) return json.loads(self.data) async def streaming_json(self) -> AsyncIterable[JSON]: if not self.is_multipart: raise ValueError("Streaming not supported") def parse_chunk(text: str) -> JSON | None: # TODO: better parsing? :) with contextlib.suppress(json.JSONDecodeError): return json.loads(text) if isinstance(self.data, AsyncIterable): chunks = self.data async for chunk in chunks: lines = chunk.decode("utf-8").split("\r\n") for text in lines: if data := parse_chunk(text): yield data else: # TODO: we do this because httpx doesn't support streaming # it would be nice to fix httpx instead of doing this, # but we might have the same issue in other clients too # TODO: better message logger.warning("Didn't receive a stream, parsing it sync") chunks = self.data.decode("utf-8").split("\r\n") for chunk in chunks: if data := parse_chunk(chunk): yield data class HttpClient(abc.ABC): @abc.abstractmethod def __init__( self, schema: Schema, graphiql: bool | None = None, graphql_ide: GraphQL_IDE | None = "graphiql", allow_queries_via_get: bool = True, keep_alive: bool = False, keep_alive_interval: float = 1, subscription_protocols: Sequence[str] = (), connection_init_wait_timeout: timedelta = timedelta(minutes=1), result_override: ResultOverrideFunction = None, multipart_uploads_enabled: bool = False, ): ... @abc.abstractmethod async def _graphql_request( self, method: Literal["get", "post"], query: str | None = None, operation_name: str | None = None, variables: dict[str, object] | None = None, files: dict[str, BytesIO] | None = None, headers: dict[str, str] | None = None, extensions: dict[str, Any] | None = None, **kwargs: Any, ) -> Response: ... @abc.abstractmethod async def request( self, url: str, method: Literal["head", "get", "post", "patch", "put", "delete"], headers: dict[str, str] | None = None, ) -> Response: ... @abc.abstractmethod async def get( self, url: str, headers: dict[str, str] | None = None, ) -> Response: ... @abc.abstractmethod async def post( self, url: str, data: bytes | None = None, json: JSON | None = None, headers: dict[str, str] | None = None, ) -> Response: ... async def query( self, query: str, method: Literal["get", "post"] = "post", operation_name: str | None = None, variables: dict[str, object] | None = None, files: dict[str, BytesIO] | None = None, headers: dict[str, str] | None = None, extensions: dict[str, Any] | None = None, ) -> Response: return await self._graphql_request( method, query=query, operation_name=operation_name, headers=headers, variables=variables, files=files, extensions=extensions, ) def _get_headers( self, method: Literal["get", "post"], headers: dict[str, str] | None, files: dict[str, BytesIO] | None, ) -> dict[str, str]: additional_headers = {} headers = headers or {} # TODO: fix case sensitivity content_type = headers.get("content-type") if not content_type and method == "post" and not files: content_type = "application/json" additional_headers = {"Content-Type": content_type} if content_type else {} return {**additional_headers, **headers} def _build_body( self, query: str | None = None, operation_name: str | None = None, variables: dict[str, object] | None = None, files: dict[str, BytesIO] | None = None, method: Literal["get", "post"] = "post", extensions: dict[str, Any] | None = None, ) -> dict[str, object] | None: if query is None: assert files is None assert variables is None return None body: dict[str, object] = {"query": query} if operation_name is not None: body["operationName"] = operation_name if variables is not None: body["variables"] = variables if extensions is not None: body["extensions"] = extensions if files: assert variables is not None file_map = self._build_multipart_file_map(variables, files) body = { "operations": json.dumps(body), "map": json.dumps(file_map), } if method == "get" and variables: body["variables"] = json.dumps(variables) if method == "get" and extensions: body["extensions"] = json.dumps(extensions) return body @staticmethod def _build_multipart_file_map( variables: dict[str, object], files: dict[str, BytesIO] ) -> dict[str, list[str]]: # TODO: remove code duplication files_map: dict[str, list[str]] = {} for key, values in variables.items(): if isinstance(values, dict): folder_key = next(iter(values.keys())) key += f".{folder_key}" # noqa: PLW2901 # the list of file is inside the folder keyword values = values[folder_key] # noqa: PLW2901 # If the variable is an array of files we must number the keys if isinstance(values, list): # copying `files` as when we map a file we must discard from the dict _kwargs = files.copy() for index, _ in enumerate(values): k = next(iter(_kwargs.keys())) _kwargs.pop(k) files_map.setdefault(k, []) files_map[k].append(f"variables.{key}.{index}") else: files_map[key] = [f"variables.{key}"] return files_map def ws_connect( self, url: str, *, protocols: list[str], ) -> contextlib.AbstractAsyncContextManager["WebSocketClient"]: raise NotImplementedError @dataclass class Message: type: Any data: Any extra: str | None = None def json(self) -> Any: return json.loads(self.data) class WebSocketClient(abc.ABC): def name(self) -> str: return "" @abc.abstractmethod async def send_text(self, payload: str) -> None: ... @abc.abstractmethod async def send_json(self, payload: Mapping[str, object]) -> None: ... @abc.abstractmethod async def send_bytes(self, payload: bytes) -> None: ... @abc.abstractmethod async def receive(self, timeout: float | None = None) -> Message: ... @abc.abstractmethod async def receive_json(self, timeout: float | None = None) -> Any: ... @abc.abstractmethod async def close(self) -> None: ... @property @abc.abstractmethod def accepted_subprotocol(self) -> str | None: ... @property @abc.abstractmethod def closed(self) -> bool: ... @property @abc.abstractmethod def close_code(self) -> int: ... @property @abc.abstractmethod def close_reason(self) -> str | None: ... async def __aiter__(self) -> AsyncGenerator[Message, None]: while not self.closed: yield await self.receive() async def send_message(self, message: GraphQLTransportWSMessage) -> None: await self.send_json(message) async def send_legacy_message(self, message: OperationMessage) -> None: await self.send_json(message) class DebuggableGraphQLTransportWSHandler( BaseGraphQLTransportWSHandler[dict[str, object], object] ): def on_init(self) -> None: """This method can be patched by unit tests to get the instance of the transport handler when it is initialized. """ def __init__(self, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) self.original_context = kwargs.get("context", {}) DebuggableGraphQLTransportWSHandler.on_init(self) def get_tasks(self) -> list: return [op.task for op in self.operations.values()] @property def context(self): self.original_context["ws"] = self.websocket self.original_context["get_tasks"] = self.get_tasks self.original_context["connectionInitTimeoutTask"] = ( self.connection_init_timeout_task ) return self.original_context @context.setter def context(self, value): self.original_context = value class DebuggableGraphQLWSHandler(BaseGraphQLWSHandler[dict[str, object], object]): def __init__(self, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) self.original_context = kwargs.get("context", {}) def get_tasks(self) -> list: return list(self.tasks.values()) @property def context(self): self.original_context["ws"] = self.websocket self.original_context["get_tasks"] = self.get_tasks self.original_context["connectionInitTimeoutTask"] = None return self.original_context @context.setter def context(self, value): self.original_context = value strawberry-graphql-0.287.0/tests/http/clients/chalice.py000066400000000000000000000117141511033167500232620ustar00rootroot00000000000000from __future__ import annotations import urllib.parse from io import BytesIO from json import dumps from typing import Any, Literal from chalice.app import Chalice from chalice.app import Request as ChaliceRequest from chalice.test import Client from strawberry import Schema from strawberry.chalice.views import GraphQLView as BaseGraphQLView from strawberry.http import GraphQLHTTPResponse from strawberry.http.ides import GraphQL_IDE from strawberry.http.temporal_response import TemporalResponse from strawberry.schema.config import StrawberryConfig from strawberry.types import ExecutionResult from tests.http.context import get_context from tests.views.schema import Query from .base import JSON, HttpClient, Response, ResultOverrideFunction class GraphQLView(BaseGraphQLView[dict[str, object], object]): result_override: ResultOverrideFunction = None def get_root_value(self, request: ChaliceRequest) -> Query: super().get_root_value(request) # for coverage return Query() def get_context( self, request: ChaliceRequest, response: TemporalResponse ) -> dict[str, object]: context = super().get_context(request, response) return get_context(context) def process_result( self, request: ChaliceRequest, result: ExecutionResult ) -> GraphQLHTTPResponse: if self.result_override: return self.result_override(result) return super().process_result(request, result) class ChaliceHttpClient(HttpClient): def __init__( self, schema: Schema, graphiql: bool | None = None, graphql_ide: GraphQL_IDE | None = "graphiql", allow_queries_via_get: bool = True, result_override: ResultOverrideFunction = None, multipart_uploads_enabled: bool = False, schema_config: StrawberryConfig | None = None, ): self.app = Chalice(app_name="TheStackBadger") view = GraphQLView( schema=schema, graphiql=graphiql, graphql_ide=graphql_ide, allow_queries_via_get=allow_queries_via_get, ) view.result_override = result_override @self.app.route( "/graphql", methods=["GET", "POST"], content_types=["application/json"] ) def handle_graphql(): assert self.app.current_request is not None return view.execute_request(self.app.current_request) async def _graphql_request( self, method: Literal["get", "post"], query: str | None = None, operation_name: str | None = None, variables: dict[str, object] | None = None, files: dict[str, BytesIO] | None = None, headers: dict[str, str] | None = None, extensions: dict[str, Any] | None = None, **kwargs: Any, ) -> Response: body = self._build_body( query=query, operation_name=operation_name, variables=variables, files=files, method=method, extensions=extensions, ) data: dict[str, object] | str | None = None if body and files: body.update({name: (file, name) for name, file in files.items()}) url = "/graphql" if method == "get": body_encoded = urllib.parse.urlencode(body or {}) url = f"{url}?{body_encoded}" else: if body: data = body if files else dumps(body) kwargs["body"] = data with Client(self.app) as client: response = getattr(client.http, method)( url, headers=self._get_headers(method=method, headers=headers, files=files), **kwargs, ) return Response( status_code=response.status_code, data=response.body, headers=response.headers, ) async def request( self, url: str, method: Literal["head", "get", "post", "patch", "put", "delete"], headers: dict[str, str] | None = None, ) -> Response: with Client(self.app) as client: response = getattr(client.http, method)(url, headers=headers) return Response( status_code=response.status_code, data=response.body, headers=response.headers, ) async def get( self, url: str, headers: dict[str, str] | None = None, ) -> Response: return await self.request(url, "get", headers=headers) async def post( self, url: str, data: bytes | None = None, json: JSON | None = None, headers: dict[str, str] | None = None, ) -> Response: body = dumps(json) if json is not None else data with Client(self.app) as client: response = client.http.post(url, headers=headers, body=body) return Response( status_code=response.status_code, data=response.body, headers=response.headers, ) strawberry-graphql-0.287.0/tests/http/clients/channels.py000066400000000000000000000266111511033167500234670ustar00rootroot00000000000000from __future__ import annotations import contextlib import json as json_module from collections.abc import AsyncGenerator, Mapping, Sequence from datetime import timedelta from io import BytesIO from typing import Any, Literal from channels.testing import HttpCommunicator, WebsocketCommunicator from urllib3 import encode_multipart_formdata from strawberry.channels import ( GraphQLHTTPConsumer, GraphQLWSConsumer, SyncGraphQLHTTPConsumer, ) from strawberry.channels.handlers.http_handler import ChannelsRequest from strawberry.http import GraphQLHTTPResponse from strawberry.http.ides import GraphQL_IDE from strawberry.http.temporal_response import TemporalResponse from strawberry.schema import Schema from strawberry.subscriptions import ( GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL, ) from strawberry.types import ExecutionResult from tests.http.context import get_context from tests.views.schema import Query from tests.websockets.views import OnWSConnectMixin from .base import ( JSON, DebuggableGraphQLTransportWSHandler, DebuggableGraphQLWSHandler, HttpClient, Message, Response, ResultOverrideFunction, WebSocketClient, ) def generate_get_path( path: str, query: str, variables: dict[str, Any] | None = None, extensions: dict[str, Any] | None = None, ) -> str: body: dict[str, Any] = {"query": query} if variables is not None: body["variables"] = json_module.dumps(variables) if extensions is not None: body["extensions"] = json_module.dumps(extensions) parts = [f"{k}={v}" for k, v in body.items()] return f"{path}?{'&'.join(parts)}" def create_multipart_request_body( body: dict[str, object], files: dict[str, BytesIO] ) -> tuple[list[tuple[str, str]], bytes]: fields = { "operations": body["operations"], "map": body["map"], } for filename, data in files.items(): fields[filename] = (filename, data.read().decode(), "text/plain") request_body, content_type_header = encode_multipart_formdata(fields) headers = [ ("Content-Type", content_type_header), ("Content-Length", f"{len(request_body)}"), ] return headers, request_body class DebuggableGraphQLHTTPConsumer(GraphQLHTTPConsumer[dict[str, object], object]): result_override: ResultOverrideFunction = None def __init__(self, *args: Any, **kwargs: Any): self.result_override = kwargs.pop("result_override") super().__init__(*args, **kwargs) async def get_root_value(self, request: ChannelsRequest): return Query() async def get_context(self, request: ChannelsRequest, response: TemporalResponse): context = await super().get_context(request, response) return get_context(context) async def process_result( self, request: ChannelsRequest, result: ExecutionResult ) -> GraphQLHTTPResponse: if self.result_override: return self.result_override(result) return await super().process_result(request, result) class DebuggableSyncGraphQLHTTPConsumer( SyncGraphQLHTTPConsumer[dict[str, object], object] ): result_override: ResultOverrideFunction = None def __init__(self, *args: Any, **kwargs: Any): self.result_override = kwargs.pop("result_override") super().__init__(*args, **kwargs) def get_root_value(self, request: ChannelsRequest): return Query() def get_context(self, request: ChannelsRequest, response: TemporalResponse): context = super().get_context(request, response) return get_context(context) def process_result( self, request: ChannelsRequest, result: ExecutionResult ) -> GraphQLHTTPResponse: if self.result_override: return self.result_override(result) return super().process_result(request, result) class DebuggableGraphQLWSConsumer( OnWSConnectMixin, GraphQLWSConsumer[dict[str, object], object] ): graphql_transport_ws_handler_class = DebuggableGraphQLTransportWSHandler graphql_ws_handler_class = DebuggableGraphQLWSHandler async def get_context( self, request: GraphQLWSConsumer, response: GraphQLWSConsumer ): context = await super().get_context(request, response) return get_context(context) class ChannelsHttpClient(HttpClient): """A client to test websockets over channels.""" def __init__( self, schema: Schema, graphiql: bool | None = None, graphql_ide: GraphQL_IDE | None = "graphiql", allow_queries_via_get: bool = True, keep_alive: bool = False, keep_alive_interval: float = 1, subscription_protocols: Sequence[str] = ( GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL, ), connection_init_wait_timeout: timedelta = timedelta(minutes=1), result_override: ResultOverrideFunction = None, multipart_uploads_enabled: bool = False, ): self.ws_app = DebuggableGraphQLWSConsumer.as_asgi( schema=schema, keep_alive=keep_alive, keep_alive_interval=keep_alive_interval, subscription_protocols=subscription_protocols, connection_init_wait_timeout=connection_init_wait_timeout, ) self.http_app = DebuggableGraphQLHTTPConsumer.as_asgi( schema=schema, graphiql=graphiql, graphql_ide=graphql_ide, allow_queries_via_get=allow_queries_via_get, result_override=result_override, multipart_uploads_enabled=multipart_uploads_enabled, ) async def _graphql_request( self, method: Literal["get", "post"], query: str | None = None, operation_name: str | None = None, variables: dict[str, object] | None = None, files: dict[str, BytesIO] | None = None, headers: dict[str, str] | None = None, extensions: dict[str, Any] | None = None, **kwargs: Any, ) -> Response: body = self._build_body( query=query, operation_name=operation_name, variables=variables, files=files, method=method, extensions=extensions, ) headers = self._get_headers(method=method, headers=headers, files=files) if method == "post": if body and files: header_pairs, body = create_multipart_request_body(body, files) headers = dict(header_pairs) else: body = json_module.dumps(body).encode() endpoint_url = "/graphql" else: body = b"" endpoint_url = generate_get_path("/graphql", query, variables, extensions) return await self.request( url=endpoint_url, method=method, body=body, headers=headers ) async def request( self, url: str, method: Literal["head", "get", "post", "patch", "put", "delete"], headers: dict[str, str] | None = None, body: bytes = b"", ) -> Response: # HttpCommunicator expects tuples of bytestrings header_tuples = ( [(k.encode(), v.encode()) for k, v in headers.items()] if headers else [] ) communicator = HttpCommunicator( self.http_app, method.upper(), url, body=body, headers=header_tuples, ) response = await communicator.get_response() return Response( status_code=response["status"], data=response["body"], headers={k.decode(): v.decode() for k, v in response["headers"]}, ) async def get( self, url: str, headers: dict[str, str] | None = None, ) -> Response: return await self.request(url, "get", headers=headers) async def post( self, url: str, data: bytes | None = None, json: JSON | None = None, headers: dict[str, str] | None = None, ) -> Response: body = b"" if data is not None: body = data elif json is not None: body = json_module.dumps(json).encode() return await self.request(url, "post", body=body, headers=headers) @contextlib.asynccontextmanager async def ws_connect( self, url: str, *, protocols: list[str], ) -> AsyncGenerator[WebSocketClient, None]: client = WebsocketCommunicator(self.ws_app, url, subprotocols=protocols) connected, subprotocol_or_close_code = await client.connect() assert connected try: yield ChannelsWebSocketClient( client, accepted_subprotocol=subprotocol_or_close_code ) finally: await client.disconnect() class SyncChannelsHttpClient(ChannelsHttpClient): def __init__( self, schema: Schema, graphiql: bool | None = None, graphql_ide: GraphQL_IDE | None = "graphiql", allow_queries_via_get: bool = True, result_override: ResultOverrideFunction = None, multipart_uploads_enabled: bool = False, ): self.http_app = DebuggableSyncGraphQLHTTPConsumer.as_asgi( schema=schema, graphiql=graphiql, graphql_ide=graphql_ide, allow_queries_via_get=allow_queries_via_get, result_override=result_override, multipart_uploads_enabled=multipart_uploads_enabled, ) class ChannelsWebSocketClient(WebSocketClient): def __init__(self, client: WebsocketCommunicator, accepted_subprotocol: str | None): self.ws = client self._closed: bool = False self._close_code: int | None = None self._close_reason: str | None = None self._accepted_subprotocol = accepted_subprotocol def name(self) -> str: return "channels" async def send_text(self, payload: str) -> None: await self.ws.send_to(text_data=payload) async def send_json(self, payload: Mapping[str, object]) -> None: await self.ws.send_json_to(payload) async def send_bytes(self, payload: bytes) -> None: await self.ws.send_to(bytes_data=payload) async def receive(self, timeout: float | None = None) -> Message: m = await self.ws.receive_output(timeout=timeout) # type: ignore if m["type"] == "websocket.close": self._closed = True self._close_code = m["code"] self._close_reason = m.get("reason") return Message(type=m["type"], data=m["code"], extra=m.get("reason")) if m["type"] == "websocket.send": return Message(type=m["type"], data=m["text"]) return Message(type=m["type"], data=m["data"], extra=m["extra"]) async def receive_json(self, timeout: float | None = None) -> Any: m = await self.ws.receive_output(timeout=timeout) # type: ignore assert m["type"] == "websocket.send" assert "text" in m return json_module.loads(m["text"]) async def close(self) -> None: await self.ws.disconnect() self._closed = True @property def accepted_subprotocol(self) -> str | None: return self._accepted_subprotocol @property def closed(self) -> bool: return self._closed @property def close_code(self) -> int: assert self._close_code is not None return self._close_code @property def close_reason(self) -> str | None: return self._close_reason strawberry-graphql-0.287.0/tests/http/clients/django.py000066400000000000000000000133571511033167500231410ustar00rootroot00000000000000from __future__ import annotations from io import BytesIO from json import dumps from typing import Any, Literal from django.core.exceptions import BadRequest, SuspiciousOperation from django.core.files.uploadedfile import SimpleUploadedFile from django.http import Http404, HttpRequest, HttpResponse from django.test.client import RequestFactory from strawberry.django.views import GraphQLView as BaseGraphQLView from strawberry.http import GraphQLHTTPResponse from strawberry.http.ides import GraphQL_IDE from strawberry.schema import Schema from strawberry.types import ExecutionResult from tests.http.context import get_context from tests.views.schema import Query from .base import JSON, HttpClient, Response, ResultOverrideFunction class GraphQLView(BaseGraphQLView[dict[str, object], object]): result_override: ResultOverrideFunction = None def get_root_value(self, request) -> Query: super().get_root_value(request) # for coverage return Query() def get_context( self, request: HttpRequest, response: HttpResponse ) -> dict[str, object]: context = {"request": request, "response": response} return get_context(context) def process_result( self, request: HttpRequest, result: ExecutionResult ) -> GraphQLHTTPResponse: if self.result_override: return self.result_override(result) return super().process_result(request, result) class DjangoHttpClient(HttpClient): def __init__( self, schema: Schema, graphiql: bool | None = None, graphql_ide: GraphQL_IDE | None = "graphiql", allow_queries_via_get: bool = True, result_override: ResultOverrideFunction = None, multipart_uploads_enabled: bool = False, ): self.view = GraphQLView.as_view( schema=schema, graphiql=graphiql, graphql_ide=graphql_ide, allow_queries_via_get=allow_queries_via_get, result_override=result_override, multipart_uploads_enabled=multipart_uploads_enabled, ) def _get_header_name(self, key: str) -> str: return f"HTTP_{key.upper().replace('-', '_')}" def _to_django_headers(self, headers: dict[str, str]) -> dict[str, str]: return {self._get_header_name(key): value for key, value in headers.items()} def _get_headers( self, method: Literal["get", "post"], headers: dict[str, str] | None, files: dict[str, BytesIO] | None, ) -> dict[str, str]: headers = headers or {} headers = self._to_django_headers(headers) return super()._get_headers(method=method, headers=headers, files=files) async def _do_request(self, request: HttpRequest) -> Response: try: response = self.view(request) except Http404: return Response(status_code=404, data=b"Not found") except (BadRequest, SuspiciousOperation) as e: return Response(status_code=400, data=e.args[0].encode()) else: return Response( status_code=response.status_code, data=response.content, headers=dict(response.headers), ) async def _graphql_request( self, method: Literal["get", "post"], query: str | None = None, operation_name: str | None = None, variables: dict[str, object] | None = None, files: dict[str, BytesIO] | None = None, headers: dict[str, str] | None = None, extensions: dict[str, Any] | None = None, **kwargs: Any, ) -> Response: headers = self._get_headers(method=method, headers=headers, files=files) additional_arguments = {**kwargs, **headers} body = self._build_body( query=query, operation_name=operation_name, variables=variables, files=files, method=method, extensions=extensions, ) data: dict[str, object] | str | None = None if body and files: body.update( { name: SimpleUploadedFile(name, file.read()) for name, file in files.items() } ) else: additional_arguments["content_type"] = "application/json" if body: data = body if files or method == "get" else dumps(body) factory = RequestFactory() request = getattr(factory, method)( "/graphql", data=data, **additional_arguments, ) return await self._do_request(request) async def request( self, url: str, method: Literal["head", "get", "post", "patch", "put", "delete"], headers: dict[str, str] | None = None, ) -> Response: headers = headers or {} factory = RequestFactory() request = getattr(factory, method)(url, **headers) return await self._do_request(request) async def get( self, url: str, headers: dict[str, str] | None = None, ) -> Response: django_headers = self._to_django_headers(headers or {}) return await self.request(url, "get", headers=django_headers) async def post( self, url: str, data: bytes | None = None, json: JSON | None = None, headers: dict[str, str] | None = None, ) -> Response: headers = headers or {} content_type = headers.pop("Content-Type", "") body = dumps(json) if json is not None else data factory = RequestFactory() request = factory.post( url, data=body, content_type=content_type, headers=headers, ) return await self._do_request(request) strawberry-graphql-0.287.0/tests/http/clients/fastapi.py000066400000000000000000000136761511033167500233320ustar00rootroot00000000000000from __future__ import annotations import contextlib import json from collections.abc import AsyncGenerator, Sequence from datetime import timedelta from io import BytesIO from typing import Any, Literal from fastapi import BackgroundTasks, Depends, FastAPI, Request, WebSocket from fastapi.testclient import TestClient from strawberry.fastapi import GraphQLRouter as BaseGraphQLRouter from strawberry.http import GraphQLHTTPResponse from strawberry.http.ides import GraphQL_IDE from strawberry.schema import Schema from strawberry.subscriptions import ( GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL, ) from strawberry.types import ExecutionResult from tests.http.context import get_context from tests.views.schema import Query from tests.websockets.views import OnWSConnectMixin from .asgi import AsgiWebSocketClient from .base import ( JSON, DebuggableGraphQLTransportWSHandler, DebuggableGraphQLWSHandler, HttpClient, Response, ResultOverrideFunction, WebSocketClient, ) def custom_context_dependency() -> str: return "Hi!" def fastapi_get_context( background_tasks: BackgroundTasks, request: Request = None, # type: ignore ws: WebSocket = None, # type: ignore custom_value: str = Depends(custom_context_dependency), ) -> dict[str, object]: return get_context( { "request": request or ws, "background_tasks": background_tasks, } ) def get_root_value( request: Request = None, # type: ignore - FastAPI ws: WebSocket = None, # type: ignore - FastAPI ) -> Query: return Query() class GraphQLRouter(OnWSConnectMixin, BaseGraphQLRouter[dict[str, object], object]): result_override: ResultOverrideFunction = None graphql_transport_ws_handler_class = DebuggableGraphQLTransportWSHandler graphql_ws_handler_class = DebuggableGraphQLWSHandler async def process_result( self, request: Request, result: ExecutionResult ) -> GraphQLHTTPResponse: if self.result_override: return self.result_override(result) return await super().process_result(request, result) class FastAPIHttpClient(HttpClient): def __init__( self, schema: Schema, graphiql: bool | None = None, graphql_ide: GraphQL_IDE | None = "graphiql", allow_queries_via_get: bool = True, keep_alive: bool = False, keep_alive_interval: float = 1, subscription_protocols: Sequence[str] = ( GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL, ), connection_init_wait_timeout: timedelta = timedelta(minutes=1), result_override: ResultOverrideFunction = None, multipart_uploads_enabled: bool = False, ): self.app = FastAPI() graphql_app = GraphQLRouter( schema, graphiql=graphiql, graphql_ide=graphql_ide, allow_queries_via_get=allow_queries_via_get, keep_alive=keep_alive, keep_alive_interval=keep_alive_interval, subscription_protocols=subscription_protocols, connection_init_wait_timeout=connection_init_wait_timeout, multipart_uploads_enabled=multipart_uploads_enabled, context_getter=fastapi_get_context, root_value_getter=get_root_value, ) graphql_app.result_override = result_override self.app.include_router(graphql_app, prefix="/graphql") self.client = TestClient(self.app) async def _handle_response(self, response: Any) -> Response: # TODO: here we should handle the stream return Response( status_code=response.status_code, data=response.content, headers=response.headers, ) async def _graphql_request( self, method: Literal["get", "post"], query: str | None = None, operation_name: str | None = None, variables: dict[str, object] | None = None, files: dict[str, BytesIO] | None = None, headers: dict[str, str] | None = None, extensions: dict[str, Any] | None = None, **kwargs: Any, ) -> Response: body = self._build_body( query=query, operation_name=operation_name, variables=variables, files=files, method=method, extensions=extensions, ) if body: if method == "get": kwargs["params"] = body elif files: kwargs["data"] = body else: kwargs["content"] = json.dumps(body) if files: kwargs["files"] = files response = getattr(self.client, method)( "/graphql", headers=self._get_headers(method=method, headers=headers, files=files), **kwargs, ) return await self._handle_response(response) async def request( self, url: str, method: Literal["head", "get", "post", "patch", "put", "delete"], headers: dict[str, str] | None = None, ) -> Response: response = getattr(self.client, method)(url, headers=headers) return await self._handle_response(response) async def get( self, url: str, headers: dict[str, str] | None = None, ) -> Response: return await self.request(url, "get", headers=headers) async def post( self, url: str, data: bytes | None = None, json: JSON | None = None, headers: dict[str, str] | None = None, ) -> Response: response = self.client.post(url, headers=headers, content=data, json=json) return await self._handle_response(response) @contextlib.asynccontextmanager async def ws_connect( self, url: str, *, protocols: list[str], ) -> AsyncGenerator[WebSocketClient, None]: with self.client.websocket_connect(url, protocols) as ws: yield AsgiWebSocketClient(ws) strawberry-graphql-0.287.0/tests/http/clients/flask.py000066400000000000000000000122511511033167500227670ustar00rootroot00000000000000from __future__ import annotations import asyncio import contextvars import functools import json import urllib.parse from io import BytesIO from typing import Any, Literal from flask import Flask from flask import Request as FlaskRequest from flask import Response as FlaskResponse from strawberry.flask.views import GraphQLView as BaseGraphQLView from strawberry.http import GraphQLHTTPResponse from strawberry.http.ides import GraphQL_IDE from strawberry.schema import Schema from strawberry.types import ExecutionResult from tests.http.context import get_context from tests.views.schema import Query from .base import JSON, HttpClient, Response, ResultOverrideFunction class GraphQLView(BaseGraphQLView[dict[str, object], object]): # this allows to test our code path for checking the request type # TODO: we might want to remove our check since it is done by flask # already methods = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"] result_override: ResultOverrideFunction = None def __init__(self, *args: Any, **kwargs: Any): self.result_override = kwargs.pop("result_override") super().__init__(*args, **kwargs) def get_root_value(self, request: FlaskRequest) -> object: super().get_root_value(request) # for coverage return Query() def get_context( self, request: FlaskRequest, response: FlaskResponse ) -> dict[str, object]: context = super().get_context(request, response) return get_context(context) def process_result( self, request: FlaskRequest, result: ExecutionResult ) -> GraphQLHTTPResponse: if self.result_override: return self.result_override(result) return super().process_result(request, result) class FlaskHttpClient(HttpClient): def __init__( self, schema: Schema, graphiql: bool | None = None, graphql_ide: GraphQL_IDE | None = "graphiql", allow_queries_via_get: bool = True, result_override: ResultOverrideFunction = None, multipart_uploads_enabled: bool = False, ): self.app = Flask(__name__) self.app.debug = True view = GraphQLView.as_view( "graphql_view", schema=schema, graphiql=graphiql, graphql_ide=graphql_ide, allow_queries_via_get=allow_queries_via_get, result_override=result_override, multipart_uploads_enabled=multipart_uploads_enabled, ) self.app.add_url_rule( "/graphql", view_func=view, ) async def _graphql_request( self, method: Literal["get", "post"], query: str | None = None, operation_name: str | None = None, variables: dict[str, object] | None = None, files: dict[str, BytesIO] | None = None, headers: dict[str, str] | None = None, extensions: dict[str, Any] | None = None, **kwargs: Any, ) -> Response: body = self._build_body( query=query, operation_name=operation_name, variables=variables, files=files, method=method, extensions=extensions, ) data: dict[str, object] | str | None = None if body and files: body.update({name: (file, name) for name, file in files.items()}) url = "/graphql" if method == "get": body_encoded = urllib.parse.urlencode(body or {}) url = f"{url}?{body_encoded}" else: if body: data = body if files else json.dumps(body) kwargs["data"] = data headers = self._get_headers(method=method, headers=headers, files=files) return await self.request(url, method, headers=headers, **kwargs) def _do_request( self, url: str, method: Literal["get", "post", "patch", "put", "delete"], headers: dict[str, str] | None = None, **kwargs: Any, ): with self.app.test_client() as client: response = getattr(client, method)(url, headers=headers, **kwargs) return Response( status_code=response.status_code, data=response.data, headers=response.headers, ) async def request( self, url: str, method: Literal["head", "get", "post", "patch", "put", "delete"], headers: dict[str, str] | None = None, **kwargs: Any, ) -> Response: loop = asyncio.get_running_loop() ctx = contextvars.copy_context() func_call = functools.partial( ctx.run, self._do_request, url=url, method=method, headers=headers, **kwargs ) return await loop.run_in_executor(None, func_call) # type: ignore async def get( self, url: str, headers: dict[str, str] | None = None, ) -> Response: return await self.request(url, "get", headers=headers) async def post( self, url: str, data: bytes | None = None, json: JSON | None = None, headers: dict[str, str] | None = None, ) -> Response: return await self.request(url, "post", headers=headers, data=data, json=json) strawberry-graphql-0.287.0/tests/http/clients/litestar.py000066400000000000000000000177211511033167500235250ustar00rootroot00000000000000from __future__ import annotations import contextlib import json from collections.abc import AsyncGenerator, Mapping, Sequence from datetime import timedelta from io import BytesIO from typing import Any, Literal from litestar import Litestar, Request from litestar.exceptions import WebSocketDisconnect from litestar.testing import TestClient from litestar.testing.websocket_test_session import WebSocketTestSession from strawberry.http import GraphQLHTTPResponse from strawberry.http.ides import GraphQL_IDE from strawberry.litestar import make_graphql_controller from strawberry.schema import Schema from strawberry.subscriptions import ( GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL, ) from strawberry.types import ExecutionResult from tests.http.context import get_context from tests.views.schema import Query from tests.websockets.views import OnWSConnectMixin from .base import ( JSON, DebuggableGraphQLTransportWSHandler, DebuggableGraphQLWSHandler, HttpClient, Message, Response, ResultOverrideFunction, WebSocketClient, ) def custom_context_dependency() -> str: return "Hi!" async def litestar_get_context(request: Request = None): return get_context({"request": request}) async def get_root_value(request: Request = None): return Query() class LitestarHttpClient(HttpClient): def __init__( self, schema: Schema, graphiql: bool | None = None, graphql_ide: GraphQL_IDE | None = "graphiql", allow_queries_via_get: bool = True, keep_alive: bool = False, keep_alive_interval: float = 1, debug: bool = False, subscription_protocols: Sequence[str] = ( GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL, ), connection_init_wait_timeout: timedelta = timedelta(minutes=1), result_override: ResultOverrideFunction = None, multipart_uploads_enabled: bool = False, ): BaseGraphQLController = make_graphql_controller( schema=schema, graphiql=graphiql, graphql_ide=graphql_ide, allow_queries_via_get=allow_queries_via_get, keep_alive=keep_alive, keep_alive_interval=keep_alive_interval, subscription_protocols=subscription_protocols, connection_init_wait_timeout=connection_init_wait_timeout, multipart_uploads_enabled=multipart_uploads_enabled, path="/graphql", context_getter=litestar_get_context, root_value_getter=get_root_value, ) class GraphQLController(OnWSConnectMixin, BaseGraphQLController): graphql_transport_ws_handler_class = DebuggableGraphQLTransportWSHandler graphql_ws_handler_class = DebuggableGraphQLWSHandler async def process_result( self, request: Request, result: ExecutionResult ) -> GraphQLHTTPResponse: if result_override: return result_override(result) return await super().process_result(request, result) self.app = Litestar(route_handlers=[GraphQLController]) self.client = TestClient(self.app) async def _graphql_request( self, method: Literal["get", "post"], query: str | None = None, operation_name: str | None = None, variables: dict[str, object] | None = None, files: dict[str, BytesIO] | None = None, headers: dict[str, str] | None = None, extensions: dict[str, Any] | None = None, **kwargs: Any, ) -> Response: if body := self._build_body( query=query, operation_name=operation_name, variables=variables, files=files, method=method, extensions=extensions, ): if method == "get": kwargs["params"] = body elif files: kwargs["data"] = body else: kwargs["content"] = json.dumps(body) if files: kwargs["files"] = files response = getattr(self.client, method)( "/graphql", headers=self._get_headers(method=method, headers=headers, files=files), **kwargs, ) return Response( status_code=response.status_code, data=response.content, headers=response.headers, ) async def request( self, url: str, method: Literal["head", "get", "post", "patch", "put", "delete"], headers: dict[str, str] | None = None, ) -> Response: response = getattr(self.client, method)(url, headers=headers) return Response( status_code=response.status_code, data=response.content, headers=response.headers, ) async def get( self, url: str, headers: dict[str, str] | None = None, ) -> Response: return await self.request(url, "get", headers=headers) async def post( self, url: str, data: bytes | None = None, json: JSON | None = None, headers: dict[str, str] | None = None, ) -> Response: response = self.client.post(url, headers=headers, content=data, json=json) return Response( status_code=response.status_code, data=response.content, headers=dict(response.headers), ) @contextlib.asynccontextmanager async def ws_connect( self, url: str, *, protocols: list[str], ) -> AsyncGenerator[WebSocketClient, None]: with self.client.websocket_connect(url, protocols) as ws: yield LitestarWebSocketClient(ws) class LitestarWebSocketClient(WebSocketClient): def __init__(self, ws: WebSocketTestSession): self.ws = ws self._closed: bool = False self._close_code: int | None = None self._close_reason: str | None = None async def send_text(self, payload: str) -> None: self.ws.send_text(payload) async def send_json(self, payload: Mapping[str, object]) -> None: self.ws.send_json(payload) async def send_bytes(self, payload: bytes) -> None: self.ws.send_bytes(payload) async def receive(self, timeout: float | None = None) -> Message: if self._closed: # if close was received via exception, fake it so that recv works return Message( type="websocket.close", data=self._close_code, extra=self._close_reason ) try: m = self.ws.receive() except WebSocketDisconnect as exc: self._closed = True self._close_code = exc.code self._close_reason = exc.detail return Message(type="websocket.close", data=exc.code, extra=exc.detail) if m["type"] == "websocket.close": # Probably never happens self._closed = True self._close_code = m["code"] self._close_reason = m["reason"] return Message(type=m["type"], data=m["code"], extra=m["reason"]) if m["type"] == "websocket.send": return Message(type=m["type"], data=m["text"]) assert "data" in m return Message(type=m["type"], data=m["data"], extra=m["extra"]) async def receive_json(self, timeout: float | None = None) -> Any: m = self.ws.receive() assert m["type"] == "websocket.send" assert "text" in m assert m["text"] is not None return json.loads(m["text"]) async def close(self) -> None: self.ws.close() self._closed = True @property def accepted_subprotocol(self) -> str | None: return self.ws.accepted_subprotocol @property def closed(self) -> bool: return self._closed @property def close_code(self) -> int: assert self._close_code is not None return self._close_code @property def close_reason(self) -> str | None: return self._close_reason strawberry-graphql-0.287.0/tests/http/clients/quart.py000066400000000000000000000157661511033167500230410ustar00rootroot00000000000000import contextlib import json import urllib.parse from collections.abc import AsyncGenerator, Sequence from datetime import timedelta from io import BytesIO from typing import Any, Literal from quart import Quart from quart import Request as QuartRequest from quart import Response as QuartResponse from quart import Websocket as QuartWebsocket from quart.datastructures import FileStorage from starlette.testclient import TestClient from starlette.types import Receive, Scope, Send from strawberry.http import GraphQLHTTPResponse from strawberry.http.ides import GraphQL_IDE from strawberry.quart.views import GraphQLView as BaseGraphQLView from strawberry.schema import Schema from strawberry.subscriptions import ( GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL, ) from strawberry.types import ExecutionResult from tests.http.context import get_context from tests.views.schema import Query from tests.websockets.views import OnWSConnectMixin from .asgi import AsgiWebSocketClient from .base import ( JSON, DebuggableGraphQLTransportWSHandler, DebuggableGraphQLWSHandler, HttpClient, Response, ResultOverrideFunction, WebSocketClient, ) class GraphQLView(OnWSConnectMixin, BaseGraphQLView[dict[str, object], object]): methods = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"] result_override: ResultOverrideFunction = None graphql_transport_ws_handler_class = DebuggableGraphQLTransportWSHandler graphql_ws_handler_class = DebuggableGraphQLWSHandler def __init__(self, *args: Any, **kwargs: Any): self.result_override = kwargs.pop("result_override", None) super().__init__(*args, **kwargs) async def get_root_value(self, request: QuartRequest | QuartWebsocket) -> Query: await super().get_root_value(request) # for coverage return Query() async def get_context( self, request: QuartRequest | QuartWebsocket, response: QuartResponse ) -> dict[str, object]: context = await super().get_context(request, response) return get_context(context) async def process_result( self, request: QuartRequest, result: ExecutionResult ) -> GraphQLHTTPResponse: if self.result_override: return self.result_override(result) return await super().process_result(request, result) class QuartAsgiAppAdapter: def __init__(self, app: Quart): self.app = app async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: scope["asgi"] = scope.get("asgi", {}) # Our WebSocket tests depend on WebSocket close reasons. # Quart only sends close reason if the ASGI spec version in the scope is => 2.3 # https://github.com/pallets/quart/blob/b5593ca4c8c657564cdf2d35c9f0298fce63636b/src/quart/asgi.py#L347-L348 scope["asgi"]["spec_version"] = "2.3" await self.app(scope, receive, send) # type: ignore class QuartHttpClient(HttpClient): def __init__( self, schema: Schema, graphiql: bool | None = None, graphql_ide: GraphQL_IDE | None = "graphiql", allow_queries_via_get: bool = True, keep_alive: bool = False, keep_alive_interval: float = 1, subscription_protocols: Sequence[str] = ( GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL, ), connection_init_wait_timeout: timedelta = timedelta(minutes=1), result_override: ResultOverrideFunction = None, multipart_uploads_enabled: bool = False, ): self.app = Quart(__name__) self.app.debug = True view = GraphQLView.as_view( "graphql_view", schema=schema, graphiql=graphiql, graphql_ide=graphql_ide, allow_queries_via_get=allow_queries_via_get, result_override=result_override, keep_alive=keep_alive, keep_alive_interval=keep_alive_interval, subscription_protocols=subscription_protocols, connection_init_wait_timeout=connection_init_wait_timeout, multipart_uploads_enabled=multipart_uploads_enabled, ) self.app.add_url_rule( "/graphql", view_func=view, ) self.app.add_url_rule( "/graphql", view_func=view, methods=["GET"], websocket=True, ) self.client = TestClient(QuartAsgiAppAdapter(self.app)) async def _graphql_request( self, method: Literal["get", "post"], query: str | None = None, operation_name: str | None = None, variables: dict[str, object] | None = None, files: dict[str, BytesIO] | None = None, headers: dict[str, str] | None = None, extensions: dict[str, Any] | None = None, **kwargs: Any, ) -> Response: body = self._build_body( query=query, operation_name=operation_name, variables=variables, files=files, method=method, extensions=extensions, ) url = "/graphql" if method == "get": body_encoded = urllib.parse.urlencode(body or {}) url = f"{url}?{body_encoded}" elif body: if files: kwargs["form"] = body kwargs["files"] = { k: FileStorage(v, filename=k) for k, v in files.items() } else: kwargs["data"] = json.dumps(body) headers = self._get_headers(method=method, headers=headers, files=files) return await self.request(url, method, headers=headers, **kwargs) async def request( self, url: str, method: Literal["head", "get", "post", "patch", "put", "delete"], headers: dict[str, str] | None = None, **kwargs: Any, ) -> Response: async with self.app.test_app() as test_app, self.app.app_context(): client = test_app.test_client() response = await getattr(client, method)(url, headers=headers, **kwargs) return Response( status_code=response.status_code, data=(await response.data), headers=response.headers, ) async def get( self, url: str, headers: dict[str, str] | None = None, ) -> Response: return await self.request(url, "get", headers=headers) async def post( self, url: str, data: bytes | None = None, json: JSON | None = None, headers: dict[str, str] | None = None, ) -> Response: kwargs = {"headers": headers, "data": data, "json": json} return await self.request( url, "post", **{k: v for k, v in kwargs.items() if v is not None} ) @contextlib.asynccontextmanager async def ws_connect( self, url: str, *, protocols: list[str], ) -> AsyncGenerator[WebSocketClient, None]: with self.client.websocket_connect(url, protocols) as ws: yield AsgiWebSocketClient(ws) strawberry-graphql-0.287.0/tests/http/clients/sanic.py000066400000000000000000000113121511033167500227610ustar00rootroot00000000000000from __future__ import annotations from io import BytesIO from json import dumps from random import randint from typing import Any, Literal from sanic import Sanic from sanic.request import Request as SanicRequest from strawberry.http import GraphQLHTTPResponse from strawberry.http.ides import GraphQL_IDE from strawberry.http.temporal_response import TemporalResponse from strawberry.sanic.views import GraphQLView as BaseGraphQLView from strawberry.schema import Schema from strawberry.types import ExecutionResult from tests.http.context import get_context from tests.views.schema import Query from .base import JSON, HttpClient, Response, ResultOverrideFunction class GraphQLView(BaseGraphQLView[object, Query]): result_override: ResultOverrideFunction = None def __init__(self, *args: Any, **kwargs: Any): self.result_override = kwargs.pop("result_override") super().__init__(*args, **kwargs) async def get_root_value(self, request: SanicRequest) -> Query: await super().get_root_value(request) # for coverage return Query() async def get_context( self, request: SanicRequest, response: TemporalResponse ) -> object: context = await super().get_context(request, response) return get_context(context) async def process_result( self, request: SanicRequest, result: ExecutionResult ) -> GraphQLHTTPResponse: if self.result_override: return self.result_override(result) return await super().process_result(request, result) class SanicHttpClient(HttpClient): def __init__( self, schema: Schema, graphiql: bool | None = None, graphql_ide: GraphQL_IDE | None = "graphiql", allow_queries_via_get: bool = True, result_override: ResultOverrideFunction = None, multipart_uploads_enabled: bool = False, ): self.app = Sanic( f"test_{int(randint(0, 1000))}", # noqa: S311 ) view = GraphQLView.as_view( schema=schema, graphiql=graphiql, graphql_ide=graphql_ide, allow_queries_via_get=allow_queries_via_get, result_override=result_override, multipart_uploads_enabled=multipart_uploads_enabled, ) self.app.add_route(view, "/graphql") async def _graphql_request( self, method: Literal["get", "post"], query: str | None = None, operation_name: str | None = None, variables: dict[str, object] | None = None, files: dict[str, BytesIO] | None = None, headers: dict[str, str] | None = None, extensions: dict[str, Any] | None = None, **kwargs: Any, ) -> Response: body = self._build_body( query=query, operation_name=operation_name, variables=variables, files=files, method=method, extensions=extensions, ) if body: if method == "get": kwargs["params"] = body elif files: kwargs["data"] = body else: kwargs["content"] = dumps(body) _request, response = await self.app.asgi_client.request( method, "/graphql", headers=self._get_headers(method=method, headers=headers, files=files), files=files, **kwargs, ) return Response( status_code=response.status_code, data=response.content, headers=response.headers, ) async def request( self, url: str, method: Literal["head", "get", "post", "patch", "put", "delete"], headers: dict[str, str] | None = None, ) -> Response: _request, response = await self.app.asgi_client.request( method, url, headers=headers, ) return Response( status_code=response.status_code, data=response.content, headers=response.headers, ) async def get( self, url: str, headers: dict[str, str] | None = None, ) -> Response: return await self.request(url, "get", headers=headers) async def post( self, url: str, data: bytes | None = None, json: JSON | None = None, headers: dict[str, str] | None = None, ) -> Response: body = dumps(json) if json is not None else data _request, response = await self.app.asgi_client.request( "post", url, content=body, headers=headers ) return Response( status_code=response.status_code, data=response.content, headers=response.headers, ) strawberry-graphql-0.287.0/tests/http/conftest.py000066400000000000000000000036121511033167500220540ustar00rootroot00000000000000import importlib from collections.abc import Generator from typing import Any import pytest from tests.views.schema import schema from .clients.base import HttpClient def _get_http_client_classes() -> Generator[Any, None, None]: for client, module, marks in [ ("AioHttpClient", "aiohttp", [pytest.mark.aiohttp]), ("AsgiHttpClient", "asgi", [pytest.mark.asgi]), ("AsyncDjangoHttpClient", "async_django", [pytest.mark.django]), ("AsyncFlaskHttpClient", "async_flask", [pytest.mark.flask]), ("ChannelsHttpClient", "channels", [pytest.mark.channels]), ("ChaliceHttpClient", "chalice", [pytest.mark.chalice]), ("DjangoHttpClient", "django", [pytest.mark.django]), ("FastAPIHttpClient", "fastapi", [pytest.mark.fastapi]), ("FlaskHttpClient", "flask", [pytest.mark.flask]), ("QuartHttpClient", "quart", [pytest.mark.quart]), ("SanicHttpClient", "sanic", [pytest.mark.sanic]), ("LitestarHttpClient", "litestar", [pytest.mark.litestar]), ( "SyncChannelsHttpClient", "channels", [pytest.mark.channels, pytest.mark.django_db], ), ]: try: client_class = getattr( importlib.import_module(f".{module}", package="tests.http.clients"), client, ) except ImportError: client_class = None yield pytest.param( client_class, marks=[ *marks, pytest.mark.skipif( client_class is None, reason=f"Client {client} not found" ), ], ) @pytest.fixture(params=_get_http_client_classes()) def http_client_class(request: Any) -> type[HttpClient]: return request.param @pytest.fixture def http_client(http_client_class: type[HttpClient]) -> HttpClient: return http_client_class(schema) strawberry-graphql-0.287.0/tests/http/context.py000066400000000000000000000002341511033167500217100ustar00rootroot00000000000000def get_context(context: object) -> dict[str, object]: assert isinstance(context, dict) return {**context, "custom_value": "a value from context"} strawberry-graphql-0.287.0/tests/http/incremental/000077500000000000000000000000001511033167500221545ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/http/incremental/__init__.py000066400000000000000000000000001511033167500242530ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/http/incremental/conftest.py000066400000000000000000000033701511033167500243560ustar00rootroot00000000000000import contextlib import pytest from tests.http.clients.base import HttpClient from tests.views.schema import schema @pytest.fixture def incremental_http_client_class( http_client_class: type[HttpClient], ) -> type[HttpClient]: with contextlib.suppress(ImportError): import django if django.VERSION < (4, 2): pytest.skip(reason="Django < 4.2 doesn't async streaming responses") from tests.http.clients.django import DjangoHttpClient if http_client_class is DjangoHttpClient: pytest.skip(reason="(sync) DjangoHttpClient doesn't support streaming") with contextlib.suppress(ImportError): from tests.http.clients.channels import SyncChannelsHttpClient # TODO: why do we have a sync channels client? if http_client_class is SyncChannelsHttpClient: pytest.skip(reason="SyncChannelsHttpClient doesn't support streaming") with contextlib.suppress(ImportError): from tests.http.clients.async_flask import AsyncFlaskHttpClient from tests.http.clients.flask import FlaskHttpClient if http_client_class is FlaskHttpClient: pytest.skip(reason="FlaskHttpClient doesn't support streaming") if http_client_class is AsyncFlaskHttpClient: pytest.xfail(reason="AsyncFlaskHttpClient doesn't support streaming") with contextlib.suppress(ImportError): from tests.http.clients.chalice import ChaliceHttpClient if http_client_class is ChaliceHttpClient: pytest.skip(reason="ChaliceHttpClient doesn't support streaming") return http_client_class @pytest.fixture def http_client(incremental_http_client_class: type[HttpClient]) -> HttpClient: return incremental_http_client_class(schema) strawberry-graphql-0.287.0/tests/http/incremental/test_defer.py000066400000000000000000000061671511033167500246640ustar00rootroot00000000000000import contextlib from typing import Literal import pytest from inline_snapshot import snapshot import strawberry from strawberry.extensions.mask_errors import MaskErrors from strawberry.schema.config import StrawberryConfig from tests.conftest import skip_if_gql_32 from tests.http.clients.base import HttpClient from tests.views.schema import Mutation, Query, Subscription pytestmark = skip_if_gql_32("GraphQL 3.3.0 is required for incremental execution") @pytest.mark.parametrize("method", ["get", "post"]) async def test_basic_defer(method: Literal["get", "post"], http_client: HttpClient): response = await http_client.query( method=method, query=""" query HeroNameQuery { character { id ...NameFragment @defer } } fragment NameFragment on Hero { name } """, ) async with contextlib.aclosing(response.streaming_json()) as stream: initial = await stream.__anext__() assert initial == snapshot( { "data": {"character": {"id": "1"}}, "hasNext": True, "pending": [{"path": ["character"], "id": "0"}], # TODO: check if we need this and how to handle it "extensions": None, } ) subsequent = await stream.__anext__() assert subsequent == snapshot( { "incremental": [ { "data": {"name": "Thiago Bellini"}, "id": "0", "path": ["character"], "label": None, } ], "completed": [{"id": "0"}], "hasNext": False, # TODO: same as above "extensions": None, } ) @pytest.mark.parametrize("method", ["get", "post"]) async def test_defer_with_mask_error_extension( method: Literal["get", "post"], incremental_http_client_class: type[HttpClient] ): schema = strawberry.Schema( query=Query, mutation=Mutation, subscription=Subscription, extensions=[MaskErrors()], config=StrawberryConfig(enable_experimental_incremental_execution=(True)), ) http_client = incremental_http_client_class(schema=schema) response = await http_client.query( method=method, query=""" query HeroNameQuery { someError character { id ...NameFragment @defer } } fragment NameFragment on Hero { name(fail: true) } """, ) async with contextlib.aclosing(response.streaming_json()) as stream: initial_errors = (await stream.__anext__())["errors"] assert initial_errors[0]["message"] == "Unexpected error." # TODO: not yet supported properly (the error is not masked) subsequent = (await stream.__anext__())["completed"][0] subsequent_errors = subsequent["errors"] assert subsequent_errors[0]["message"] == "Failed to get name" strawberry-graphql-0.287.0/tests/http/incremental/test_multipart_subscription.py000066400000000000000000000115321511033167500304140ustar00rootroot00000000000000import contextlib from typing import Literal import pytest import strawberry from strawberry.http.base import BaseView from strawberry.schema.config import StrawberryConfig from tests.http.clients.base import HttpClient from tests.views.schema import Mutation, MyExtension, Query, Subscription, schema @pytest.fixture def http_client(http_client_class: type[HttpClient]) -> HttpClient: with contextlib.suppress(ImportError): import django if django.VERSION < (4, 2): pytest.skip(reason="Django < 4.2 doesn't async streaming responses") from tests.http.clients.django import DjangoHttpClient if http_client_class is DjangoHttpClient: pytest.skip( reason="(sync) DjangoHttpClient doesn't support multipart subscriptions" ) with contextlib.suppress(ImportError): from tests.http.clients.channels import SyncChannelsHttpClient # TODO: why do we have a sync channels client? if http_client_class is SyncChannelsHttpClient: pytest.skip( reason="SyncChannelsHttpClient doesn't support multipart subscriptions" ) with contextlib.suppress(ImportError): from tests.http.clients.async_flask import AsyncFlaskHttpClient from tests.http.clients.flask import FlaskHttpClient if http_client_class is FlaskHttpClient: pytest.skip( reason="FlaskHttpClient doesn't support multipart subscriptions" ) if http_client_class is AsyncFlaskHttpClient: pytest.xfail( reason="AsyncFlaskHttpClient doesn't support multipart subscriptions" ) with contextlib.suppress(ImportError): from tests.http.clients.chalice import ChaliceHttpClient if http_client_class is ChaliceHttpClient: pytest.skip( reason="ChaliceHttpClient doesn't support multipart subscriptions" ) return http_client_class(schema=schema) @pytest.mark.parametrize("method", ["get", "post"]) @pytest.mark.parametrize( "accept_header", [ pytest.param( "multipart/mixed;boundary=graphql;subscriptionSpec=1.0,application/json", id="with-boundary", ), pytest.param( 'multipart/mixed;subscriptionSpec="1.0",application/json', id="no-boundary-with-quotes", ), ], ) async def test_multipart_subscription( http_client: HttpClient, method: Literal["get", "post"], accept_header: str ): response = await http_client.query( method=method, query='subscription { echo(message: "Hello world", delay: 0.2) }', headers={ "accept": accept_header, "content-type": "application/json", }, ) data = [d async for d in response.streaming_json()] assert data == [ { "payload": { "data": {"echo": "Hello world"}, "extensions": {"example": "example"}, } } ] assert response.status_code == 200 async def test_multipart_subscription_use_the_views_decode_json_method( http_client: HttpClient, mocker ): spy = mocker.spy(BaseView, "decode_json") response = await http_client.query( query='subscription { echo(message: "Hello world", delay: 0.2) }', headers={ "accept": "multipart/mixed;boundary=graphql;subscriptionSpec=1.0,application/json", "content-type": "application/json", }, ) data = [d async for d in response.streaming_json()] assert data == [ { "payload": { "data": {"echo": "Hello world"}, "extensions": {"example": "example"}, } } ] assert response.status_code == 200 assert spy.call_count == 1 async def test_returns_error_when_trying_to_use_batching_with_multipart_subscriptions( http_client_class: type[HttpClient], ): http_client = http_client_class( schema=strawberry.Schema( query=Query, mutation=Mutation, subscription=Subscription, extensions=[MyExtension], config=StrawberryConfig(batching_config={"max_operations": 10}), ) ) response = await http_client.post( url="/graphql", json=[ {"query": 'subscription { echo(message: "Hello world", delay: 0.2) }'}, {"query": 'subscription { echo(message: "Hello world", delay: 0.2) }'}, {"query": 'subscription { echo(message: "Hello world", delay: 0.2) }'}, ], headers={ "content-type": "application/json", "accept": "multipart/mixed;boundary=graphql;subscriptionSpec=1.0,application/json", }, ) assert response.status_code == 400 assert "Batching is not supported for multipart subscriptions" in response.text strawberry-graphql-0.287.0/tests/http/incremental/test_stream.py000066400000000000000000000037311511033167500250640ustar00rootroot00000000000000import contextlib import pytest from inline_snapshot import snapshot from tests.conftest import skip_if_gql_32 from tests.http.clients.base import HttpClient pytestmark = skip_if_gql_32("GraphQL 3.3.0 is required for incremental execution") async def test_basic_stream(http_client: HttpClient): response = await http_client.query( method="get", query=""" query Stream { streamableField @stream } """, ) async with contextlib.aclosing(response.streaming_json()) as stream: initial = await stream.__anext__() assert initial == snapshot( { "data": {"streamableField": []}, "hasNext": True, "pending": [{"id": "0", "path": ["streamableField"]}], "extensions": None, } ) first = await stream.__anext__() assert first == snapshot( { "hasNext": True, "extensions": None, "incremental": [ { "items": ["Hello 0"], "id": "0", "path": ["streamableField"], "label": None, } ], } ) second = await stream.__anext__() assert second == snapshot( { "hasNext": True, "extensions": None, "incremental": [ { "items": ["Hello 1"], "id": "0", "path": ["streamableField"], "label": None, } ], } ) third = await stream.__anext__() assert third == snapshot( {"hasNext": False, "extensions": None, "completed": [{"id": "0"}]} ) with pytest.raises(StopAsyncIteration): await stream.__anext__() strawberry-graphql-0.287.0/tests/http/test_async_base_view.py000066400000000000000000000056201511033167500244300ustar00rootroot00000000000000import asyncio from asyncio import sleep from collections import Counter from collections.abc import AsyncGenerator from random import random from typing import Any, cast import pytest from strawberry.http.async_base_view import AsyncBaseHTTPView @pytest.mark.parametrize( "expected", [ pytest.param(["last"], id="single_item"), pytest.param(["1st", "last"], id="two_items"), pytest.param(["1st", "2nd", "last"], id="three_items"), ], ) async def test_stream_with_heartbeat_should_yield_items_correctly( expected: list[str], ) -> None: """ Verifies _stream_with_heartbeat reliably delivers all items in correct order. Tests three critical stream properties: 1. Completeness: All source items appear in output (especially the last item) 2. Uniqueness: Each expected item appears exactly once 3. Order: Original sequence of items is preserved Uses multiple test cases via parametrization and runs 100 concurrent streams with randomized delays to stress-test the implementation. This specifically targets race conditions between the drain task and queue consumer that could cause missing items, duplicates, or reordering. """ assert len(set(expected)) == len(expected), "Test requires unique elements" class MockAsyncBaseHTTPView: def encode_multipart_data(self, *_: Any, **__: Any) -> str: return "" view = MockAsyncBaseHTTPView() async def stream() -> AsyncGenerator[str, None]: for elem in expected: yield elem async def collect() -> list[str]: result = [] async for item in AsyncBaseHTTPView._stream_with_heartbeat( cast("AsyncBaseHTTPView", view), stream, "" )(): result.append(item) # Random sleep to promote race conditions between concurrent tasks await sleep(random() / 1000) # noqa: S311 return result for actual in await asyncio.gather(*(collect() for _ in range(100))): # Validation 1: Item completeness count = Counter(actual) if missing_items := set(expected) - set(count): assert not missing_items, f"Missing expected items: {list(missing_items)}" # Validation 2: No duplicates for item in expected: item_count = count[item] assert item_count == 1, ( f"Expected item '{item}' appears {item_count} times (should appear exactly once)" ) # Validation 3: Preserved ordering item_indices = {item: actual.index(item) for item in expected} for i in range(len(expected) - 1): curr, next_item = expected[i], expected[i + 1] assert item_indices[curr] < item_indices[next_item], ( f"Order incorrect: '{curr}' (at index {item_indices[curr]}) " f"should appear before '{next_item}' (at index {item_indices[next_item]})" ) strawberry-graphql-0.287.0/tests/http/test_graphql_ide.py000066400000000000000000000056371511033167500235560ustar00rootroot00000000000000from typing import Literal import pytest from tests.views.schema import schema from .clients.base import HttpClient @pytest.mark.parametrize("header_value", ["text/html", "*/*"]) @pytest.mark.parametrize("graphql_ide", ["graphiql", "apollo-sandbox", "pathfinder"]) async def test_renders_graphql_ide( header_value: str, http_client_class: type[HttpClient], graphql_ide: Literal["graphiql", "apollo-sandbox", "pathfinder"], ): http_client = http_client_class(schema, graphql_ide=graphql_ide) response = await http_client.get("/graphql", headers={"Accept": header_value}) content_type = response.headers.get( "content-type", response.headers.get("Content-Type", "") ) assert response.status_code == 200 assert "text/html" in content_type assert "Strawberry" in response.text if graphql_ide == "apollo-sandbox": assert "embeddable-sandbox.cdn.apollographql" in response.text if graphql_ide == "pathfinder": assert "@pathfinder-ide/react" in response.text if graphql_ide == "graphiql": assert "unpkg.com/graphiql" in response.text @pytest.mark.parametrize("header_value", ["text/html", "*/*"]) async def test_renders_graphql_ide_deprecated( header_value: str, http_client_class: type[HttpClient] ): with pytest.deprecated_call( match=r"The `graphiql` argument is deprecated in favor of `graphql_ide`" ): http_client = http_client_class(schema, graphiql=True) response = await http_client.get("/graphql", headers={"Accept": header_value}) content_type = response.headers.get( "content-type", response.headers.get("Content-Type", "") ) assert response.status_code == 200 assert "text/html" in content_type assert "<title>Strawberry GraphiQL" in response.text assert "https://unpkg.com/graphiql" in response.text async def test_does_not_render_graphiql_if_wrong_accept( http_client_class: type[HttpClient], ): http_client = http_client_class(schema) response = await http_client.get("/graphql", headers={"Accept": "text/xml"}) # THIS might need to be changed to 404 assert response.status_code == 400 @pytest.mark.parametrize("graphql_ide", [False, None]) async def test_renders_graphiql_disabled( http_client_class: type[HttpClient], graphql_ide: bool | None, ): http_client = http_client_class(schema, graphql_ide=graphql_ide) response = await http_client.get("/graphql", headers={"Accept": "text/html"}) assert response.status_code == 404 async def test_renders_graphiql_disabled_deprecated( http_client_class: type[HttpClient], ): with pytest.deprecated_call( match=r"The `graphiql` argument is deprecated in favor of `graphql_ide`" ): http_client = http_client_class(schema, graphiql=False) response = await http_client.get("/graphql", headers={"Accept": "text/html"}) assert response.status_code == 404 strawberry-graphql-0.287.0/tests/http/test_graphql_over_http_spec.py000066400000000000000000000523361511033167500260370ustar00rootroot00000000000000""" This file essentially mirrors the GraphQL over HTTP audits: https://github.com/graphql/graphql-http/blob/main/src/audits/server.ts """ import pytest try: from tests.http.clients.chalice import ChaliceHttpClient except ImportError: ChaliceHttpClient = type(None) try: from tests.http.clients.django import DjangoHttpClient except ImportError: DjangoHttpClient = type(None) try: from tests.http.clients.sanic import SanicHttpClient except ImportError: SanicHttpClient = type(None) @pytest.mark.xfail( reason="Our integrations currently only return application/json", raises=AssertionError, ) async def test_22eb(http_client): """ SHOULD accept application/graphql-response+json and match the content-type """ response = await http_client.query( method="post", headers={ "Content-Type": "application/json", "Accept": "application/graphql-response+json", }, query="{ __typename }", ) assert response.status_code == 200 assert "application/graphql-response+json" in response.headers["content-type"] async def test_4655(http_client): """ MUST accept application/json and match the content-type """ response = await http_client.query( method="post", headers={ "Content-Type": "application/json", "Accept": "application/json", }, query="{ __typename }", ) assert response.status_code == 200 assert "application/json" in response.headers["content-type"] async def test_47de(http_client): """ SHOULD accept */* and use application/json for the content-type """ response = await http_client.query( method="post", headers={ "Content-Type": "application/json", "Accept": "*/*", }, query="{ __typename }", ) assert response.status_code == 200 assert "application/json" in response.headers["content-type"] async def test_80d8(http_client): """ SHOULD assume application/json content-type when accept is missing """ response = await http_client.query( method="post", headers={"Content-Type": "application/json"}, query="{ __typename }", ) assert response.status_code == 200 assert "application/json" in response.headers["content-type"] async def test_82a3(http_client): """ MUST use utf-8 encoding when responding """ response = await http_client.query( method="post", headers={"Content-Type": "application/json"}, query="{ __typename }", ) assert response.status_code == 200 assert isinstance(response.data, bytes) try: response.data.decode(encoding="utf-8", errors="strict") except UnicodeDecodeError: pytest.fail("Response body is not UTF-8 encoded") async def test_bf61(http_client): """ MUST accept utf-8 encoded request """ response = await http_client.query( method="post", headers={"Content-Type": "application/json; charset=utf-8"}, query='{ __type(name: "Run🏃Swim🏊") { name } }', ) assert response.status_code == 200 async def test_78d5(http_client): """ MUST assume utf-8 in request if encoding is unspecified """ response = await http_client.query( method="post", headers={"Content-Type": "application/json"}, query="{ __typename }", ) assert response.status_code == 200 async def test_2c94(http_client): """ MUST accept POST requests """ response = await http_client.query( method="post", headers={"Content-Type": "application/json"}, query="{ __typename }", ) assert response.status_code == 200 async def test_5a70(http_client): """ MAY accept application/x-www-form-urlencoded formatted GET requests """ response = await http_client.query(method="get", query="{ __typename }") assert response.status_code == 200 async def test_9c48(http_client): """ MAY NOT allow executing mutations on GET requests """ response = await http_client.query( method="get", headers={"Accept": "application/graphql-response+json"}, query="mutation { __typename }", ) assert 400 <= response.status_code <= 499 @pytest.mark.xfail( reason="OPTIONAL - currently supported by Channels, Chalice, Django, and Sanic", raises=AssertionError, ) async def test_9abe(http_client): """ MAY respond with 4xx status code if content-type is not supplied on POST requests """ response = await http_client.post( url="/graphql", headers={}, json={"query": "{ __typename }"}, ) assert 400 <= response.status_code <= 499 async def test_03d4(http_client): """ MUST accept application/json POST requests """ response = await http_client.query( method="post", headers={"Content-Type": "application/json"}, query="{ __typename }", ) assert response.status_code == 200 async def test_a5bf(http_client): """ MAY use 400 status code when request body is missing on POST """ response = await http_client.post( url="/graphql", headers={"Content-Type": "application/json"}, ) assert response.status_code == 400 async def test_423l(http_client): """ MAY use 400 status code on missing {query} parameter """ response = await http_client.post( url="/graphql", headers={ "Content-Type": "application/json", "Accept": "application/graphql-response+json", }, json={"notquery": "{ __typename }"}, ) assert response.status_code == 400 @pytest.mark.parametrize( "invalid", [{"obj": "ect"}, 0, False, ["array"]], ids=["LKJ0", "LKJ1", "LKJ2", "LKJ3"], ) async def test_lkj_(http_client, invalid): """ MAY use 400 status code on invalid {query} parameter """ response = await http_client.post( url="/graphql", headers={"Content-Type": "application/json"}, json={"query": invalid}, ) assert response.status_code == 400 async def test_34a2(http_client): """ SHOULD allow string {query} parameter when accepting application/graphql-response+json """ response = await http_client.query( method="post", headers={ "Content-Type": "application/json", "Accept": "application/graphql-response+json", }, query="{ __typename }", ) assert response.status_code == 200 async def test_13ee(http_client): """ MUST allow string {query} parameter when accepting application/json """ response = await http_client.query( method="post", headers={ "Content-Type": "application/json", "Accept": "application/json", }, query="{ __typename }", ) assert response.status_code == 200 assert isinstance(response.json, dict) assert "errors" not in response.json @pytest.mark.parametrize( "invalid", [{"obj": "ect"}, 0, False, ["array"]], ids=["6C00", "6C01", "6C02", "6C03"], ) async def test_6c0_(http_client, invalid): """ MAY use 400 status code on invalid {operationName} parameter """ response = await http_client.post( url="/graphql", headers={"Content-Type": "application/json"}, json={ "operationName": invalid, "query": "{ __typename }", }, ) assert response.status_code == 400 async def test_8161(http_client): """ SHOULD allow string {operationName} parameter when accepting application/graphql-response+json """ response = await http_client.post( url="/graphql", headers={ "Content-Type": "application/json", "Accept": "application/graphql-response+json", }, json={ "operationName": "Query", "query": "query Query { __typename }", }, ) assert response.status_code == 200 async def test_b8b3(http_client): """ MUST allow string {operationName} parameter when accepting application/json """ response = await http_client.post( url="/graphql", headers={ "Content-Type": "application/json", "Accept": "application/json", }, json={ "operationName": "Query", "query": "query Query { __typename }", }, ) assert response.status_code == 200 assert isinstance(response.json, dict) assert "errors" not in response.json @pytest.mark.parametrize( "parameter", ["variables", "operationName", "extensions"], ids=["94B0", "94B1", "94B2"], ) async def test_94b_(http_client, parameter): """ SHOULD allow null variables/operationName/extensions parameter when accepting application/graphql-response+json """ response = await http_client.post( url="/graphql", headers={ "Content-Type": "application/json", "Accept": "application/graphql-response+json", }, json={ "query": "{ __typename }", parameter: None, }, ) assert response.status_code == 200 assert "errors" not in response.json @pytest.mark.parametrize( "parameter", ["variables", "operationName", "extensions"], ids=["0220", "0221", "0222"], ) async def test_022_(http_client, parameter): """ MUST allow null variables/operationName/extensions parameter when accepting application/json """ response = await http_client.post( url="/graphql", headers={ "Content-Type": "application/json", "Accept": "application/json", }, json={ "query": "{ __typename }", parameter: None, }, ) assert response.status_code == 200 assert "errors" not in response.json @pytest.mark.parametrize( "invalid", ["string", 0, False, ["array"]], ids=["4760", "4761", "4762", "4763"], ) async def test_476_(http_client, invalid): """ MAY use 400 status code on invalid {variables} parameter """ response = await http_client.post( url="/graphql", headers={"Content-Type": "application/json"}, json={ "query": "{ __typename }", "variables": invalid, }, ) assert response.status_code == 400 async def test_2ea1(http_client): """ SHOULD allow map {variables} parameter when accepting application/graphql-response+json """ response = await http_client.post( url="/graphql", headers={ "Content-Type": "application/json", "Accept": "application/graphql-response+json", }, json={ "query": "query Type($name: String!) { __type(name: $name) { name } }", "variables": {"name": "sometype"}, }, ) assert response.status_code == 200 async def test_28b9(http_client): """ MUST allow map {variables} parameter when accepting application/json """ response = await http_client.post( url="/graphql", headers={ "Content-Type": "application/json", "Accept": "application/json", }, json={ "query": "query Type($name: String!) { __type(name: $name) { name } }", "variables": {"name": "sometype"}, }, ) assert response.status_code == 200 assert isinstance(response.json, dict) assert "errors" not in response.json async def test_d6d5(http_client): """ MAY allow URL-encoded JSON string {variables} parameter in GETs when accepting application/graphql-response+json """ response = await http_client.query( query="query Type($name: String!) { __type(name: $name) { name } }", variables={"name": "sometype"}, method="get", headers={"Accept": "application/graphql-response+json"}, ) assert response.status_code == 200 async def test_6a70(http_client): """ MAY allow URL-encoded JSON string {variables} parameter in GETs when accepting application/json """ response = await http_client.query( query="query Type($name: String!) { __type(name: $name) { name } }", variables={"name": "sometype"}, method="get", headers={"Accept": "application/json"}, ) assert response.status_code == 200 assert isinstance(response.json, dict) assert "errors" not in response.json @pytest.mark.parametrize( "invalid", ["string", 0, False, ["array"]], ids=["58B0", "58B1", "58B2", "58B3"], ) async def test_58b_(http_client, invalid): """ MAY use 400 status code on invalid {extensions} parameter """ response = await http_client.post( url="/graphql", headers={"Content-Type": "application/json"}, json={ "query": "{ __typename }", "extensions": invalid, }, ) assert response.status_code == 400 async def test_428f(http_client): """ SHOULD allow map {extensions} parameter when accepting application/graphql-response+json """ response = await http_client.post( url="/graphql", headers={ "Content-Type": "application/json", "Accept": "application/graphql-response+json", }, json={ "query": "{ __typename }", "extensions": {"some": "value"}, }, ) assert response.status_code == 200 async def test_1b7a(http_client): """ MUST allow map {extensions} parameter when accepting application/json """ response = await http_client.post( url="/graphql", headers={ "Content-Type": "application/json", "Accept": "application/json", }, json={ "query": "{ __typename }", "extensions": {"some": "value"}, }, ) assert response.status_code == 200 assert isinstance(response.json, dict) assert "errors" not in response.json async def test_b6dc(http_client): """ MAY use 4xx or 5xx status codes on JSON parsing failure """ response = await http_client.post( url="/graphql", headers={"Content-Type": "application/json"}, data=b'{ "not a JSON', ) assert 400 <= response.status_code <= 599 async def test_bcf8(http_client): """ MAY use 400 status code on JSON parsing failure """ response = await http_client.post( url="/graphql", headers={"Content-Type": "application/json"}, data=b'{ "not a JSON', ) assert response.status_code == 400 async def test_8764(http_client): """ MAY use 4xx or 5xx status codes if parameters are invalid """ response = await http_client.post( url="/graphql", headers={"Content-Type": "application/json"}, json={"qeury": "{ __typename }"}, # typo in 'query' ) assert 400 <= response.status_code <= 599 async def test_3e3a(http_client): """ MAY use 400 status code if parameters are invalid """ response = await http_client.post( url="/graphql", headers={"Content-Type": "application/json"}, json={"qeury": "{ __typename }"}, # typo in 'query' ) assert response.status_code == 400 async def test_39aa(http_client): """ MUST accept a map for the {extensions} parameter """ response = await http_client.post( url="/graphql", headers={ "Content-Type": "application/json", "Accept": "application/json", }, json={ "query": "{ __typename }", "extensions": {"some": "value"}, }, ) assert response.status_code == 200 assert isinstance(response.json, dict) assert "errors" not in response.json async def test_572b(http_client): """ SHOULD use 200 status code on document parsing failure when accepting application/json """ response = await http_client.post( url="/graphql", headers={ "Content-Type": "application/json", "Accept": "application/json", }, json={"query": "{"}, ) assert response.status_code == 200 async def test_dfe2(http_client): """ SHOULD use 200 status code on document validation failure when accepting application/json """ response = await http_client.post( url="/graphql", headers={ "Content-Type": "application/json", "Accept": "application/json", }, json={ "query": "{ 8f31403dfe404bccbb0e835f2629c6a7 }" }, # making sure the field doesn't exist ) assert response.status_code == 200 async def test_7b9b(http_client): """ SHOULD use a status code of 200 on variable coercion failure when accepting application/json """ response = await http_client.post( url="/graphql", headers={ "Content-Type": "application/json", "Accept": "application/json", }, json={ "query": "query CoerceFailure($id: ID!){ __typename }", "variables": {"id": None}, }, ) assert response.status_code == 200 @pytest.mark.xfail( reason="Currently results in status 200 with GraphQL errors", raises=AssertionError ) async def test_865d(http_client): """ SHOULD use 4xx or 5xx status codes on document parsing failure when accepting application/graphql-response+json """ response = await http_client.post( url="/graphql", headers={ "Content-Type": "application/json", "Accept": "application/graphql-response+json", }, json={"query": "{"}, ) assert 400 <= response.status_code <= 599 @pytest.mark.xfail( reason="Currently results in status 200 with GraphQL errors", raises=AssertionError ) async def test_556a(http_client): """ SHOULD use 400 status code on document parsing failure when accepting application/graphql-response+json """ response = await http_client.post( url="/graphql", headers={ "Content-Type": "application/json", "Accept": "application/graphql-response+json", }, json={"query": "{"}, ) assert response.status_code == 400 @pytest.mark.xfail( reason="Currently results in status 200 with GraphQL errors", raises=AssertionError ) async def test_d586(http_client): """ SHOULD NOT contain the data entry on document parsing failure when accepting application/graphql-response+json """ response = await http_client.post( url="/graphql", headers={ "Content-Type": "application/json", "Accept": "application/graphql-response+json", }, json={"query": "{"}, ) assert response.status_code == 400 assert "data" not in response.json @pytest.mark.xfail( reason="Currently results in status 200 with GraphQL errors", raises=AssertionError ) async def test_51fe(http_client): """ SHOULD use 4xx or 5xx status codes on document validation failure when accepting application/graphql-response+json """ response = await http_client.post( url="/graphql", headers={ "Content-Type": "application/json", "Accept": "application/graphql-response+json", }, json={ "query": "{ 8f31403dfe404bccbb0e835f2629c6a7 }", # making sure the field doesn't exist }, ) assert 400 <= response.status_code <= 599 @pytest.mark.xfail( reason="Currently results in status 200 with GraphQL errors", raises=AssertionError ) async def test_74ff(http_client): """ SHOULD use 400 status code on document validation failure when accepting application/graphql-response+json """ response = await http_client.post( url="/graphql", headers={ "Content-Type": "application/json", "Accept": "application/graphql-response+json", }, json={ "query": "{ 8f31403dfe404bccbb0e835f2629c6a7 }", # making sure the field doesn't exist }, ) assert response.status_code == 400 @pytest.mark.xfail( reason="Currently results in status 200 with GraphQL errors", raises=AssertionError ) async def test_5e5b(http_client): """ SHOULD NOT contain the data entry on document validation failure when accepting application/graphql-response+json """ response = await http_client.post( url="/graphql", headers={ "Content-Type": "application/json", "Accept": "application/graphql-response+json", }, json={ "query": "{ 8f31403dfe404bccbb0e835f2629c6a7 }", # making sure the field doesn't exist }, ) assert response.status_code == 400 assert "data" not in response.json @pytest.mark.xfail( reason="Currently results in status 200 with GraphQL errors", raises=AssertionError ) async def test_86ee(http_client): """ SHOULD use a status code of 400 on variable coercion failure when accepting application/graphql-response+json """ response = await http_client.post( url="/graphql", headers={ "Content-Type": "application/json", "Accept": "application/graphql-response+json", }, json={ "query": "query CoerceFailure($id: ID!){ __typename }", "variables": {"id": None}, }, ) assert response.status_code == 400 strawberry-graphql-0.287.0/tests/http/test_http.py000066400000000000000000000035671511033167500222560ustar00rootroot00000000000000import json from typing import Literal import pytest from strawberry.http.base import BaseView from .clients.base import HttpClient @pytest.mark.parametrize("method", ["delete", "head", "put", "patch"]) async def test_does_only_allow_get_and_post( method: Literal["delete", "head", "put", "patch"], http_client: HttpClient, ): response = await http_client.request(url="/graphql", method=method) assert response.status_code == 405 async def test_the_http_handler_uses_the_views_decode_json_method( http_client: HttpClient, mocker ): spy = mocker.spy(BaseView, "decode_json") response = await http_client.query(query="{ hello }") assert response.status_code == 200 assert response.headers["content-type"].split(";")[0] == "application/json" data = response.json["data"] assert isinstance(data, dict) assert data["hello"] == "Hello world" assert spy.call_count == 1 async def test_the_http_handler_supports_bytes_encoded_json( http_client: HttpClient, mocker ): """Check that http handlers correctly deal with byte return type from `encode_json`""" def patched_encode_json(self, data: object) -> bytes: return json.dumps(data).encode() mocker.patch("strawberry.http.base.BaseView.encode_json", patched_encode_json) response = await http_client.query(query="{ hello }") assert response.status_code == 200 assert response.headers["content-type"].split(";")[0] == "application/json" data = response.json["data"] assert isinstance(data, dict) assert data["hello"] == "Hello world" async def test_exception(http_client: HttpClient, mocker): response = await http_client.query(query="{ hello }", operation_name="wrong") assert response.status_code == 400 assert response.headers["content-type"].split(";")[0] == "text/plain" assert response.data == b'Unknown operation named "wrong".' strawberry-graphql-0.287.0/tests/http/test_mutation.py000066400000000000000000000006141511033167500231250ustar00rootroot00000000000000from .clients.base import HttpClient async def test_mutation(http_client: HttpClient): response = await http_client.query( query="mutation { hello }", headers={ "Content-Type": "application/json", }, ) data = response.json["data"] assert response.status_code == 200 assert isinstance(data, dict) assert data["hello"] == "strawberry" strawberry-graphql-0.287.0/tests/http/test_parse_content_type.py000066400000000000000000000033341511033167500251740ustar00rootroot00000000000000import pytest from strawberry.http.parse_content_type import parse_content_type @pytest.mark.parametrize( ("content_type", "expected"), [ ("application/json", ("application/json", {})), ("", ("", {})), ("application/json; charset=utf-8", ("application/json", {"charset": "utf-8"})), ( "application/json; charset=utf-8; boundary=foobar", ("application/json", {"charset": "utf-8", "boundary": "foobar"}), ), ( "application/json; boundary=foobar; charset=utf-8", ("application/json", {"boundary": "foobar", "charset": "utf-8"}), ), ( "application/json; boundary=foobar", ("application/json", {"boundary": "foobar"}), ), ( "application/json; boundary=foobar; charset=utf-8; foo=bar", ( "application/json", {"boundary": "foobar", "charset": "utf-8", "foo": "bar"}, ), ), ( 'multipart/mixed; boundary="graphql"; subscriptionSpec=1.0, application/json', ( "multipart/mixed", { "boundary": "graphql", "subscriptionspec": "1.0, application/json", }, ), ), ( 'multipart/mixed; subscriptionSpec="1.0", application/json', ( "multipart/mixed", { "subscriptionspec": '"1.0", application/json', }, ), ), ], ) async def test_parse_content_type( content_type: str, expected: tuple[str, dict[str, str]], ): assert parse_content_type(content_type) == expected strawberry-graphql-0.287.0/tests/http/test_process_result.py000066400000000000000000000016171511033167500243450ustar00rootroot00000000000000from __future__ import annotations from typing import Literal import pytest from strawberry.http import GraphQLHTTPResponse from strawberry.types import ExecutionResult from tests.views.schema import schema from .clients.base import HttpClient def process_result(result: ExecutionResult) -> GraphQLHTTPResponse: if result.data: return { "data": {key.upper(): result for key, result in result.data.items()}, } return {} @pytest.fixture def http_client(http_client_class) -> HttpClient: return http_client_class(schema, result_override=process_result) @pytest.mark.parametrize("method", ["get", "post"]) async def test_custom_process_result( method: Literal["get", "post"], http_client: HttpClient ): response = await http_client.query( method=method, query="{ hello }", ) assert response.json["data"] == {"HELLO": "Hello world"} strawberry-graphql-0.287.0/tests/http/test_query.py000066400000000000000000000247631511033167500224450ustar00rootroot00000000000000from typing import Literal import pytest from graphql import GraphQLError from pytest_mock import MockFixture from tests.conftest import skip_if_gql_32 from .clients.base import HttpClient @pytest.mark.parametrize("method", ["get", "post"]) async def test_graphql_query(method: Literal["get", "post"], http_client: HttpClient): response = await http_client.query( method=method, query="{ hello }", ) assert response.status_code == 200 data = response.json["data"] assert isinstance(data, dict) assert data["hello"] == "Hello world" @pytest.mark.parametrize("method", ["get", "post"]) async def test_calls_handle_errors( method: Literal["get", "post"], http_client: HttpClient, mocker: MockFixture ): sync_mock = mocker.patch( "strawberry.http.sync_base_view.SyncBaseHTTPView._handle_errors" ) async_mock = mocker.patch( "strawberry.http.async_base_view.AsyncBaseHTTPView._handle_errors" ) response = await http_client.query( method=method, query="{ hey }", ) data = response.json["data"] assert response.status_code == 200 assert data is None assert response.json["errors"] == [ { "message": "Cannot query field 'hey' on type 'Query'.", "locations": [{"line": 1, "column": 3}], } ] error = GraphQLError("Cannot query field 'hey' on type 'Query'.") response_data = { "data": None, "errors": [ { "message": "Cannot query field 'hey' on type 'Query'.", "locations": [{"line": 1, "column": 3}], }, ], "extensions": {"example": "example"}, } call_args = async_mock.call_args[0] if async_mock.called else sync_mock.call_args[0] assert call_args[0][0].message == error.message assert call_args[1] == response_data @pytest.mark.parametrize("method", ["get", "post"]) async def test_graphql_can_pass_variables( method: Literal["get", "post"], http_client: HttpClient ): response = await http_client.query( method=method, query="query hello($name: String!) { hello(name: $name) }", variables={"name": "Jake"}, ) assert response.status_code == 200 data = response.json["data"] assert isinstance(data, dict) assert data["hello"] == "Hello Jake" @pytest.mark.parametrize("extra_kwargs", [{"variables": None}, {}]) async def test_operation_variables_may_be_null_or_omitted( http_client: HttpClient, extra_kwargs ): response = await http_client.query( query="{ __typename }", **extra_kwargs, ) data = response.json["data"] assert response.status_code == 200 assert isinstance(data, dict) assert data["__typename"] == "Query" @pytest.mark.parametrize( "not_an_object_or_null", ["string", 0, False, ["array"]], ) async def test_requests_with_invalid_variables_parameter_are_rejected( http_client: HttpClient, not_an_object_or_null ): response = await http_client.query( query="{ __typename }", variables=not_an_object_or_null, ) assert response.status_code == 400 assert ( response.data == b"The GraphQL operation's `variables` must be an object or null, if provided." ) @pytest.mark.parametrize("method", ["get", "post"]) async def test_root_value(method: Literal["get", "post"], http_client: HttpClient): response = await http_client.query( method=method, query="{ rootName }", ) assert response.status_code == 200 data = response.json["data"] assert isinstance(data, dict) assert data["rootName"] == "Query" @pytest.mark.parametrize("method", ["get", "post"]) async def test_passing_invalid_query( method: Literal["get", "post"], http_client: HttpClient ): response = await http_client.query( method=method, query="{ h", ) assert response.status_code == 200 assert response.json["errors"] == [ { "message": "Syntax Error: Expected Name, found .", "locations": [{"line": 1, "column": 4}], } ] @pytest.mark.parametrize("method", ["get", "post"]) async def test_returns_errors(method: Literal["get", "post"], http_client: HttpClient): response = await http_client.query( method=method, query="{ maya }", ) assert response.status_code == 200 assert response.json["errors"] == [ { "message": "Cannot query field 'maya' on type 'Query'.", "locations": [{"line": 1, "column": 3}], } ] @pytest.mark.parametrize("method", ["get", "post"]) async def test_returns_errors_and_data( method: Literal["get", "post"], http_client: HttpClient ): response = await http_client.query( method=method, query="{ hello, alwaysFail }", ) assert response.status_code == 200 data = response.json["data"] errors = response.json["errors"] assert errors == [ { "locations": [{"column": 10, "line": 1}], "message": "You are not authorized", "path": ["alwaysFail"], } ] assert data == {"hello": "Hello world", "alwaysFail": None} async def test_passing_invalid_json_post(http_client: HttpClient): response = await http_client.post( url="/graphql", data=b"{ h", headers={"Content-Type": "application/json"}, ) assert response.status_code == 400 assert "Unable to parse request body as JSON" in response.text async def test_passing_invalid_json_get(http_client: HttpClient): response = await http_client.get( url="/graphql?query={ hello }&variables='{'", ) assert response.status_code == 400 assert "Unable to parse request body as JSON" in response.text async def test_query_parameters_are_never_interpreted_as_list(http_client: HttpClient): response = await http_client.get( url='/graphql?query=query($name: String!) { hello(name: $name) }&variables={"name": "Jake"}&variables={"name": "Jake"}', ) assert response.status_code == 200 assert response.json["data"] == {"hello": "Hello Jake"} async def test_missing_query(http_client: HttpClient): response = await http_client.post( url="/graphql", json={}, headers={"Content-Type": "application/json"}, ) assert response.status_code == 400 assert "No GraphQL query found in the request" in response.text @pytest.mark.parametrize( "not_stringified_json", [{"obj": "ect"}, 0, False, ["array"]], ) async def test_requests_with_invalid_query_parameter_are_rejected( http_client: HttpClient, not_stringified_json ): response = await http_client.query( query=not_stringified_json, ) assert response.status_code == 400 assert ( response.data == b"The GraphQL operation's `query` must be a string or null, if provided." ) @pytest.mark.parametrize("method", ["get", "post"]) async def test_query_context(method: Literal["get", "post"], http_client: HttpClient): response = await http_client.query( method=method, query="{ valueFromContext }", ) assert response.status_code == 200 data = response.json["data"] assert isinstance(data, dict) assert data["valueFromContext"] == "a value from context" @skip_if_gql_32("formatting is different in gql 3.2") @pytest.mark.parametrize("method", ["get", "post"]) async def test_query_extensions( method: Literal["get", "post"], http_client: HttpClient ): response = await http_client.query( method=method, query='{ valueFromExtensions(key:"test") }', extensions={"test": "hello"}, ) assert response.status_code == 200 data = response.json["data"] assert isinstance(data, dict) assert data["valueFromExtensions"] == "hello" @pytest.mark.parametrize( "not_an_object_or_null", ["string", 0, False, ["array"]], ) async def test_requests_with_invalid_extension_parameter_are_rejected( http_client: HttpClient, not_an_object_or_null ): response = await http_client.query( query="{ __typename }", extensions=not_an_object_or_null, ) assert response.status_code == 400 assert ( response.data == b"The GraphQL operation's `extensions` must be an object or null, if provided." ) @pytest.mark.parametrize("method", ["get", "post"]) async def test_returning_status_code( method: Literal["get", "post"], http_client: HttpClient ): response = await http_client.query( method=method, query="{ returns401 }", ) assert response.status_code == 401 assert response.json["data"] == {"returns401": "hey"} @pytest.mark.parametrize("method", ["get", "post"]) async def test_updating_headers( method: Literal["get", "post"], http_client: HttpClient ): response = await http_client.query( method=method, variables={"name": "Jake"}, query="query ($name: String!) { setHeader(name: $name) }", ) assert response.status_code == 200 assert response.json["data"] == {"setHeader": "Jake"} assert response.headers["x-name"] == "Jake" @pytest.mark.parametrize( ("extra_kwargs", "expected_message"), [ ({}, "Hello Foo"), ({"operation_name": None}, "Hello Foo"), ({"operation_name": "Query1"}, "Hello Foo"), ({"operation_name": "Query2"}, "Hello Bar"), ], ) async def test_operation_selection( http_client: HttpClient, extra_kwargs, expected_message ): response = await http_client.query( query=""" query Query1 { hello(name: "Foo") } query Query2 { hello(name: "Bar") } """, **extra_kwargs, ) assert response.status_code == 200 assert response.json["data"] == {"hello": expected_message} @pytest.mark.parametrize( "operation_name", ["", "Query3"], ) async def test_invalid_operation_selection(http_client: HttpClient, operation_name): response = await http_client.query( query=""" query Query1 { hello(name: "Foo") } query Query2 { hello(name: "Bar") } """, operation_name=operation_name, ) assert response.status_code == 400 assert response.data == f'Unknown operation named "{operation_name}".'.encode() async def test_operation_selection_without_operations(http_client: HttpClient): response = await http_client.query( query=""" fragment Fragment1 on Query { __typename } """, ) assert response.status_code == 400 assert response.data == b"Can't get GraphQL operation type" strawberry-graphql-0.287.0/tests/http/test_query_batching.py000066400000000000000000000144361511033167500243000ustar00rootroot00000000000000import pytest import strawberry from strawberry.schema.config import StrawberryConfig from tests.conftest import skip_if_gql_32 from tests.http.clients.base import HttpClient from tests.views.schema import Mutation, MyExtension, Query, Subscription @pytest.fixture def batching_http_client(http_client_class: type[HttpClient]) -> HttpClient: return http_client_class( schema=strawberry.Schema( query=Query, mutation=Mutation, subscription=Subscription, extensions=[MyExtension], config=StrawberryConfig(batching_config={"max_operations": 10}), ) ) async def test_batching_works_with_multiple_queries(batching_http_client): response = await batching_http_client.post( url="/graphql", json=[ {"query": "{ hello }"}, {"query": "{ hello }"}, ], headers={"content-type": "application/json"}, ) assert response.status_code == 200 assert response.json == [ {"data": {"hello": "Hello world"}, "extensions": {"example": "example"}}, {"data": {"hello": "Hello world"}, "extensions": {"example": "example"}}, ] async def test_batching_works_with_single_query(batching_http_client): response = await batching_http_client.post( url="/graphql", json=[ {"query": "{ hello }"}, ], headers={"content-type": "application/json"}, ) assert response.status_code == 200 assert response.json == [ {"data": {"hello": "Hello world"}, "extensions": {"example": "example"}}, ] async def test_variables_can_be_supplied_per_query(batching_http_client): response = await batching_http_client.post( url="/graphql", json=[ { "query": "query InjectedVariables($name: String!) { hello(name: $name) }", "variables": {"name": "Alice"}, }, { "query": "query InjectedVariables($name: String!) { hello(name: $name) }", "variables": {"name": "Bob"}, }, {"query": "query NoVariables{ hello }"}, ], headers={"content-type": "application/json"}, ) assert response.status_code == 200 assert response.json == [ {"data": {"hello": "Hello Alice"}, "extensions": {"example": "example"}}, {"data": {"hello": "Hello Bob"}, "extensions": {"example": "example"}}, {"data": {"hello": "Hello world"}, "extensions": {"example": "example"}}, ] async def test_operations_can_be_selected_per_query(batching_http_client): response = await batching_http_client.post( url="/graphql", json=[ { "query": 'query Op1 { hello(name: "Op1") } query Op2 { hello(name: "Op2") }', "operationName": "Op1", }, { "query": 'query Op1 { hello(name: "Op1") } query Op2 { hello(name: "Op2") }', "operationName": "Op2", }, ], headers={"content-type": "application/json"}, ) assert response.status_code == 200 assert response.json == [ {"data": {"hello": "Hello Op1"}, "extensions": {"example": "example"}}, {"data": {"hello": "Hello Op2"}, "extensions": {"example": "example"}}, ] @skip_if_gql_32("formatting is different in gql 3.2") async def test_extensions_are_handled_per_query(batching_http_client): response = await batching_http_client.post( url="/graphql", json=[ { "query": 'query { valueFromExtensions(key: "test") }', "extensions": {"test": "op1-value"}, }, { "query": 'query { valueFromExtensions(key: "test") }', "extensions": {"test": "op2-value"}, }, ], headers={"content-type": "application/json"}, ) assert response.status_code == 200 assert response.json == [ { "data": {"valueFromExtensions": "op1-value"}, "extensions": {"example": "example"}, }, { "data": {"valueFromExtensions": "op2-value"}, "extensions": {"example": "example"}, }, ] async def test_context_is_shared_between_operations(batching_http_client): response = await batching_http_client.post( url="/graphql", json=[ {"query": 'mutation { updateContext(key: "test", value: "shared-value") }'}, {"query": 'query { valueFromContext(key: "test") }'}, ], headers={"content-type": "application/json"}, ) assert response.status_code == 200 assert response.json == [ { "data": {"updateContext": True}, "extensions": {"example": "example"}, }, { "data": {"valueFromContext": "shared-value"}, "extensions": {"example": "example"}, }, ] async def test_returns_error_when_batching_is_disabled( http_client_class: type[HttpClient], ): http_client = http_client_class( schema=strawberry.Schema( query=Query, mutation=Mutation, subscription=Subscription, extensions=[MyExtension], config=StrawberryConfig(batching_config=None), ) ) response = await http_client.post( url="/graphql", json=[ {"query": "{ hello }"}, {"query": "{ hello }"}, ], headers={"content-type": "application/json"}, ) assert response.status_code == 400 assert "Batching is not enabled" in response.text async def test_returns_error_when_trying_too_many_operations( http_client_class: type[HttpClient], ): http_client = http_client_class( schema=strawberry.Schema( query=Query, mutation=Mutation, subscription=Subscription, extensions=[MyExtension], config=StrawberryConfig(batching_config={"max_operations": 2}), ) ) response = await http_client.post( url="/graphql", json=[ {"query": "{ hello }"}, {"query": "{ hello }"}, {"query": "{ hello }"}, ], headers={"content-type": "application/json"}, ) assert response.status_code == 400 assert "Too many operations" in response.text strawberry-graphql-0.287.0/tests/http/test_query_via_get.py000066400000000000000000000025611511033167500241330ustar00rootroot00000000000000from tests.views.schema import schema from .clients.base import HttpClient async def test_sending_get_with_content_type_passes(http_client_class): http_client = http_client_class(schema) response = await http_client.query( method="get", query="query {hello}", headers={ "Content-Type": "application/json", }, ) data = response.json["data"] assert response.status_code == 200 assert data["hello"] == "Hello world" async def test_sending_empty_query(http_client_class): http_client = http_client_class(schema) response = await http_client.query( method="get", query="", variables={"fake": "variable"} ) assert response.status_code == 400 assert "No GraphQL query found in the request" in response.text async def test_does_not_allow_mutation(http_client: HttpClient): response = await http_client.query(method="get", query="mutation { hello }") assert response.status_code == 400 assert "mutations are not allowed when using GET" in response.text async def test_fails_if_allow_queries_via_get_false(http_client_class): http_client = http_client_class(schema, allow_queries_via_get=False) response = await http_client.query(method="get", query="{ hello }") assert response.status_code == 400 assert "queries are not allowed when using GET" in response.text strawberry-graphql-0.287.0/tests/http/test_upload.py000066400000000000000000000165071511033167500225610ustar00rootroot00000000000000import contextlib import json from io import BytesIO import pytest from urllib3 import encode_multipart_formdata from tests.views.schema import schema from .clients.base import HttpClient @pytest.fixture def http_client(http_client_class: type[HttpClient]) -> HttpClient: with contextlib.suppress(ImportError): from .clients.chalice import ChaliceHttpClient if http_client_class is ChaliceHttpClient: pytest.xfail(reason="Chalice does not support uploads") return http_client_class(schema) @pytest.fixture def enabled_http_client(http_client_class: type[HttpClient]) -> HttpClient: with contextlib.suppress(ImportError): from .clients.chalice import ChaliceHttpClient if http_client_class is ChaliceHttpClient: pytest.xfail(reason="Chalice does not support uploads") return http_client_class(schema, multipart_uploads_enabled=True) async def test_multipart_uploads_are_disabled_by_default(http_client: HttpClient): f = BytesIO(b"strawberry") query = """ mutation($textFile: Upload!) { readText(textFile: $textFile) } """ response = await http_client.query( query, variables={"textFile": None}, files={"textFile": f}, ) assert response.status_code == 400 assert response.data == b"Unsupported content type" async def test_upload(enabled_http_client: HttpClient): f = BytesIO(b"strawberry") query = """ mutation($textFile: Upload!) { readText(textFile: $textFile) } """ response = await enabled_http_client.query( query, variables={"textFile": None}, files={"textFile": f}, ) assert response.json.get("errors") is None assert response.json["data"] == {"readText": "strawberry"} async def test_file_list_upload(enabled_http_client: HttpClient): query = "mutation($files: [Upload!]!) { readFiles(files: $files) }" file1 = BytesIO(b"strawberry1") file2 = BytesIO(b"strawberry2") response = await enabled_http_client.query( query=query, variables={"files": [None, None]}, files={"file1": file1, "file2": file2}, ) data = response.json["data"] assert isinstance(data, dict) assert len(data["readFiles"]) == 2 assert data["readFiles"][0] == "strawberry1" assert data["readFiles"][1] == "strawberry2" async def test_nested_file_list(enabled_http_client: HttpClient): query = "mutation($folder: FolderInput!) { readFolder(folder: $folder) }" file1 = BytesIO(b"strawberry1") file2 = BytesIO(b"strawberry2") response = await enabled_http_client.query( query=query, variables={"folder": {"files": [None, None]}}, files={"file1": file1, "file2": file2}, ) data = response.json["data"] assert isinstance(data, dict) assert len(data["readFolder"]) == 2 assert data["readFolder"][0] == "strawberry1" assert data["readFolder"][1] == "strawberry2" async def test_upload_single_and_list_file_together(enabled_http_client: HttpClient): query = """ mutation($files: [Upload!]!, $textFile: Upload!) { readFiles(files: $files) readText(textFile: $textFile) } """ file1 = BytesIO(b"strawberry1") file2 = BytesIO(b"strawberry2") file3 = BytesIO(b"strawberry3") response = await enabled_http_client.query( query=query, variables={"files": [None, None], "textFile": None}, files={"file1": file1, "file2": file2, "textFile": file3}, ) data = response.json["data"] assert isinstance(data, dict) assert len(data["readFiles"]) == 2 assert data["readFiles"][0] == "strawberry1" assert data["readFiles"][1] == "strawberry2" assert data["readText"] == "strawberry3" async def test_upload_invalid_query(enabled_http_client: HttpClient): f = BytesIO(b"strawberry") query = """ mutation($textFile: Upload!) { readT """ response = await enabled_http_client.query( query, variables={"textFile": None}, files={"textFile": f}, ) assert response.status_code == 200 assert response.json["data"] is None assert response.json["errors"] == [ { "locations": [{"column": 5, "line": 4}], "message": "Syntax Error: Expected Name, found .", } ] async def test_upload_missing_file(enabled_http_client: HttpClient): f = BytesIO(b"strawberry") query = """ mutation($textFile: Upload!) { readText(textFile: $textFile) } """ response = await enabled_http_client.query( query, variables={"textFile": None}, # using the wrong name to simulate a missing file # this is to make it easier to run tests with our client files={"a": f}, ) assert response.status_code == 400 assert "File(s) missing in form data" in response.text class FakeWriter: def __init__(self): self.buffer = BytesIO() async def write(self, data: bytes): self.buffer.write(data) @property def value(self) -> bytes: return self.buffer.getvalue() async def test_extra_form_data_fields_are_ignored(enabled_http_client: HttpClient): query = """mutation($textFile: Upload!) { readText(textFile: $textFile) }""" f = BytesIO(b"strawberry") operations = json.dumps({"query": query, "variables": {"textFile": None}}) file_map = json.dumps({"textFile": ["variables.textFile"]}) extra_field_data = json.dumps({}) f = BytesIO(b"strawberry") fields = { "operations": operations, "map": file_map, "extra_field": extra_field_data, "textFile": ("textFile.txt", f.read(), "text/plain"), } data, header = encode_multipart_formdata(fields) response = await enabled_http_client.post( url="/graphql", data=data, headers={ "content-type": header, "content-length": f"{len(data)}", }, ) assert response.status_code == 200 assert response.json["data"] == {"readText": "strawberry"} async def test_sending_invalid_form_data(enabled_http_client: HttpClient): headers = {"content-type": "multipart/form-data; boundary=----fake"} response = await enabled_http_client.post("/graphql", headers=headers) assert response.status_code == 400 # TODO: consolidate this, it seems only AIOHTTP returns the second error # due to validating the boundary assert ( "No GraphQL query found in the request" in response.text or "Unable to parse the multipart body" in response.text ) @pytest.mark.aiohttp async def test_sending_invalid_json_body(enabled_http_client: HttpClient): f = BytesIO(b"strawberry") operations = "}" file_map = json.dumps({"textFile": ["variables.textFile"]}) fields = { "operations": operations, "map": file_map, "textFile": ("textFile.txt", f.read(), "text/plain"), } data, header = encode_multipart_formdata(fields) response = await enabled_http_client.post( "/graphql", data=data, headers={ "content-type": header, "content-length": f"{len(data)}", }, ) assert response.status_code == 400 assert ( "Unable to parse the multipart body" in response.text or "Unable to parse request body as JSON" in response.text ) strawberry-graphql-0.287.0/tests/litestar/000077500000000000000000000000001511033167500205235ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/litestar/__init__.py000066400000000000000000000000001511033167500226220ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/litestar/app.py000066400000000000000000000015701511033167500216600ustar00rootroot00000000000000from typing import Any from litestar import Litestar, Request from litestar.di import Provide from strawberry.litestar import make_graphql_controller from tests.views.schema import schema def custom_context_dependency() -> str: return "Hi!" async def get_root_value(request: Request = None): return request async def get_context(app_dependency: str, request: Request = None): return {"custom_value": app_dependency, "request": request} def create_app(schema=schema, **kwargs: Any): GraphQLController = make_graphql_controller( schema, path="/graphql", context_getter=get_context, root_value_getter=get_root_value, **kwargs, ) return Litestar( route_handlers=[GraphQLController], dependencies={ "app_dependency": Provide(custom_context_dependency, sync_to_thread=True) }, ) strawberry-graphql-0.287.0/tests/litestar/conftest.py000066400000000000000000000006471511033167500227310ustar00rootroot00000000000000import pytest @pytest.fixture def test_client(): from litestar.testing import TestClient from tests.litestar.app import create_app app = create_app() return TestClient(app) @pytest.fixture def test_client_keep_alive(): from litestar.testing import TestClient from tests.litestar.app import create_app app = create_app(keep_alive=True, keep_alive_interval=0.1) return TestClient(app) strawberry-graphql-0.287.0/tests/litestar/test_context.py000066400000000000000000000151251511033167500236240ustar00rootroot00000000000000import strawberry def test_base_context(): from strawberry.litestar import BaseContext base_context = BaseContext() assert base_context.request is None def test_with_class_context_getter(): from litestar import Litestar from litestar.di import Provide from litestar.testing import TestClient from strawberry.litestar import BaseContext, make_graphql_controller @strawberry.type class Query: @strawberry.field def abc(self, info: strawberry.Info) -> str: assert isinstance(info.context, CustomContext) assert info.context.request is not None assert info.context.strawberry == "rocks" return "abc" class CustomContext(BaseContext): strawberry: str def custom_context_dependency() -> CustomContext: return CustomContext(strawberry="rocks") async def get_context(custom_context_dependency: CustomContext): return custom_context_dependency schema = strawberry.Schema(query=Query) graphql_controller = make_graphql_controller( path="/graphql", schema=schema, context_getter=get_context ) app = Litestar( route_handlers=[graphql_controller], dependencies={ "custom_context_dependency": Provide( custom_context_dependency, sync_to_thread=True ) }, ) test_client = TestClient(app) response = test_client.post("/graphql", json={"query": "{ abc }"}) assert response.status_code == 200 assert response.json() == {"data": {"abc": "abc"}} def test_with_dict_context_getter(): from litestar import Litestar from litestar.di import Provide from litestar.testing import TestClient from strawberry.litestar import make_graphql_controller @strawberry.type class Query: @strawberry.field def abc(self, info: strawberry.Info) -> str: assert isinstance(info.context, dict) assert info.context.get("request") is not None assert info.context.get("strawberry") == "rocks" return "abc" def custom_context_dependency() -> str: return "rocks" async def get_context(custom_context_dependency: str) -> dict[str, str]: return {"strawberry": custom_context_dependency} schema = strawberry.Schema(query=Query) graphql_controller = make_graphql_controller( path="/graphql", schema=schema, context_getter=get_context ) app = Litestar( route_handlers=[graphql_controller], dependencies={ "custom_context_dependency": Provide( custom_context_dependency, sync_to_thread=True ) }, ) test_client = TestClient(app) response = test_client.post("/graphql", json={"query": "{ abc }"}) assert response.status_code == 200 assert response.json() == {"data": {"abc": "abc"}} def test_without_context_getter(): from litestar import Litestar from litestar.testing import TestClient from strawberry.litestar import make_graphql_controller @strawberry.type class Query: @strawberry.field def abc(self, info: strawberry.Info) -> str: assert isinstance(info.context, dict) assert info.context.get("request") is not None assert info.context.get("strawberry") is None return "abc" schema = strawberry.Schema(query=Query) graphql_controller = make_graphql_controller( path="/graphql", schema=schema, context_getter=None ) app = Litestar(route_handlers=[graphql_controller]) test_client = TestClient(app) response = test_client.post("/graphql", json={"query": "{ abc }"}) assert response.status_code == 200 assert response.json() == {"data": {"abc": "abc"}} def test_with_invalid_context_getter(): from litestar import Litestar from litestar.di import Provide from litestar.testing import TestClient from strawberry.litestar import make_graphql_controller @strawberry.type class Query: @strawberry.field def abc(self, info: strawberry.Info) -> str: assert info.context.get("request") is not None assert info.context.get("strawberry") is None return "abc" def custom_context_dependency() -> str: return "rocks" async def get_context(custom_context_dependency: str) -> str: return custom_context_dependency schema = strawberry.Schema(query=Query) graphql_controller = make_graphql_controller( path="/graphql", schema=schema, context_getter=get_context ) app = Litestar( route_handlers=[graphql_controller], dependencies={ "custom_context_dependency": Provide( custom_context_dependency, sync_to_thread=True ) }, ) test_client = TestClient(app, raise_server_exceptions=True) # TODO: test exception message # assert starlite.exceptions.http_exceptions.InternalServerException is raised # with pytest.raises( # InternalServerException, # r"A dependency failed validation for POST .*" # ), # ): response = test_client.post("/graphql", json={"query": "{ abc }"}) assert response.status_code == 500 assert response.json()["detail"] == "Internal Server Error" def test_custom_context(): from litestar.testing import TestClient from tests.litestar.app import create_app @strawberry.type class Query: @strawberry.field def custom_context_value(self, info: strawberry.Info) -> str: return info.context["custom_value"] schema = strawberry.Schema(query=Query) app = create_app(schema=schema) test_client = TestClient(app) response = test_client.post("/graphql", json={"query": "{ customContextValue }"}) assert response.status_code == 200 assert response.json() == {"data": {"customContextValue": "Hi!"}} def test_can_set_background_task(): from litestar.testing import TestClient from tests.litestar.app import create_app task_complete = False async def task(): nonlocal task_complete task_complete = True @strawberry.type class Query: @strawberry.field def something(self, info: strawberry.Info) -> str: response = info.context["response"] response.background.tasks.append(task) return "foo" schema = strawberry.Schema(query=Query) app = create_app(schema=schema) test_client = TestClient(app) response = test_client.post("/graphql", json={"query": "{ something }"}) assert response.json() == {"data": {"something": "foo"}} assert task_complete strawberry-graphql-0.287.0/tests/litestar/test_response_headers.py000066400000000000000000000040541511033167500254700ustar00rootroot00000000000000import strawberry # TODO: move this to common tests def test_set_response_headers(): from litestar import Litestar from litestar.testing import TestClient from strawberry.litestar import make_graphql_controller @strawberry.type class Query: @strawberry.field def abc(self, info: strawberry.Info) -> str: assert info.context.get("response") is not None info.context["response"].headers["X-Strawberry"] = "rocks" return "abc" schema = strawberry.Schema(query=Query) graphql_controller = make_graphql_controller(path="/graphql", schema=schema) app = Litestar(route_handlers=[graphql_controller]) test_client = TestClient(app) response = test_client.post("/graphql", json={"query": "{ abc }"}) assert response.status_code == 200 assert response.json() == {"data": {"abc": "abc"}} assert response.headers["x-strawberry"] == "rocks" def test_set_cookie_headers(): from litestar import Litestar from litestar.testing import TestClient from strawberry.litestar import make_graphql_controller @strawberry.type class Query: @strawberry.field def abc(self, info: strawberry.Info) -> str: assert info.context.get("response") is not None info.context["response"].set_cookie( key="strawberry", value="rocks", ) info.context["response"].set_cookie( key="Litestar", value="rocks", ) return "abc" schema = strawberry.Schema(query=Query) graphql_controller = make_graphql_controller(path="/graphql", schema=schema) app = Litestar(route_handlers=[graphql_controller]) test_client = TestClient(app) response = test_client.post("/graphql", json={"query": "{ abc }"}) assert response.status_code == 200 assert response.json() == {"data": {"abc": "abc"}} assert response.headers["set-cookie"] == ( "strawberry=rocks; Path=/; SameSite=lax, Litestar=rocks; Path=/; SameSite=lax" ) strawberry-graphql-0.287.0/tests/litestar/test_response_status.py000066400000000000000000000030461511033167500254000ustar00rootroot00000000000000import strawberry # TODO: move this to common tests def test_set_custom_http_response_status(): from litestar import Litestar from litestar.testing import TestClient from strawberry.litestar import make_graphql_controller @strawberry.type class Query: @strawberry.field def abc(self, info: strawberry.Info) -> str: assert info.context.get("response") is not None info.context["response"].status_code = 418 return "abc" schema = strawberry.Schema(query=Query) graphql_controller = make_graphql_controller(path="/graphql", schema=schema) app = Litestar(route_handlers=[graphql_controller]) test_client = TestClient(app) response = test_client.post("/graphql", json={"query": "{ abc }"}) assert response.status_code == 418 assert response.json() == {"data": {"abc": "abc"}} def test_set_without_setting_http_response_status(): from litestar import Litestar from litestar.testing import TestClient from strawberry.litestar import make_graphql_controller @strawberry.type class Query: @strawberry.field def abc(self) -> str: return "abc" schema = strawberry.Schema(query=Query) graphql_controller = make_graphql_controller(path="/graphql", schema=schema) app = Litestar(route_handlers=[graphql_controller]) test_client = TestClient(app) response = test_client.post("/graphql", json={"query": "{ abc }"}) assert response.status_code == 200 assert response.json() == {"data": {"abc": "abc"}} strawberry-graphql-0.287.0/tests/objects/000077500000000000000000000000001511033167500203255ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/objects/__init__.py000066400000000000000000000000001511033167500224240ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/objects/generics/000077500000000000000000000000001511033167500221245ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/objects/generics/__init__.py000066400000000000000000000000001511033167500242230ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/objects/generics/test_generic_objects.py000066400000000000000000000423571511033167500266750ustar00rootroot00000000000000import datetime from typing import Annotated, Generic, TypeVar import pytest import strawberry from strawberry.types.base import ( StrawberryList, StrawberryOptional, StrawberryTypeVar, get_object_definition, ) from strawberry.types.union import StrawberryUnion T = TypeVar("T") def test_basic_generic(): directive = object() @strawberry.type class Edge(Generic[T]): node_field: T = strawberry.field(directives=[directive]) definition = get_object_definition(Edge, strict=True) assert definition.is_graphql_generic assert definition.type_params == [T] [field] = definition.fields assert field.python_name == "node_field" assert isinstance(field.type, StrawberryTypeVar) assert field.type.type_var is T # let's make a copy of this generic type copy = get_object_definition(Edge, strict=True).copy_with({"T": str}) definition_copy = get_object_definition(copy, strict=True) assert not definition_copy.is_graphql_generic assert definition_copy.type_params == [] [field_copy] = definition_copy.fields assert field_copy.python_name == "node_field" assert field_copy.type is str assert field_copy.directives == [directive] def test_generics_nested(): @strawberry.type class Edge(Generic[T]): node: T @strawberry.type class Connection(Generic[T]): edge: Edge[T] definition = get_object_definition(Connection, strict=True) assert definition.is_graphql_generic assert definition.type_params == [T] [field] = definition.fields assert field.python_name == "edge" assert get_object_definition(field.type, strict=True).type_params == [T] # let's make a copy of this generic type definition_copy = get_object_definition( get_object_definition(Connection, strict=True).copy_with({"T": str}), strict=True, ) assert not definition_copy.is_graphql_generic assert definition_copy.type_params == [] [field_copy] = definition_copy.fields assert field_copy.python_name == "edge" def test_generics_name(): @strawberry.type(name="AnotherName") class EdgeName: node: str @strawberry.type class Connection(Generic[T]): edge: T definition_copy = get_object_definition( get_object_definition(Connection, strict=True).copy_with({"T": EdgeName}), strict=True, ) assert not definition_copy.is_graphql_generic assert definition_copy.type_params == [] [field_copy] = definition_copy.fields assert field_copy.python_name == "edge" def test_generics_nested_in_list(): @strawberry.type class Edge(Generic[T]): node: T @strawberry.type class Connection(Generic[T]): edges: list[Edge[T]] definition = get_object_definition(Connection, strict=True) assert definition.is_graphql_generic assert definition.type_params == [T] [field] = definition.fields assert field.python_name == "edges" assert isinstance(field.type, StrawberryList) assert get_object_definition(field.type.of_type, strict=True).type_params == [T] # let's make a copy of this generic type definition_copy = get_object_definition( definition.copy_with({"T": str}), strict=True, ) assert not definition_copy.is_graphql_generic assert definition_copy.type_params == [] [field_copy] = definition_copy.fields assert field_copy.python_name == "edges" assert isinstance(field_copy.type, StrawberryList) def test_list_inside_generic(): T = TypeVar("T") @strawberry.type class Value(Generic[T]): valuation_date: datetime.date value: T @strawberry.type class Foo: string: Value[str] strings: Value[list[str]] optional_string: Value[str | None] optional_strings: Value[list[str] | None] definition = get_object_definition(Foo, strict=True) assert not definition.is_graphql_generic [ string_field, strings_field, optional_string_field, optional_strings_field, ] = definition.fields assert string_field.python_name == "string" assert strings_field.python_name == "strings" assert optional_string_field.python_name == "optional_string" assert optional_strings_field.python_name == "optional_strings" def test_generic_with_optional(): @strawberry.type class Edge(Generic[T]): node: T | None definition = get_object_definition(Edge, strict=True) assert definition.is_graphql_generic assert definition.type_params == [T] [field] = definition.fields assert field.python_name == "node" assert isinstance(field.type, StrawberryOptional) assert isinstance(field.type.of_type, StrawberryTypeVar) assert field.type.of_type.type_var is T # let's make a copy of this generic type definition_copy = get_object_definition( definition.copy_with({"T": str}), strict=True ) assert not definition_copy.is_graphql_generic assert definition_copy.type_params == [] [field_copy] = definition_copy.fields assert field_copy.python_name == "node" assert isinstance(field_copy.type, StrawberryOptional) assert field_copy.type.of_type is str def test_generic_with_list(): @strawberry.type class Connection(Generic[T]): edges: list[T] definition = get_object_definition(Connection, strict=True) assert definition.is_graphql_generic assert definition.type_params == [T] [field] = definition.fields assert field.python_name == "edges" assert isinstance(field.type, StrawberryList) assert isinstance(field.type.of_type, StrawberryTypeVar) assert field.type.of_type.type_var is T # let's make a copy of this generic type definition_copy = get_object_definition( definition.copy_with({"T": str}), strict=True ) assert not definition_copy.is_graphql_generic assert definition_copy.type_params == [] [field_copy] = definition_copy.fields assert field_copy.python_name == "edges" assert isinstance(field_copy.type, StrawberryList) assert field_copy.type.of_type is str def test_generic_with_list_of_optionals(): @strawberry.type class Connection(Generic[T]): edges: list[T | None] definition = get_object_definition(Connection, strict=True) assert definition.is_graphql_generic assert definition.type_params == [T] [field] = definition.fields assert field.python_name == "edges" assert isinstance(field.type, StrawberryList) assert isinstance(field.type.of_type, StrawberryOptional) assert isinstance(field.type.of_type.of_type, StrawberryTypeVar) assert field.type.of_type.of_type.type_var is T # let's make a copy of this generic type definition_copy = get_object_definition( definition.copy_with({"T": str}), strict=True ) assert not definition_copy.is_graphql_generic assert definition_copy.type_params == [] [field_copy] = definition_copy.fields assert field_copy.python_name == "edges" assert isinstance(field_copy.type, StrawberryList) assert isinstance(field_copy.type.of_type, StrawberryOptional) assert field_copy.type.of_type.of_type is str def test_generics_with_unions(): @strawberry.type class Error: message: str @strawberry.type class Edge(Generic[T]): node: Error | T definition = get_object_definition(Edge, strict=True) assert definition.type_params == [T] [field] = definition.fields assert field.python_name == "node" assert isinstance(field.type, StrawberryUnion) assert field.type.types == (Error, T) # let's make a copy of this generic type @strawberry.type class Node: name: str definition_copy = get_object_definition( definition.copy_with({"T": Node}), strict=True ) assert not definition_copy.is_graphql_generic assert definition_copy.type_params == [] [field_copy] = definition_copy.fields assert field_copy.python_name == "node" assert isinstance(field_copy.type, StrawberryUnion) assert field_copy.type.types == (Error, Node) def test_using_generics(): @strawberry.type class Edge(Generic[T]): node: T @strawberry.type class User: name: str @strawberry.type class Query: user: Edge[User] definition = get_object_definition(Query, strict=True) [field] = definition.fields assert field.python_name == "user" user_edge_definition = get_object_definition(field.type, strict=True) assert not user_edge_definition.is_graphql_generic [node_field] = user_edge_definition.fields assert node_field.python_name == "node" assert node_field.type is User def test_using_generics_nested(): @strawberry.type class Edge(Generic[T]): node: T @strawberry.type class Connection(Generic[T]): edges: Edge[T] @strawberry.type class User: name: str @strawberry.type class Query: users: Connection[User] connection_definition = get_object_definition(Connection, strict=True) assert connection_definition.is_graphql_generic assert connection_definition.type_params == [T] query_definition = get_object_definition(Query, strict=True) [user_field] = query_definition.fields assert user_field.python_name == "users" user_connection_definition = get_object_definition(user_field.type, strict=True) assert not user_connection_definition.is_graphql_generic [edges_field] = user_connection_definition.fields assert edges_field.python_name == "edges" def test_using_generics_raises_when_missing_annotation(): @strawberry.type class Edge(Generic[T]): node: T @strawberry.type class User: name: str error_message = ( f'Query fields cannot be resolved. The type "{Edge!r}" ' "is generic, but no type has been passed" ) @strawberry.type class Query: user: Edge with pytest.raises(TypeError, match=error_message): strawberry.Schema(Query) def test_using_generics_raises_when_missing_annotation_nested(): @strawberry.type class Edge(Generic[T]): node: T @strawberry.type class Connection(Generic[T]): edges: list[Edge[T]] @strawberry.type class User: name: str error_message = ( f'Query fields cannot be resolved. The type "{Connection!r}" ' "is generic, but no type has been passed" ) @strawberry.type class Query: users: Connection with pytest.raises(TypeError, match=error_message): strawberry.Schema(Query) def test_generics_inside_optional(): @strawberry.type class Error: message: str @strawberry.type class Edge(Generic[T]): node: T @strawberry.type class Query: user: Edge[str] | None query_definition = get_object_definition(Query, strict=True) assert query_definition.type_params == [] [field] = query_definition.fields assert field.python_name == "user" assert isinstance(field.type, StrawberryOptional) str_edge_definition = get_object_definition(field.type.of_type, strict=True) assert not str_edge_definition.is_graphql_generic def test_generics_inside_list(): @strawberry.type class Edge(Generic[T]): node: T @strawberry.type class Query: user: list[Edge[str]] query_definition = get_object_definition(Query, strict=True) assert query_definition.type_params == [] [field] = query_definition.fields assert field.python_name == "user" assert isinstance(field.type, StrawberryList) str_edge_definition = get_object_definition(field.type.of_type, strict=True) assert not str_edge_definition.is_graphql_generic def test_generics_inside_unions(): @strawberry.type class Error: message: str @strawberry.type class Edge(Generic[T]): node: T @strawberry.type class Query: user: Edge[str] | Error query_definition = get_object_definition(Query, strict=True) assert query_definition.type_params == [] [field] = query_definition.fields assert field.python_name == "user" assert not isinstance(field.type, StrawberryOptional) union = field.type assert isinstance(union, StrawberryUnion) assert not get_object_definition(union.types[0], strict=True).is_graphql_generic def test_multiple_generics_inside_unions(): @strawberry.type class Edge(Generic[T]): node: T @strawberry.type class Query: user: Edge[int] | Edge[str] query_definition = get_object_definition(Query, strict=True) assert query_definition.type_params == [] [user_field] = query_definition.fields assert user_field.python_name == "user" assert not isinstance(user_field.type, StrawberryOptional) union = user_field.type assert isinstance(union, StrawberryUnion) int_edge_definition = get_object_definition(union.types[0], strict=True) assert not int_edge_definition.is_graphql_generic assert int_edge_definition.fields[0].type is int str_edge_definition = get_object_definition(union.types[1], strict=True) assert not str_edge_definition.is_graphql_generic assert str_edge_definition.fields[0].type is str def test_union_inside_generics(): @strawberry.type class Dog: name: str @strawberry.type class Cat: name: str @strawberry.type class Connection(Generic[T]): nodes: list[T] DogCat = Annotated[Dog | Cat, strawberry.union("DogCat")] @strawberry.type class Query: connection: Connection[DogCat] query_definition = get_object_definition(Query, strict=True) assert query_definition.type_params == [] [connection_field] = query_definition.fields assert connection_field.python_name == "connection" assert not isinstance(connection_field, StrawberryOptional) dog_cat_connection_definition = get_object_definition( connection_field.type, strict=True ) [node_field] = dog_cat_connection_definition.fields assert isinstance(node_field.type, StrawberryList) union = dog_cat_connection_definition.fields[0].type.of_type assert isinstance(union, StrawberryUnion) def test_anonymous_union_inside_generics(): @strawberry.type class Dog: name: str @strawberry.type class Cat: name: str @strawberry.type class Connection(Generic[T]): nodes: list[T] @strawberry.type class Query: connection: Connection[Dog | Cat] definition = get_object_definition(Query, strict=True) assert definition.type_params == [] [connection_field] = definition.fields assert connection_field.python_name == "connection" dog_cat_connection_definition = get_object_definition( connection_field.type, strict=True ) [node_field] = dog_cat_connection_definition.fields assert isinstance(node_field.type, StrawberryList) union = node_field.type.of_type assert isinstance(union, StrawberryUnion) def test_using_generics_with_interfaces(): @strawberry.type class Edge(Generic[T]): node: T @strawberry.interface class WithName: name: str @strawberry.type class Query: user: Edge[WithName] query_definition = get_object_definition(Query, strict=True) [user_field] = query_definition.fields assert user_field.python_name == "user" with_name_definition = get_object_definition(user_field.type, strict=True) assert not with_name_definition.is_graphql_generic [node_field] = with_name_definition.fields assert node_field.python_name == "node" assert node_field.type is WithName def test_generic_with_arguments(): T = TypeVar("T") @strawberry.type class Collection(Generic[T]): @strawberry.field def by_id(self, ids: list[int]) -> list[T]: return [] @strawberry.type class Post: name: str @strawberry.type class Query: user: Collection[Post] query_definition = get_object_definition(Query, strict=True) [user_field] = query_definition.fields assert user_field.python_name == "user" post_collection_definition = get_object_definition(user_field.type, strict=True) assert not post_collection_definition.is_graphql_generic [by_id_field] = post_collection_definition.fields assert by_id_field.python_name == "by_id" assert isinstance(by_id_field.type, StrawberryList) assert by_id_field.type.of_type is Post [ids_argument] = by_id_field.arguments assert ids_argument.python_name == "ids" assert isinstance(ids_argument.type, StrawberryList) assert ids_argument.type.of_type is int def test_federation(): @strawberry.federation.type(keys=["id"]) class Edge(Generic[T]): id: strawberry.ID node_field: T definition = get_object_definition(Edge, strict=True) definition_copy = get_object_definition( definition.copy_with({"T": str}), strict=True ) assert not definition_copy.is_graphql_generic assert definition_copy.type_params == [] assert definition_copy.directives == definition.directives [field1_copy, field2_copy] = definition_copy.fields assert field1_copy.python_name == "id" assert field1_copy.type is strawberry.ID assert field2_copy.python_name == "node_field" assert field2_copy.type is str strawberry-graphql-0.287.0/tests/objects/generics/test_names.py000066400000000000000000000070611511033167500246440ustar00rootroot00000000000000import textwrap from typing import Annotated, Generic, NewType, TypeVar import pytest import strawberry from strawberry.schema.config import StrawberryConfig from strawberry.types.base import StrawberryList, StrawberryOptional from strawberry.types.enum import StrawberryEnumDefinition from strawberry.types.lazy_type import LazyType from strawberry.types.union import StrawberryUnion T = TypeVar("T") K = TypeVar("K") V = TypeVar("V") Enum = StrawberryEnumDefinition(None, name="Enum", values=[], description=None) # type: ignore CustomInt = strawberry.scalar(NewType("CustomInt", int)) @strawberry.type class TypeA: name: str @strawberry.type class TypeB: age: int @pytest.mark.parametrize( ("types", "expected_name"), [ ([StrawberryList(str)], "StrListExample"), ([StrawberryList(StrawberryList(str))], "StrListListExample"), ([StrawberryOptional(StrawberryList(str))], "StrListOptionalExample"), ([StrawberryList(StrawberryOptional(str))], "StrOptionalListExample"), ([StrawberryList(Enum)], "EnumListExample"), ([StrawberryUnion("Union", (TypeA, TypeB))], "UnionExample"), # pyright: ignore ([TypeA], "TypeAExample"), ([CustomInt], "CustomIntExample"), ([TypeA, TypeB], "TypeATypeBExample"), ( [TypeA, LazyType["TypeB", "tests.objects.generics.test_names"]], "TypeATypeBExample", ), # type: ignore ( [ TypeA, Annotated[ "TypeB", strawberry.lazy("tests.objects.generics.test_names") ], ], "TypeATypeBExample", ), ], ) def test_name_generation(types, expected_name): config = StrawberryConfig() @strawberry.type class Example(Generic[T]): a: T type_definition = Example.__strawberry_definition__ # type: ignore assert config.name_converter.from_generic(type_definition, types) == expected_name def test_nested_generics(): config = StrawberryConfig() @strawberry.type class Edge(Generic[T]): node: T @strawberry.type class Connection(Generic[T]): edges: list[T] type_definition = Connection.__strawberry_definition__ # type: ignore assert ( config.name_converter.from_generic( type_definition, [ Edge[int], ], ) == "IntEdgeConnection" ) def test_nested_generics_aliases_with_schema(): """This tests is similar to the previous test, but it also tests against the schema, since the resolution of the type name might be different. """ config = StrawberryConfig() @strawberry.type class Value(Generic[T]): value: T @strawberry.type class DictItem(Generic[K, V]): key: K value: V type_definition = Value.__strawberry_definition__ # type: ignore assert ( config.name_converter.from_generic( type_definition, [ StrawberryList(DictItem[int, str]), ], ) == "IntStrDictItemListValue" ) @strawberry.type class Query: d: Value[list[DictItem[int, str]]] schema = strawberry.Schema(query=Query) expected = textwrap.dedent( """ type IntStrDictItem { key: Int! value: String! } type IntStrDictItemListValue { value: [IntStrDictItem!]! } type Query { d: IntStrDictItemListValue! } """ ).strip() assert str(schema) == expected strawberry-graphql-0.287.0/tests/objects/test_inheritance.py000066400000000000000000000004051511033167500242260ustar00rootroot00000000000000import strawberry def test_inherited_fields(): @strawberry.type class A: a: str = strawberry.field(default="") @strawberry.type class B(A): b: str | None = strawberry.field(default=None) assert strawberry.Schema(query=B) strawberry-graphql-0.287.0/tests/objects/test_interfaces.py000066400000000000000000000063001511033167500240600ustar00rootroot00000000000000import strawberry def test_defining_interface(): @strawberry.interface class Node: id: strawberry.ID definition = Node.__strawberry_definition__ assert definition.name == "Node" assert len(definition.fields) == 1 assert definition.fields[0].python_name == "id" assert definition.fields[0].graphql_name is None assert definition.fields[0].type == strawberry.ID assert definition.is_interface def test_implementing_interfaces(): @strawberry.interface class Node: id: strawberry.ID @strawberry.type class User(Node): name: str definition = User.__strawberry_definition__ assert definition.name == "User" assert len(definition.fields) == 2 assert definition.fields[0].python_name == "id" assert definition.fields[0].graphql_name is None assert definition.fields[0].type == strawberry.ID assert definition.fields[1].python_name == "name" assert definition.fields[1].graphql_name is None assert definition.fields[1].type is str assert definition.is_interface is False assert definition.interfaces == [Node.__strawberry_definition__] def test_implementing_interface_twice(): @strawberry.interface class Node: id: strawberry.ID @strawberry.type class User(Node): name: str @strawberry.type class Person(Node): name: str definition = User.__strawberry_definition__ assert definition.name == "User" assert len(definition.fields) == 2 assert definition.fields[0].python_name == "id" assert definition.fields[0].graphql_name is None assert definition.fields[0].type == strawberry.ID assert definition.fields[1].python_name == "name" assert definition.fields[1].graphql_name is None assert definition.fields[1].type is str assert definition.is_interface is False assert definition.interfaces == [Node.__strawberry_definition__] definition = Person.__strawberry_definition__ assert definition.name == "Person" assert len(definition.fields) == 2 assert definition.fields[0].python_name == "id" assert definition.fields[0].graphql_name is None assert definition.fields[0].type == strawberry.ID assert definition.fields[1].python_name == "name" assert definition.fields[1].graphql_name is None assert definition.fields[1].type is str assert definition.is_interface is False assert definition.interfaces == [Node.__strawberry_definition__] def test_interfaces_can_implement_other_interfaces(): @strawberry.interface class Node: id: strawberry.ID @strawberry.interface class UserNodeInterface(Node): id: strawberry.ID name: str @strawberry.type class Person(UserNodeInterface): id: strawberry.ID name: str assert UserNodeInterface.__strawberry_definition__.is_interface is True assert UserNodeInterface.__strawberry_definition__.interfaces == [ Node.__strawberry_definition__ ] definition = Person.__strawberry_definition__ assert definition.is_interface is False assert definition.interfaces == [ UserNodeInterface.__strawberry_definition__, Node.__strawberry_definition__, ] strawberry-graphql-0.287.0/tests/objects/test_object_instantiation.py000066400000000000000000000005311511033167500261470ustar00rootroot00000000000000import strawberry def test_can_instantiate_types_directly(): @strawberry.type class User: username: str @strawberry.field def email(self) -> str: return self.username + "@somesite.com" user = User(username="abc") assert user.username == "abc" assert user.email() == "abc@somesite.com" strawberry-graphql-0.287.0/tests/objects/test_types.py000066400000000000000000000020611511033167500231010ustar00rootroot00000000000000import pytest import strawberry from strawberry.exceptions import ObjectIsNotClassError @pytest.mark.raises_strawberry_exception( ObjectIsNotClassError, match=( r"strawberry.type can only be used with class types. Provided " r"object .* is not a type." ), ) def test_raises_error_when_using_type_with_a_not_class_object(): @strawberry.type def not_a_class(): pass @pytest.mark.raises_strawberry_exception( ObjectIsNotClassError, match=( r"strawberry.input can only be used with class types. Provided " r"object .* is not a type." ), ) def test_raises_error_when_using_input_with_a_not_class_object(): @strawberry.input def not_a_class(): pass @pytest.mark.raises_strawberry_exception( ObjectIsNotClassError, match=( r"strawberry.interface can only be used with class types. Provided " r"object .* is not a type." ), ) def test_raises_error_when_using_interface_with_a_not_class_object(): @strawberry.interface def not_a_class(): pass strawberry-graphql-0.287.0/tests/plugins/000077500000000000000000000000001511033167500203555ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/plugins/__init__.py000066400000000000000000000000001511033167500224540ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/plugins/strawberry_exceptions.py000066400000000000000000000123371511033167500254020ustar00rootroot00000000000000import contextlib import os import re from collections import defaultdict from collections.abc import Generator from dataclasses import dataclass from pathlib import Path import pytest import rich import rich.console from _pytest.nodes import Item from pluggy._result import _Result from rich.traceback import Traceback from strawberry.exceptions import StrawberryException, UnableToFindExceptionSource WORKSPACE_FOLDER = Path(__file__).parents[2] DOCS_FOLDER = WORKSPACE_FOLDER / "docs/errors" @dataclass class Result: text: str raised_exception: StrawberryException @contextlib.contextmanager def suppress_output(verbosity_level: int = 0) -> Generator[None, None, None]: if verbosity_level >= 2: yield return with ( Path(os.devnull).open("w", encoding="utf-8") as devnull, contextlib.redirect_stdout(devnull), ): yield class StrawberryExceptionsPlugin: def __init__(self, verbosity_level: int) -> None: self._info: defaultdict[type[StrawberryException], list[Result]] = defaultdict( list ) self.verbosity_level = verbosity_level @pytest.hookimpl(hookwrapper=True) def pytest_runtest_call(self, item: Item) -> Generator[None, _Result, None]: __tracebackhide__ = True outcome = yield self._check_strawberry_exception(item, outcome) def _check_strawberry_exception(self, item: Item, outcome: _Result) -> None: __tracebackhide__ = True raises_marker = item.get_closest_marker("raises_strawberry_exception") if raises_marker is None: return exception = raises_marker.args[0] match = raises_marker.kwargs.get("match", None) if not issubclass(exception, StrawberryException): pytest.fail(f"{exception} is not a StrawberryException") raised_exception = outcome.excinfo[1] if outcome.excinfo else None # This plugin needs to work around the other hooks, see: # https://docs.pytest.org/en/7.1.x/how-to/writing_hook_functions.html#hookwrapper-executing-around-other-hooks outcome.force_result(None) if raised_exception is None: failure_message = f"Expected exception {exception}, but it did not raise" pytest.fail(failure_message, pytrace=False) if not isinstance(raised_exception, exception): failure_message = ( f"Expected exception {exception}, but raised {raised_exception}" ) raise raised_exception raised_message = str(raised_exception) self._collect_exception(item.name, raised_exception) if match is not None and not re.match(match, raised_message): failure_message = ( f'"{match}" does not match raised message "{raised_message}"' ) if self.verbosity_level >= 1: print(f"Exception: {exception}") # noqa: T201 pytest.fail(failure_message, pytrace=False) def _collect_exception( self, test_name: str, raised_exception: StrawberryException ) -> None: console = rich.console.Console(record=True, width=120) with suppress_output(self.verbosity_level): try: console.print(raised_exception) except UnableToFindExceptionSource: traceback = Traceback( Traceback.extract( raised_exception.__class__, raised_exception, raised_exception.__traceback__, ), max_frames=10, ) console.print(traceback) exception_text = console.export_text() text = f"## {test_name}\n" if exception_text.strip() == "None": text += "No exception raised\n" else: text += f"\n``````\n{exception_text.strip()}\n``````\n\n" documentation_path = DOCS_FOLDER / f"{raised_exception.documentation_path}.md" if not documentation_path.exists(): pytest.fail( f"{documentation_path.relative_to(WORKSPACE_FOLDER)} does not exist", pytrace=False, ) self._info[raised_exception.__class__].append( Result(text=text, raised_exception=raised_exception) ) def pytest_sessionfinish(self): summary_path = os.environ.get("GITHUB_STEP_SUMMARY", None) if not summary_path: return markdown = "" for exception_class, info in self._info.items(): title = " ".join(re.findall("[a-zA-Z][^A-Z]*", exception_class.__name__)) markdown += f"# {title}\n\n" markdown += ( f"Documentation URL: {info[0].raised_exception.documentation_url}\n\n" ) markdown += "\n".join([result.text for result in info]) with Path(summary_path).open("w") as f: f.write(markdown) def pytest_configure(config): config.pluginmanager.register( StrawberryExceptionsPlugin(verbosity_level=config.getoption("verbose")), "strawberry_exceptions", ) config.addinivalue_line( "markers", "raises_strawberry_exception: expect to raise a strawberry exception.", ) strawberry-graphql-0.287.0/tests/python_312/000077500000000000000000000000001511033167500206025ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/python_312/__init__.py000066400000000000000000000000001511033167500227010ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/python_312/pyproject.toml000066400000000000000000000000451511033167500235150ustar00rootroot00000000000000[tool.ruff] target-version = "py312" strawberry-graphql-0.287.0/tests/python_312/test_generic_objects.py000066400000000000000000000425671511033167500253560ustar00rootroot00000000000000# ruff: noqa: F821 import datetime import sys from typing import Annotated, Optional, Union import pytest import strawberry from strawberry.types.base import ( StrawberryList, StrawberryOptional, StrawberryTypeVar, get_object_definition, ) from strawberry.types.union import StrawberryUnion pytestmark = pytest.mark.skipif( sys.version_info < (3, 12), reason="These are tests for Python 3.12+" ) def test_basic_generic(): directive = object() @strawberry.type class Edge[T]: node_field: T = strawberry.field(directives=[directive]) definition = get_object_definition(Edge, strict=True) assert definition.is_graphql_generic assert definition.type_params[0].__name__ == "T" [field] = definition.fields assert field.python_name == "node_field" assert isinstance(field.type, StrawberryTypeVar) assert field.type.type_var.__name__ == "T" # let's make a copy of this generic type copy = get_object_definition(Edge, strict=True).copy_with({"T": str}) definition_copy = get_object_definition(copy, strict=True) assert not definition_copy.is_graphql_generic assert definition_copy.type_params == [] [field_copy] = definition_copy.fields assert field_copy.python_name == "node_field" assert field_copy.type is str assert field_copy.directives == [directive] def test_generics_nested(): @strawberry.type class Edge[T]: node: T @strawberry.type class Connection[T]: edge: Edge[T] definition = get_object_definition(Connection, strict=True) assert definition.is_graphql_generic assert definition.type_params[0].__name__ == "T" [field] = definition.fields assert field.python_name == "edge" assert get_object_definition(field.type, strict=True).type_params[0].__name__ == "T" # let's make a copy of this generic type definition_copy = get_object_definition( get_object_definition(Connection, strict=True).copy_with({"T": str}), strict=True, ) assert not definition_copy.is_graphql_generic assert definition_copy.type_params == [] [field_copy] = definition_copy.fields assert field_copy.python_name == "edge" def test_generics_name(): @strawberry.type(name="AnotherName") class EdgeName: node: str @strawberry.type class Connection[T]: edge: T definition_copy = get_object_definition( get_object_definition(Connection, strict=True).copy_with({"T": EdgeName}), strict=True, ) assert not definition_copy.is_graphql_generic assert definition_copy.type_params == [] [field_copy] = definition_copy.fields assert field_copy.python_name == "edge" def test_generics_nested_in_list(): @strawberry.type class Edge[T]: node: T @strawberry.type class Connection[T]: edges: list[Edge[T]] definition = get_object_definition(Connection, strict=True) assert definition.is_graphql_generic assert definition.type_params[0].__name__ == "T" [field] = definition.fields assert field.python_name == "edges" assert isinstance(field.type, StrawberryList) assert ( get_object_definition(field.type.of_type, strict=True).type_params[0].__name__ == "T" ) # let's make a copy of this generic type definition_copy = get_object_definition( definition.copy_with({"T": str}), strict=True, ) assert not definition_copy.is_graphql_generic assert definition_copy.type_params == [] [field_copy] = definition_copy.fields assert field_copy.python_name == "edges" assert isinstance(field_copy.type, StrawberryList) def test_list_inside_generic(): @strawberry.type class Value[T]: valuation_date: datetime.date value: T @strawberry.type class Foo: string: Value[str] strings: Value[list[str]] optional_string: Value[Optional[str]] optional_strings: Value[Optional[list[str]]] definition = get_object_definition(Foo, strict=True) assert not definition.is_graphql_generic [ string_field, strings_field, optional_string_field, optional_strings_field, ] = definition.fields assert string_field.python_name == "string" assert strings_field.python_name == "strings" assert optional_string_field.python_name == "optional_string" assert optional_strings_field.python_name == "optional_strings" def test_generic_with_optional(): @strawberry.type class Edge[T]: node: Optional[T] definition = get_object_definition(Edge, strict=True) assert definition.is_graphql_generic assert definition.type_params[0].__name__ == "T" [field] = definition.fields assert field.python_name == "node" assert isinstance(field.type, StrawberryOptional) assert isinstance(field.type.of_type, StrawberryTypeVar) assert field.type.of_type.type_var.__name__ == "T" # let's make a copy of this generic type definition_copy = get_object_definition( definition.copy_with({"T": str}), strict=True ) assert not definition_copy.is_graphql_generic assert definition_copy.type_params == [] [field_copy] = definition_copy.fields assert field_copy.python_name == "node" assert isinstance(field_copy.type, StrawberryOptional) assert field_copy.type.of_type is str def test_generic_with_list(): @strawberry.type class Connection[T]: edges: list[T] definition = get_object_definition(Connection, strict=True) assert definition.is_graphql_generic assert definition.type_params[0].__name__ == "T" [field] = definition.fields assert field.python_name == "edges" assert isinstance(field.type, StrawberryList) assert isinstance(field.type.of_type, StrawberryTypeVar) assert field.type.of_type.type_var.__name__ == "T" # let's make a copy of this generic type definition_copy = get_object_definition( definition.copy_with({"T": str}), strict=True ) assert not definition_copy.is_graphql_generic assert definition_copy.type_params == [] [field_copy] = definition_copy.fields assert field_copy.python_name == "edges" assert isinstance(field_copy.type, StrawberryList) assert field_copy.type.of_type is str def test_generic_with_list_of_optionals(): @strawberry.type class Connection[T]: edges: list[Optional[T]] definition = get_object_definition(Connection, strict=True) assert definition.is_graphql_generic assert definition.type_params[0].__name__ == "T" [field] = definition.fields assert field.python_name == "edges" assert isinstance(field.type, StrawberryList) assert isinstance(field.type.of_type, StrawberryOptional) assert isinstance(field.type.of_type.of_type, StrawberryTypeVar) assert field.type.of_type.of_type.type_var.__name__ == "T" # let's make a copy of this generic type definition_copy = get_object_definition( definition.copy_with({"T": str}), strict=True ) assert not definition_copy.is_graphql_generic assert definition_copy.type_params == [] [field_copy] = definition_copy.fields assert field_copy.python_name == "edges" assert isinstance(field_copy.type, StrawberryList) assert isinstance(field_copy.type.of_type, StrawberryOptional) assert field_copy.type.of_type.of_type is str def test_generics_with_unions(): @strawberry.type class Error: message: str @strawberry.type class Edge[T]: node: Union[Error, T] definition = get_object_definition(Edge, strict=True) assert definition.type_params[0].__name__ == "T" [field] = definition.fields assert field.python_name == "node" assert isinstance(field.type, StrawberryUnion) assert field.type.types[0] is Error assert isinstance(field.type.types[1], StrawberryTypeVar) assert field.type.types[1].type_var.__name__ == "T" # let's make a copy of this generic type @strawberry.type class Node: name: str definition_copy = get_object_definition( definition.copy_with({"T": Node}), strict=True ) assert not definition_copy.is_graphql_generic assert definition_copy.type_params == [] [field_copy] = definition_copy.fields assert field_copy.python_name == "node" assert isinstance(field_copy.type, StrawberryUnion) assert field_copy.type.types == (Error, Node) def test_using_generics(): @strawberry.type class Edge[T]: node: T @strawberry.type class User: name: str @strawberry.type class Query: user: Edge[User] definition = get_object_definition(Query, strict=True) [field] = definition.fields assert field.python_name == "user" user_edge_definition = get_object_definition(field.type, strict=True) assert not user_edge_definition.is_graphql_generic [node_field] = user_edge_definition.fields assert node_field.python_name == "node" assert node_field.type is User def test_using_generics_nested(): @strawberry.type class Edge[T]: node: T @strawberry.type class Connection[T]: edges: Edge[T] @strawberry.type class User: name: str @strawberry.type class Query: users: Connection[User] connection_definition = get_object_definition(Connection, strict=True) assert connection_definition.is_graphql_generic assert connection_definition.type_params[0].__name__ == "T" query_definition = get_object_definition(Query, strict=True) [user_field] = query_definition.fields assert user_field.python_name == "users" user_connection_definition = get_object_definition(user_field.type, strict=True) assert not user_connection_definition.is_graphql_generic [edges_field] = user_connection_definition.fields assert edges_field.python_name == "edges" def test_using_generics_raises_when_missing_annotation(): @strawberry.type class Edge[T]: node: T error_message = ( f'Query fields cannot be resolved. The type "{Edge!r}" ' "is generic, but no type has been passed" ) @strawberry.type class Query: user: Edge with pytest.raises(TypeError, match=error_message): strawberry.Schema(Query) def test_using_generics_raises_when_missing_annotation_nested(): @strawberry.type class Edge[T]: node: T @strawberry.type class Connection[T]: edges: list[Edge[T]] @strawberry.type class User: name: str error_message = ( f'Query fields cannot be resolved. The type "{Connection!r}" ' "is generic, but no type has been passed" ) @strawberry.type class Query: users: Connection with pytest.raises(TypeError, match=error_message): strawberry.Schema(Query) def test_generics_inside_optional(): @strawberry.type class Error: message: str @strawberry.type class Edge[T]: node: T @strawberry.type class Query: user: Optional[Edge[str]] query_definition = get_object_definition(Query, strict=True) assert query_definition.type_params == [] [field] = query_definition.fields assert field.python_name == "user" assert isinstance(field.type, StrawberryOptional) str_edge_definition = get_object_definition(field.type.of_type, strict=True) assert not str_edge_definition.is_graphql_generic def test_generics_inside_list(): @strawberry.type class Edge[T]: node: T @strawberry.type class Query: user: list[Edge[str]] query_definition = get_object_definition(Query, strict=True) assert query_definition.type_params == [] [field] = query_definition.fields assert field.python_name == "user" assert isinstance(field.type, StrawberryList) str_edge_definition = get_object_definition(field.type.of_type, strict=True) assert not str_edge_definition.is_graphql_generic def test_generics_inside_unions(): @strawberry.type class Error: message: str @strawberry.type class Edge[T]: node: T @strawberry.type class Query: user: Union[Edge[str], Error] query_definition = get_object_definition(Query, strict=True) assert query_definition.type_params == [] [field] = query_definition.fields assert field.python_name == "user" assert not isinstance(field.type, StrawberryOptional) union = field.type assert isinstance(union, StrawberryUnion) assert not get_object_definition(union.types[0], strict=True).is_graphql_generic def test_multiple_generics_inside_unions(): @strawberry.type class Edge[T]: node: T @strawberry.type class Query: user: Union[Edge[int], Edge[str]] query_definition = get_object_definition(Query, strict=True) assert query_definition.type_params == [] [user_field] = query_definition.fields assert user_field.python_name == "user" assert not isinstance(user_field.type, StrawberryOptional) union = user_field.type assert isinstance(union, StrawberryUnion) int_edge_definition = get_object_definition(union.types[0], strict=True) assert not int_edge_definition.is_graphql_generic assert int_edge_definition.fields[0].type is int str_edge_definition = get_object_definition(union.types[1], strict=True) assert not str_edge_definition.is_graphql_generic assert str_edge_definition.fields[0].type is str def test_union_inside_generics(): @strawberry.type class Dog: name: str @strawberry.type class Cat: name: str @strawberry.type class Connection[T]: nodes: list[T] DogCat = Annotated[Union[Dog, Cat], strawberry.union("DogCat")] @strawberry.type class Query: connection: Connection[DogCat] query_definition = get_object_definition(Query, strict=True) assert query_definition.type_params == [] [connection_field] = query_definition.fields assert connection_field.python_name == "connection" assert not isinstance(connection_field, StrawberryOptional) dog_cat_connection_definition = get_object_definition( connection_field.type, strict=True ) [node_field] = dog_cat_connection_definition.fields assert isinstance(node_field.type, StrawberryList) union = dog_cat_connection_definition.fields[0].type.of_type assert isinstance(union, StrawberryUnion) def test_anonymous_union_inside_generics(): @strawberry.type class Dog: name: str @strawberry.type class Cat: name: str @strawberry.type class Connection[T]: nodes: list[T] @strawberry.type class Query: connection: Connection[Union[Dog, Cat]] definition = get_object_definition(Query, strict=True) assert definition.type_params == [] [connection_field] = definition.fields assert connection_field.python_name == "connection" dog_cat_connection_definition = get_object_definition( connection_field.type, strict=True ) [node_field] = dog_cat_connection_definition.fields assert isinstance(node_field.type, StrawberryList) union = node_field.type.of_type assert isinstance(union, StrawberryUnion) def test_using_generics_with_interfaces(): @strawberry.type class Edge[T]: node: T @strawberry.interface class WithName: name: str @strawberry.type class Query: user: Edge[WithName] query_definition = get_object_definition(Query, strict=True) [user_field] = query_definition.fields assert user_field.python_name == "user" with_name_definition = get_object_definition(user_field.type, strict=True) assert not with_name_definition.is_graphql_generic [node_field] = with_name_definition.fields assert node_field.python_name == "node" assert node_field.type is WithName def test_generic_with_arguments(): @strawberry.type class Collection[T]: @strawberry.field def by_id(self, ids: list[int]) -> list[T]: return [] @strawberry.type class Post: name: str @strawberry.type class Query: user: Collection[Post] query_definition = get_object_definition(Query, strict=True) [user_field] = query_definition.fields assert user_field.python_name == "user" post_collection_definition = get_object_definition(user_field.type, strict=True) assert not post_collection_definition.is_graphql_generic [by_id_field] = post_collection_definition.fields assert by_id_field.python_name == "by_id" assert isinstance(by_id_field.type, StrawberryList) assert by_id_field.type.of_type is Post [ids_argument] = by_id_field.arguments assert ids_argument.python_name == "ids" assert isinstance(ids_argument.type, StrawberryList) assert ids_argument.type.of_type is int def test_federation(): @strawberry.federation.type(keys=["id"]) class Edge[T]: id: strawberry.ID node_field: T definition = get_object_definition(Edge, strict=True) definition_copy = get_object_definition( definition.copy_with({"T": str}), strict=True ) assert not definition_copy.is_graphql_generic assert definition_copy.type_params == [] assert definition_copy.directives == definition.directives [field1_copy, field2_copy] = definition_copy.fields assert field1_copy.python_name == "id" assert field1_copy.type is strawberry.ID assert field2_copy.python_name == "node_field" assert field2_copy.type is str strawberry-graphql-0.287.0/tests/python_312/test_generics_schema.py000066400000000000000000000606711511033167500253440ustar00rootroot00000000000000# ruff: noqa: F821 import sys import textwrap from enum import Enum from typing import Any, Optional, Union from typing_extensions import Self import pytest import strawberry pytestmark = pytest.mark.skipif( sys.version_info < (3, 12), reason="These are tests for Python 3.12+" ) def test_supports_generic_simple_type(): @strawberry.type class Edge[T]: cursor: strawberry.ID node_field: T @strawberry.type class Query: @strawberry.field def example(self) -> Edge[int]: return Edge(cursor=strawberry.ID("1"), node_field=1) schema = strawberry.Schema(query=Query) query = """{ example { __typename cursor nodeField } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == { "example": {"__typename": "IntEdge", "cursor": "1", "nodeField": 1} } def test_supports_generic_specialized(): @strawberry.type class Edge[T]: cursor: strawberry.ID node_field: T @strawberry.type class IntEdge(Edge[int]): ... @strawberry.type class Query: @strawberry.field def example(self) -> IntEdge: return IntEdge(cursor=strawberry.ID("1"), node_field=1) schema = strawberry.Schema(query=Query) query = """{ example { __typename cursor nodeField } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == { "example": {"__typename": "IntEdge", "cursor": "1", "nodeField": 1} } def test_supports_generic_specialized_subclass(): @strawberry.type class Edge[T]: cursor: strawberry.ID node_field: T @strawberry.type class IntEdge(Edge[int]): ... @strawberry.type class IntEdgeSubclass(IntEdge): ... @strawberry.type class Query: @strawberry.field def example(self) -> IntEdgeSubclass: return IntEdgeSubclass(cursor=strawberry.ID("1"), node_field=1) schema = strawberry.Schema(query=Query) query = """{ example { __typename cursor nodeField } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == { "example": {"__typename": "IntEdgeSubclass", "cursor": "1", "nodeField": 1} } def test_supports_generic_specialized_with_type(): @strawberry.type class Fruit: name: str @strawberry.type class Edge[T]: cursor: strawberry.ID node_field: T @strawberry.type class FruitEdge(Edge[Fruit]): ... @strawberry.type class Query: @strawberry.field def example(self) -> FruitEdge: return FruitEdge(cursor=strawberry.ID("1"), node_field=Fruit(name="Banana")) schema = strawberry.Schema(query=Query) query = """{ example { __typename cursor nodeField { name } } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == { "example": { "__typename": "FruitEdge", "cursor": "1", "nodeField": {"name": "Banana"}, } } def test_supports_generic_specialized_with_list_type(): @strawberry.type class Fruit: name: str @strawberry.type class Edge[T]: cursor: strawberry.ID nodes: list[T] @strawberry.type class FruitEdge(Edge[Fruit]): ... @strawberry.type class Query: @strawberry.field def example(self) -> FruitEdge: return FruitEdge( cursor=strawberry.ID("1"), nodes=[Fruit(name="Banana"), Fruit(name="Apple")], ) schema = strawberry.Schema(query=Query) query = """{ example { __typename cursor nodes { name } } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == { "example": { "__typename": "FruitEdge", "cursor": "1", "nodes": [ {"name": "Banana"}, {"name": "Apple"}, ], } } def test_supports_generic(): @strawberry.type class Edge[T]: cursor: strawberry.ID node: T @strawberry.type class Person: name: str @strawberry.type class Query: @strawberry.field def example(self) -> Edge[Person]: return Edge(cursor=strawberry.ID("1"), node=Person(name="Example")) schema = strawberry.Schema(query=Query) query = """{ example { __typename cursor node { name } } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == { "example": { "__typename": "PersonEdge", "cursor": "1", "node": {"name": "Example"}, } } def test_supports_multiple_generic(): @strawberry.type class Multiple[A, B]: a: A b: B @strawberry.type class Query: @strawberry.field def multiple(self) -> Multiple[int, str]: return Multiple(a=123, b="123") schema = strawberry.Schema(query=Query) query = """{ multiple { __typename a b } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == { "multiple": {"__typename": "IntStrMultiple", "a": 123, "b": "123"} } def test_support_nested_generics(): @strawberry.type class User: name: str @strawberry.type class Edge[T]: node: T @strawberry.type class Connection[T]: edge: Edge[T] @strawberry.type class Query: @strawberry.field def users(self) -> Connection[User]: return Connection(edge=Edge(node=User(name="Patrick"))) schema = strawberry.Schema(query=Query) query = """{ users { __typename edge { __typename node { name } } } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == { "users": { "__typename": "UserConnection", "edge": {"__typename": "UserEdge", "node": {"name": "Patrick"}}, } } def test_supports_optional(): @strawberry.type class User: name: str @strawberry.type class Edge[T]: node: Optional[T] = None @strawberry.type class Query: @strawberry.field def user(self) -> Edge[User]: return Edge() schema = strawberry.Schema(query=Query) query = """{ user { __typename node { name } } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == {"user": {"__typename": "UserEdge", "node": None}} def test_supports_lists(): @strawberry.type class User: name: str @strawberry.type class Edge[T]: nodes: list[T] @strawberry.type class Query: @strawberry.field def user(self) -> Edge[User]: return Edge(nodes=[]) schema = strawberry.Schema(query=Query) query = """{ user { __typename nodes { name } } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == {"user": {"__typename": "UserEdge", "nodes": []}} def test_supports_lists_of_optionals(): @strawberry.type class User: name: str @strawberry.type class Edge[T]: nodes: list[Optional[T]] @strawberry.type class Query: @strawberry.field def user(self) -> Edge[User]: return Edge(nodes=[None]) schema = strawberry.Schema(query=Query) query = """{ user { __typename nodes { name } } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == {"user": {"__typename": "UserEdge", "nodes": [None]}} def test_can_extend_generics(): @strawberry.type class User: name: str @strawberry.type class Edge[T]: node: T @strawberry.type class Connection[T]: edges: list[Edge[T]] @strawberry.type class ConnectionWithMeta[T](Connection[T]): meta: str @strawberry.type class Query: @strawberry.field def users(self) -> ConnectionWithMeta[User]: return ConnectionWithMeta( meta="123", edges=[Edge(node=User(name="Patrick"))] ) schema = strawberry.Schema(query=Query) query = """{ users { __typename meta edges { __typename node { name } } } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == { "users": { "__typename": "UserConnectionWithMeta", "meta": "123", "edges": [{"__typename": "UserEdge", "node": {"name": "Patrick"}}], } } def test_supports_generic_in_unions(): @strawberry.type class Edge[T]: cursor: strawberry.ID node: T @strawberry.type class Fallback: node: str @strawberry.type class Query: @strawberry.field def example(self) -> Union[Fallback, Edge[int]]: return Edge(cursor=strawberry.ID("1"), node=1) schema = strawberry.Schema(query=Query) query = """{ example { __typename ... on IntEdge { cursor node } } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == { "example": {"__typename": "IntEdge", "cursor": "1", "node": 1} } def test_generic_with_enum_as_param_of_type_inside_unions(): @strawberry.type class Pet: name: str @strawberry.type class ErrorNode[T]: code: T @strawberry.enum class Codes(Enum): a = "a" b = "b" @strawberry.type class Query: @strawberry.field def result(self) -> Union[Pet, ErrorNode[Codes]]: return ErrorNode(code=Codes.a) schema = strawberry.Schema(query=Query) query = """{ result { __typename ... on CodesErrorNode { code } } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == {"result": {"__typename": "CodesErrorNode", "code": "a"}} def test_generic_with_enum(): @strawberry.enum class EstimatedValueEnum(Enum): test = "test" testtest = "testtest" @strawberry.type class EstimatedValue[T]: value: T type: EstimatedValueEnum @strawberry.type class Query: @strawberry.field def estimated_value(self) -> Optional[EstimatedValue[int]]: return EstimatedValue(value=1, type=EstimatedValueEnum.test) schema = strawberry.Schema(query=Query) query = """{ estimatedValue { __typename value type } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == { "estimatedValue": { "__typename": "IntEstimatedValue", "value": 1, "type": "test", } } def test_supports_generic_in_unions_multiple_vars(): @strawberry.type class Edge[A, B]: info: A node: B @strawberry.type class Fallback: node: str @strawberry.type class Query: @strawberry.field def example(self) -> Union[Fallback, Edge[int, str]]: return Edge(node="string", info=1) schema = strawberry.Schema(query=Query) query = """{ example { __typename ... on IntStrEdge { node info } } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == { "example": {"__typename": "IntStrEdge", "node": "string", "info": 1} } def test_supports_generic_in_unions_with_nesting(): @strawberry.type class User: name: str @strawberry.type class Edge[T]: node: T @strawberry.type class Connection[T]: edge: Edge[T] @strawberry.type class Fallback: node: str @strawberry.type class Query: @strawberry.field def users(self) -> Union[Connection[User], Fallback]: return Connection(edge=Edge(node=User(name="Patrick"))) schema = strawberry.Schema(query=Query) query = """{ users { __typename ... on UserConnection { edge { __typename node { name } } } } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == { "users": { "__typename": "UserConnection", "edge": {"__typename": "UserEdge", "node": {"name": "Patrick"}}, } } def test_supports_multiple_generics_in_union(): @strawberry.type class Edge[T]: cursor: strawberry.ID node: T @strawberry.type class Query: @strawberry.field def example(self) -> list[Union[Edge[int], Edge[str]]]: return [ Edge(cursor=strawberry.ID("1"), node=1), Edge(cursor=strawberry.ID("2"), node="string"), ] schema = strawberry.Schema(query=Query) expected_schema = """ type IntEdge { cursor: ID! node: Int! } union IntEdgeStrEdge = IntEdge | StrEdge type Query { example: [IntEdgeStrEdge!]! } type StrEdge { cursor: ID! node: String! } """ assert str(schema) == textwrap.dedent(expected_schema).strip() query = """{ example { __typename ... on IntEdge { cursor intNode: node } ... on StrEdge { cursor strNode: node } } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == { "example": [ {"__typename": "IntEdge", "cursor": "1", "intNode": 1}, {"__typename": "StrEdge", "cursor": "2", "strNode": "string"}, ] } def test_generated_names(): @strawberry.type class EdgeWithCursor[T]: cursor: strawberry.ID node: T @strawberry.type class SpecialPerson: name: str @strawberry.type class Query: @strawberry.field def person_edge(self) -> EdgeWithCursor[SpecialPerson]: return EdgeWithCursor( cursor=strawberry.ID("1"), node=SpecialPerson(name="Example") ) schema = strawberry.Schema(query=Query) query = """{ personEdge { __typename cursor node { name } } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == { "personEdge": { "__typename": "SpecialPersonEdgeWithCursor", "cursor": "1", "node": {"name": "Example"}, } } def test_supports_lists_within_unions(): @strawberry.type class User: name: str @strawberry.type class Edge[T]: nodes: list[T] @strawberry.type class Query: @strawberry.field def user(self) -> Union[User, Edge[User]]: return Edge(nodes=[User(name="P")]) schema = strawberry.Schema(query=Query) query = """{ user { __typename ... on UserEdge { nodes { name } } } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == {"user": {"__typename": "UserEdge", "nodes": [{"name": "P"}]}} def test_supports_lists_within_unions_empty_list(): @strawberry.type class User: name: str @strawberry.type class Edge[T]: nodes: list[T] @strawberry.type class Query: @strawberry.field def user(self) -> Union[User, Edge[User]]: return Edge(nodes=[]) schema = strawberry.Schema(query=Query) query = """{ user { __typename ... on UserEdge { nodes { name } } } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == {"user": {"__typename": "UserEdge", "nodes": []}} @pytest.mark.xfail def test_raises_error_when_unable_to_find_type(): @strawberry.type class User: name: str @strawberry.type class Edge[T]: nodes: list[T] @strawberry.type class Query: @strawberry.field def user(self) -> Union[User, Edge[User]]: return Edge(nodes=["bad example"]) # type: ignore schema = strawberry.Schema(query=Query) query = """{ user { __typename ... on UserEdge { nodes { name } } } }""" result = schema.execute_sync(query) assert result.errors[0].message == ( "Unable to find type for .Edge'> " "and (,)" ) def test_generic_with_arguments(): @strawberry.type class Collection[T]: @strawberry.field def by_id(self, ids: list[int]) -> list[T]: return [] @strawberry.type class Post: name: str @strawberry.type class Query: user: Collection[Post] schema = strawberry.Schema(Query) expected_schema = """ type Post { name: String! } type PostCollection { byId(ids: [Int!]!): [Post!]! } type Query { user: PostCollection! } """ assert str(schema) == textwrap.dedent(expected_schema).strip() def test_generic_argument(): @strawberry.type class Node[T]: @strawberry.field def edge(self, arg: T) -> bool: return bool(arg) @strawberry.field def edges(self, args: list[T]) -> int: return len(args) @strawberry.type class Query: i_node: Node[int] b_node: Node[bool] schema = strawberry.Schema(Query) expected_schema = """ type BoolNode { edge(arg: Boolean!): Boolean! edges(args: [Boolean!]!): Int! } type IntNode { edge(arg: Int!): Boolean! edges(args: [Int!]!): Int! } type Query { iNode: IntNode! bNode: BoolNode! } """ assert str(schema) == textwrap.dedent(expected_schema).strip() def test_generic_extra_type(): @strawberry.type class Node[T]: field: T @strawberry.type class Query: name: str schema = strawberry.Schema(Query, types=[Node[int]]) expected_schema = """ type IntNode { field: Int! } type Query { name: String! } """ assert str(schema) == textwrap.dedent(expected_schema).strip() def test_generic_extending_with_type_var(): @strawberry.interface class Node[T]: id: strawberry.ID def _resolve(self) -> Optional[T]: return None @strawberry.type class Book(Node[str]): name: str @strawberry.type class Query: @strawberry.field def books(self) -> list[Book]: return [] schema = strawberry.Schema(query=Query) expected_schema = """ type Book implements Node { id: ID! name: String! } interface Node { id: ID! } type Query { books: [Book!]! } """ assert str(schema) == textwrap.dedent(expected_schema).strip() def test_self(): @strawberry.interface class INode: field: Optional[Self] fields: list[Self] @strawberry.type class Node(INode): ... schema = strawberry.Schema(query=Node) expected_schema = """ schema { query: Node } interface INode { field: INode fields: [INode!]! } type Node implements INode { field: Node fields: [Node!]! } """ assert str(schema) == textwrap.dedent(expected_schema).strip() query = """{ field { __typename } fields { __typename } }""" result = schema.execute_sync(query, root_value=Node(field=None, fields=[])) assert result.data == {"field": None, "fields": []} def test_supports_generic_input_type(): @strawberry.input class Input[T]: field: T @strawberry.type class Query: @strawberry.field def field(self, input: Input[str]) -> str: return input.field schema = strawberry.Schema(query=Query) query = """{ field(input: { field: "data" }) }""" result = schema.execute_sync(query) assert not result.errors assert result.data == {"field": "data"} def test_generic_interface(): @strawberry.interface class ObjectType: obj: strawberry.Private[Any] @strawberry.field def repr(self) -> str: return str(self.obj) @strawberry.type class GenericObject[T](ObjectType): @strawberry.field def value(self) -> T: return self.obj @strawberry.type class Query: @strawberry.field def foo(self) -> GenericObject[str]: return GenericObject(obj="foo") schema = strawberry.Schema(query=Query) query_result = schema.execute_sync( """ query { foo { __typename value repr } } """ ) assert not query_result.errors assert query_result.data == { "foo": { "__typename": "StrGenericObject", "value": "foo", "repr": "foo", } } def test_generic_interface_extra_types(): @strawberry.interface class Abstract: x: str = "" @strawberry.type class Real[T](Abstract): y: T @strawberry.type class Query: @strawberry.field def real(self) -> Abstract: return Real[int](y=0) schema = strawberry.Schema(Query, types=[Real[int]]) assert ( str(schema) == textwrap.dedent( """ interface Abstract { x: String! } type IntReal implements Abstract { x: String! y: Int! } type Query { real: Abstract! } """ ).strip() ) query_result = schema.execute_sync("{ real { __typename x } }") assert not query_result.errors assert query_result.data == {"real": {"__typename": "IntReal", "x": ""}} def test_generics_via_anonymous_union(): @strawberry.type class Edge[T]: cursor: str node: T @strawberry.type class Connection[T]: edges: list[Edge[T]] @strawberry.type class Entity1: id: int @strawberry.type class Entity2: id: int @strawberry.type class Query: entities: Connection[Union[Entity1, Entity2]] schema = strawberry.Schema(query=Query) expected_schema = textwrap.dedent( """ type Entity1 { id: Int! } union Entity1Entity2 = Entity1 | Entity2 type Entity1Entity2Connection { edges: [Entity1Entity2Edge!]! } type Entity1Entity2Edge { cursor: String! node: Entity1Entity2! } type Entity2 { id: Int! } type Query { entities: Entity1Entity2Connection! } """ ).strip() assert str(schema) == expected_schema strawberry-graphql-0.287.0/tests/python_312/test_inspect.py000066400000000000000000000060401511033167500236600ustar00rootroot00000000000000# ruff: noqa: F821 import pytest import strawberry from strawberry.utils.inspect import get_specialized_type_var_map @pytest.mark.parametrize("value", [object, type(None), int, str, type("Foo", (), {})]) def test_get_specialized_type_var_map_non_generic(value: type): assert get_specialized_type_var_map(value) is None def test_get_specialized_type_var_map_generic_not_specialized(): @strawberry.type class Foo[T]: ... assert get_specialized_type_var_map(Foo) == {} def test_get_specialized_type_var_map_generic(): @strawberry.type class Foo[T]: ... @strawberry.type class Bar(Foo[int]): ... assert get_specialized_type_var_map(Bar) == {"T": int} def test_get_specialized_type_var_map_from_alias(): @strawberry.type class Foo[T]: ... SpecializedFoo = Foo[int] assert get_specialized_type_var_map(SpecializedFoo) == {"T": int} def test_get_specialized_type_var_map_from_alias_with_inheritance(): @strawberry.type class Foo[T]: ... SpecializedFoo = Foo[int] @strawberry.type class Bar(SpecializedFoo): ... assert get_specialized_type_var_map(Bar) == {"T": int} def test_get_specialized_type_var_map_generic_subclass(): @strawberry.type class Foo[T]: ... @strawberry.type class Bar(Foo[int]): ... @strawberry.type class BarSubclass(Bar): ... assert get_specialized_type_var_map(BarSubclass) == {"T": int} def test_get_specialized_type_var_map_double_generic(): @strawberry.type class Foo[T]: ... @strawberry.type class Bar[T](Foo[T]): ... @strawberry.type class Bin(Bar[int]): ... assert get_specialized_type_var_map(Bin) == {"T": int} def test_get_specialized_type_var_map_double_generic_subclass(): @strawberry.type class Foo[T]: ... @strawberry.type class Bar[T](Foo[T]): ... @strawberry.type class Bin(Bar[int]): ... @strawberry.type class BinSubclass(Bin): ... assert get_specialized_type_var_map(Bin) == {"T": int} def test_get_specialized_type_var_map_double_generic_passthrough(): @strawberry.type class Foo[T]: ... @strawberry.type class Bar[K](Foo[K]): ... @strawberry.type class Bin(Bar[int]): ... assert get_specialized_type_var_map(Bin) == { "T": int, "K": int, } def test_get_specialized_type_var_map_multiple_inheritance(): @strawberry.type class Foo[T]: ... @strawberry.type class Bar[K]: ... @strawberry.type class Bin(Foo[int]): ... @strawberry.type class Baz(Bin, Bar[str]): ... assert get_specialized_type_var_map(Baz) == { "T": int, "K": str, } def test_get_specialized_type_var_map_multiple_inheritance_subclass(): @strawberry.type class Foo[T]: ... @strawberry.type class Bar[K]: ... @strawberry.type class Bin(Foo[int]): ... @strawberry.type class Baz(Bin, Bar[str]): ... @strawberry.type class BazSubclass(Baz): ... assert get_specialized_type_var_map(BazSubclass) == { "T": int, "K": str, } strawberry-graphql-0.287.0/tests/python_312/test_python_generics.py000066400000000000000000000103401511033167500254110ustar00rootroot00000000000000"""These tests are for Generics that don't expose any generic parts to the schema.""" import sys import textwrap import pytest import strawberry pytestmark = pytest.mark.skipif( sys.version_info < (3, 12), reason="These are tests for Python 3.12+" ) def test_does_not_create_a_new_type_when_no_generic_field_exposed(): @strawberry.type class Edge[T]: cursor: strawberry.ID node_field: strawberry.Private[T] @strawberry.type class Query: @strawberry.field def example(self) -> Edge[int]: return Edge(cursor=strawberry.ID("1"), node_field=1) schema = strawberry.Schema(query=Query) query = """{ example { __typename cursor } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == {"example": {"__typename": "Edge", "cursor": "1"}} def test_does_not_create_a_new_type_when_no_generic_field_exposed_argument(): @strawberry.type class Edge[T]: cursor: strawberry.ID def something(input: T) -> T: return input @strawberry.type class Query: @strawberry.field def example(self) -> Edge[int]: return Edge(cursor=strawberry.ID("1")) schema = strawberry.Schema(query=Query) query = """{ example { __typename cursor } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == {"example": {"__typename": "Edge", "cursor": "1"}} def test_with_interface(): @strawberry.interface class GenericInterface[T]: data: strawberry.Private[T] @strawberry.field def value(self) -> str: raise NotImplementedError @strawberry.type class ImplementationOne(GenericInterface[str]): @strawberry.field def value(self) -> str: return self.data @strawberry.type class ImplementationTwo(GenericInterface[bool]): @strawberry.field def value(self) -> str: if self.data is True: return "true" return "false" @strawberry.type class Query: @strawberry.field @staticmethod async def generic_field() -> GenericInterface: # type: ignore return ImplementationOne(data="foo") schema = strawberry.Schema( query=Query, types=[ImplementationOne, ImplementationTwo] ) expected_schema = textwrap.dedent( """ interface GenericInterface { value: String! } type ImplementationOne implements GenericInterface { value: String! } type ImplementationTwo implements GenericInterface { value: String! } type Query { genericField: GenericInterface! } """ ).strip() assert str(schema) == expected_schema def test_with_interface_and_type(): @strawberry.interface class GenericInterface[T]: data: strawberry.Private[T] @strawberry.field def value(self) -> str: raise NotImplementedError @strawberry.type class ImplementationOne(GenericInterface[str]): @strawberry.field def value(self) -> str: return self.data @strawberry.type class ImplementationTwo(GenericInterface[bool]): @strawberry.field def value(self) -> str: if self.data is True: return "true" return "false" @strawberry.type class Query: @strawberry.field @staticmethod async def generic_field() -> GenericInterface[str | bool]: return ImplementationOne(data="foo") schema = strawberry.Schema( query=Query, types=[ImplementationOne, ImplementationTwo] ) expected_schema = textwrap.dedent( """ interface GenericInterface { value: String! } type ImplementationOne implements GenericInterface { value: String! } type ImplementationTwo implements GenericInterface { value: String! } type Query { genericField: GenericInterface! } """ ).strip() assert str(schema) == expected_schema strawberry-graphql-0.287.0/tests/relay/000077500000000000000000000000001511033167500200105ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/relay/__init__.py000066400000000000000000000000001511033167500221070ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/relay/schema.py000066400000000000000000000242661511033167500216340ustar00rootroot00000000000000import dataclasses from collections.abc import ( AsyncGenerator, AsyncIterable, AsyncIterator, Generator, Iterable, Iterator, ) from typing import ( Annotated, Any, NamedTuple, TypeAlias, cast, ) from typing_extensions import Self import strawberry from strawberry import relay from strawberry.permission import BasePermission from strawberry.relay.utils import to_base64 from strawberry.types import Info @strawberry.type class Fruit(relay.Node): id: relay.NodeID[int] name: str color: str @classmethod def resolve_nodes( cls, *, info: strawberry.Info, node_ids: Iterable[str], required: bool = False, ) -> Iterable[Self | None]: if node_ids is not None: return [fruits[nid] if required else fruits.get(nid) for nid in node_ids] return fruits.values() @classmethod def is_type_of(cls, obj: Any, _info: strawberry.Info) -> bool: # This is here to support FruitConcrete, which is mimicing an integration # object which would return an object alike Fruit (e.g. the django integration) return isinstance(obj, (cls, FruitConcrete)) @dataclasses.dataclass class FruitConcrete: id: int name: str color: str @strawberry.type class FruitAsync(relay.Node): id: relay.NodeID[int] name: str color: str @classmethod async def resolve_nodes( cls, *, info: Info | None = None, node_ids: Iterable[str], required: bool = False, ) -> Iterable[Self | None]: if node_ids is not None: return [ fruits_async[nid] if required else fruits_async.get(nid) for nid in node_ids ] return fruits_async.values() @classmethod async def resolve_id(cls, root: Self, *, info: strawberry.Info) -> str: return str(root.id) @strawberry.type class FruitCustomPaginationConnection(relay.Connection[Fruit]): @strawberry.field def something(self) -> str: return "foobar" @classmethod def resolve_connection( cls, nodes: Iterable[Fruit], *, info: Info | None = None, total_count: int | None = None, before: str | None = None, after: str | None = None, first: int | None = None, last: int | None = None, **kwargs: Any, ) -> Self: edges_mapping = { to_base64("fruit_name", n.name): relay.Edge( node=n, cursor=to_base64("fruit_name", n.name), ) for n in sorted(nodes, key=lambda f: f.name) } edges = list(edges_mapping.values()) first_edge = edges[0] if edges else None last_edge = edges[-1] if edges else None if after is not None: after_edge_idx = edges.index(edges_mapping[after]) edges = [e for e in edges if edges.index(e) > after_edge_idx] if before is not None: before_edge_idx = edges.index(edges_mapping[before]) edges = [e for e in edges if edges.index(e) < before_edge_idx] if first is not None: edges = edges[:first] if last is not None: edges = edges[-last:] return cls( edges=edges, page_info=relay.PageInfo( start_cursor=edges[0].cursor if edges else None, end_cursor=edges[-1].cursor if edges else None, has_previous_page=first_edge is not None and bool(edges) and edges[0] != first_edge, has_next_page=last_edge is not None and bool(edges) and edges[-1] != last_edge, ), ) fruits = { str(f.id): f for f in [ Fruit(id=1, name="Banana", color="yellow"), Fruit(id=2, name="Apple", color="red"), Fruit(id=3, name="Pineapple", color="yellow"), Fruit(id=4, name="Grape", color="purple"), Fruit(id=5, name="Orange", color="orange"), ] } fruits_async = { k: FruitAsync(id=v.id, name=v.name, color=v.color) for k, v in fruits.items() } class FruitAlike(NamedTuple): id: int name: str color: str @strawberry.type class FruitAlikeConnection(relay.ListConnection[Fruit]): @classmethod def resolve_node( cls, node: FruitAlike, *, info: strawberry.Info, **kwargs: Any ) -> Fruit: return Fruit( id=node.id, name=node.name, color=node.color, ) def fruits_resolver() -> Iterable[Fruit]: return fruits.values() async def fruits_async_resolver() -> Iterable[FruitAsync]: return fruits_async.values() class DummyPermission(BasePermission): message = "Dummy message" async def has_permission( self, source: Any, info: strawberry.Info, **kwargs: Any ) -> bool: return True FruitsListConnectionAlias: TypeAlias = relay.ListConnection[Fruit] @strawberry.type class Query: node: relay.Node = relay.node() node_with_async_permissions: relay.Node = relay.node( permission_classes=[DummyPermission] ) nodes: list[relay.Node] = relay.node() node_optional: relay.Node | None = relay.node() nodes_optional: list[relay.Node | None] = relay.node() fruits: relay.ListConnection[Fruit] = relay.connection(resolver=fruits_resolver) fruits_lazy: relay.ListConnection[ Annotated["Fruit", strawberry.lazy("tests.relay.schema")] ] = relay.connection(resolver=fruits_resolver) fruits_alias: FruitsListConnectionAlias = relay.connection(resolver=fruits_resolver) fruits_alias_lazy: Annotated[ "FruitsListConnectionAlias", strawberry.lazy("tests.relay.schema"), ] = relay.connection(resolver=fruits_resolver) fruits_async: relay.ListConnection[FruitAsync] = relay.connection( resolver=fruits_async_resolver ) fruits_custom_pagination: FruitCustomPaginationConnection = relay.connection( resolver=fruits_resolver ) @relay.connection(relay.ListConnection[Fruit]) def fruits_concrete_resolver( self, info: strawberry.Info, name_endswith: str | None = None, ) -> list[Fruit]: # This is mimicing integrations, like Django return [ cast( "Fruit", FruitConcrete( id=f.id, name=f.name, color=f.color, ), ) for f in fruits.values() if name_endswith is None or f.name.endswith(name_endswith) ] @relay.connection(relay.ListConnection[Fruit]) def fruits_custom_resolver( self, info: strawberry.Info, name_endswith: str | None = None, ) -> list[Fruit]: return [ f for f in fruits.values() if name_endswith is None or f.name.endswith(name_endswith) ] @relay.connection(relay.ListConnection[Fruit]) def fruits_custom_resolver_lazy( self, info: strawberry.Info, name_endswith: str | None = None, ) -> list[Annotated["Fruit", strawberry.lazy("tests.relay.schema")]]: return [ f for f in fruits.values() if name_endswith is None or f.name.endswith(name_endswith) ] @relay.connection(relay.ListConnection[Fruit]) def fruits_custom_resolver_iterator( self, info: strawberry.Info, name_endswith: str | None = None, ) -> Iterator[Fruit]: for f in fruits.values(): if name_endswith is None or f.name.endswith(name_endswith): yield f @relay.connection(relay.ListConnection[Fruit]) def fruits_custom_resolver_iterable( self, info: strawberry.Info, name_endswith: str | None = None, ) -> Iterator[Fruit]: for f in fruits.values(): if name_endswith is None or f.name.endswith(name_endswith): yield f @relay.connection(relay.ListConnection[Fruit]) def fruits_custom_resolver_generator( self, info: strawberry.Info, name_endswith: str | None = None, ) -> Generator[Fruit, None, None]: for f in fruits.values(): if name_endswith is None or f.name.endswith(name_endswith): yield f @relay.connection(relay.ListConnection[Fruit]) async def fruits_custom_resolver_async_iterable( self, info: strawberry.Info, name_endswith: str | None = None, ) -> AsyncIterable[Fruit]: for f in fruits.values(): if name_endswith is None or f.name.endswith(name_endswith): yield f @relay.connection(relay.ListConnection[Fruit]) async def fruits_custom_resolver_async_iterator( self, info: strawberry.Info, name_endswith: str | None = None, ) -> AsyncIterator[Fruit]: for f in fruits.values(): if name_endswith is None or f.name.endswith(name_endswith): yield f @relay.connection(relay.ListConnection[Fruit]) async def fruits_custom_resolver_async_generator( self, info: strawberry.Info, name_endswith: str | None = None, ) -> AsyncGenerator[Fruit, None]: for f in fruits.values(): if name_endswith is None or f.name.endswith(name_endswith): yield f @relay.connection(FruitAlikeConnection) def fruit_alike_connection_custom_resolver( self, info: strawberry.Info, name_endswith: str | None = None, ) -> list[FruitAlike]: return [ FruitAlike(f.id, f.name, f.color) for f in fruits.values() if name_endswith is None or f.name.endswith(name_endswith) ] @strawberry.relay.connection(strawberry.relay.ListConnection[Fruit]) def some_fruits(self) -> list[Fruit]: return [Fruit(id=x, name="apple", color="green") for x in range(200)] @strawberry.type class CreateFruitPayload: fruit: Fruit @strawberry.type class Mutation: @strawberry.mutation def create_fruit( self, info: strawberry.Info, name: str, color: str, ) -> CreateFruitPayload: ... schema = strawberry.Schema(query=Query, mutation=Mutation) strawberry-graphql-0.287.0/tests/relay/schema_future_annotations.py000066400000000000000000000236001511033167500256320ustar00rootroot00000000000000from __future__ import annotations import dataclasses from collections.abc import ( AsyncGenerator, AsyncIterable, AsyncIterator, Generator, Iterable, Iterator, ) from typing import ( Annotated, Any, NamedTuple, cast, ) from typing_extensions import Self import strawberry from strawberry import relay from strawberry.permission import BasePermission from strawberry.relay.utils import to_base64 from strawberry.types import Info @strawberry.type class Fruit(relay.Node): id: relay.NodeID[int] name: str color: str @classmethod def resolve_nodes( cls, *, info: strawberry.Info, node_ids: Iterable[str], required: bool = False, ) -> Iterable[Self | None]: if node_ids is not None: return [fruits[nid] if required else fruits.get(nid) for nid in node_ids] return fruits.values() @classmethod def is_type_of(cls, obj: Any, _info: strawberry.Info) -> bool: # This is here to support FruitConcrete, which is mimicing an integration # object which would return an object alike Fruit (e.g. the django integration) return isinstance(obj, (cls, FruitConcrete)) @dataclasses.dataclass class FruitConcrete: id: int name: str color: str @strawberry.type class FruitAsync(relay.Node): id: relay.NodeID[int] name: str color: str @classmethod async def resolve_nodes( cls, *, info: Info | None = None, node_ids: Iterable[str], required: bool = False, ) -> Iterable[Self | None]: if node_ids is not None: return [ fruits_async[nid] if required else fruits_async.get(nid) for nid in node_ids ] return fruits_async.values() @classmethod async def resolve_id(cls, root: Self, *, info: strawberry.Info) -> str: return str(root.id) @strawberry.type class FruitCustomPaginationConnection(relay.Connection[Fruit]): @strawberry.field def something(self) -> str: return "foobar" @classmethod def resolve_connection( cls, nodes: Iterable[Fruit], *, info: Info | None = None, total_count: int | None = None, before: str | None = None, after: str | None = None, first: int | None = None, last: int | None = None, **kwargs: Any, ) -> Self: edges_mapping = { to_base64("fruit_name", n.name): relay.Edge( node=n, cursor=to_base64("fruit_name", n.name), ) for n in sorted(nodes, key=lambda f: f.name) } edges = list(edges_mapping.values()) first_edge = edges[0] if edges else None last_edge = edges[-1] if edges else None if after is not None: after_edge_idx = edges.index(edges_mapping[after]) edges = [e for e in edges if edges.index(e) > after_edge_idx] if before is not None: before_edge_idx = edges.index(edges_mapping[before]) edges = [e for e in edges if edges.index(e) < before_edge_idx] if first is not None: edges = edges[:first] if last is not None: edges = edges[-last:] return cls( edges=edges, page_info=relay.PageInfo( start_cursor=edges[0].cursor if edges else None, end_cursor=edges[-1].cursor if edges else None, has_previous_page=first_edge is not None and bool(edges) and edges[0] != first_edge, has_next_page=last_edge is not None and bool(edges) and edges[-1] != last_edge, ), ) fruits = { str(f.id): f for f in [ Fruit(id=1, name="Banana", color="yellow"), Fruit(id=2, name="Apple", color="red"), Fruit(id=3, name="Pineapple", color="yellow"), Fruit(id=4, name="Grape", color="purple"), Fruit(id=5, name="Orange", color="orange"), ] } fruits_async = { k: FruitAsync(id=v.id, name=v.name, color=v.color) for k, v in fruits.items() } class FruitAlike(NamedTuple): id: int name: str color: str @strawberry.type class FruitAlikeConnection(relay.ListConnection[Fruit]): @classmethod def resolve_node( cls, node: FruitAlike, *, info: strawberry.Info, **kwargs: Any ) -> Fruit: return Fruit( id=node.id, name=node.name, color=node.color, ) def fruits_resolver() -> Iterable[Fruit]: return fruits.values() async def fruits_async_resolver() -> Iterable[FruitAsync]: return fruits_async.values() class DummyPermission(BasePermission): message = "Dummy message" async def has_permission( self, source: Any, info: strawberry.Info, **kwargs: Any ) -> bool: return True @strawberry.type class Query: node: relay.Node = relay.node() node_with_async_permissions: relay.Node = relay.node( permission_classes=[DummyPermission] ) nodes: list[relay.Node] = relay.node() node_optional: relay.Node | None = relay.node() nodes_optional: list[relay.Node | None] = relay.node() fruits: relay.ListConnection[Fruit] = relay.connection(resolver=fruits_resolver) fruits_lazy: relay.ListConnection[ Annotated[Fruit, strawberry.lazy("tests.relay.schema")] ] = relay.connection(resolver=fruits_resolver) fruits_async: relay.ListConnection[FruitAsync] = relay.connection( resolver=fruits_async_resolver ) fruits_custom_pagination: FruitCustomPaginationConnection = relay.connection( resolver=fruits_resolver ) @relay.connection(relay.ListConnection[Fruit]) def fruits_concrete_resolver( self, info: strawberry.Info, name_endswith: str | None = None, ) -> list[Fruit]: # This is mimicing integrations, like Django return [ cast( "Fruit", FruitConcrete( id=f.id, name=f.name, color=f.color, ), ) for f in fruits.values() if name_endswith is None or f.name.endswith(name_endswith) ] @relay.connection(relay.ListConnection[Fruit]) def fruits_custom_resolver( self, info: strawberry.Info, name_endswith: str | None = None, ) -> list[Fruit]: return [ f for f in fruits.values() if name_endswith is None or f.name.endswith(name_endswith) ] @relay.connection(relay.ListConnection[Fruit]) def fruits_custom_resolver_lazy( self, info: strawberry.Info, name_endswith: str | None = None, ) -> list[Annotated[Fruit, strawberry.lazy("tests.relay.schema")]]: return [ f for f in fruits.values() if name_endswith is None or f.name.endswith(name_endswith) ] @relay.connection(relay.ListConnection[Fruit]) def fruits_custom_resolver_iterator( self, info: strawberry.Info, name_endswith: str | None = None, ) -> Iterator[Fruit]: for f in fruits.values(): if name_endswith is None or f.name.endswith(name_endswith): yield f @relay.connection(relay.ListConnection[Fruit]) def fruits_custom_resolver_iterable( self, info: strawberry.Info, name_endswith: str | None = None, ) -> Iterator[Fruit]: for f in fruits.values(): if name_endswith is None or f.name.endswith(name_endswith): yield f @relay.connection(relay.ListConnection[Fruit]) def fruits_custom_resolver_generator( self, info: strawberry.Info, name_endswith: str | None = None, ) -> Generator[Fruit, None, None]: for f in fruits.values(): if name_endswith is None or f.name.endswith(name_endswith): yield f @relay.connection(relay.ListConnection[Fruit]) async def fruits_custom_resolver_async_iterable( self, info: strawberry.Info, name_endswith: str | None = None, ) -> AsyncIterable[Fruit]: for f in fruits.values(): if name_endswith is None or f.name.endswith(name_endswith): yield f @relay.connection(relay.ListConnection[Fruit]) async def fruits_custom_resolver_async_iterator( self, info: strawberry.Info, name_endswith: str | None = None, ) -> AsyncIterator[Fruit]: for f in fruits.values(): if name_endswith is None or f.name.endswith(name_endswith): yield f @relay.connection(relay.ListConnection[Fruit]) async def fruits_custom_resolver_async_generator( self, info: strawberry.Info, name_endswith: str | None = None, ) -> AsyncGenerator[Fruit, None]: for f in fruits.values(): if name_endswith is None or f.name.endswith(name_endswith): yield f @relay.connection(FruitAlikeConnection) def fruit_alike_connection_custom_resolver( self, info: strawberry.Info, name_endswith: str | None = None, ) -> list[FruitAlike]: return [ FruitAlike(f.id, f.name, f.color) for f in fruits.values() if name_endswith is None or f.name.endswith(name_endswith) ] @strawberry.relay.connection(strawberry.relay.ListConnection[Fruit]) def some_fruits(self) -> list[Fruit]: return [Fruit(id=x, name="apple", color="green") for x in range(200)] @strawberry.type class CreateFruitPayload: fruit: Fruit @strawberry.type class Mutation: @strawberry.mutation def create_fruit( self, info: strawberry.Info, name: str, color: str, ) -> CreateFruitPayload: ... schema = strawberry.Schema(query=Query, mutation=Mutation) strawberry-graphql-0.287.0/tests/relay/snapshots/000077500000000000000000000000001511033167500220325ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/relay/snapshots/schema.gql000066400000000000000000000235131511033167500240030ustar00rootroot00000000000000type CreateFruitPayload { fruit: Fruit! } type Fruit implements Node { """The Globally Unique ID of this object""" id: ID! name: String! color: String! } type FruitAlikeConnection { """Pagination data for this connection""" pageInfo: PageInfo! """Contains the nodes in this connection""" edges: [FruitEdge!]! } type FruitAsync implements Node { """The Globally Unique ID of this object""" id: ID! name: String! color: String! } """A connection to a list of items.""" type FruitAsyncConnection { """Pagination data for this connection""" pageInfo: PageInfo! """Contains the nodes in this connection""" edges: [FruitAsyncEdge!]! } """An edge in a connection.""" type FruitAsyncEdge { """A cursor for use in pagination""" cursor: String! """The item at the end of the edge""" node: FruitAsync! } """A connection to a list of items.""" type FruitConnection { """Pagination data for this connection""" pageInfo: PageInfo! """Contains the nodes in this connection""" edges: [FruitEdge!]! } type FruitCustomPaginationConnection { """Pagination data for this connection""" pageInfo: PageInfo! """Contains the nodes in this connection""" edges: [FruitEdge!]! something: String! } """An edge in a connection.""" type FruitEdge { """A cursor for use in pagination""" cursor: String! """The item at the end of the edge""" node: Fruit! } type Mutation { createFruit(name: String!, color: String!): CreateFruitPayload! } """An object with a Globally Unique ID""" interface Node { """The Globally Unique ID of this object""" id: ID! } """Information to aid in pagination.""" type PageInfo { """When paginating forwards, are there more items?""" hasNextPage: Boolean! """When paginating backwards, are there more items?""" hasPreviousPage: Boolean! """When paginating backwards, the cursor to continue.""" startCursor: String """When paginating forwards, the cursor to continue.""" endCursor: String } type Query { node( """The ID of the object.""" id: ID! ): Node! nodeWithAsyncPermissions( """The ID of the object.""" id: ID! ): Node! nodes( """The IDs of the objects.""" ids: [ID!]! ): [Node!]! nodeOptional( """The ID of the object.""" id: ID! ): Node nodesOptional( """The IDs of the objects.""" ids: [ID!]! ): [Node]! fruits( """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitConnection! fruitsLazy( """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitConnection! fruitsAlias( """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitConnection! fruitsAliasLazy( """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitConnection! fruitsAsync( """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitAsyncConnection! fruitsCustomPagination( """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitCustomPaginationConnection! fruitsConcreteResolver( nameEndswith: String = null """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitConnection! fruitsCustomResolver( nameEndswith: String = null """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitConnection! fruitsCustomResolverLazy( nameEndswith: String = null """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitConnection! fruitsCustomResolverIterator( nameEndswith: String = null """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitConnection! fruitsCustomResolverIterable( nameEndswith: String = null """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitConnection! fruitsCustomResolverGenerator( nameEndswith: String = null """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitConnection! fruitsCustomResolverAsyncIterable( nameEndswith: String = null """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitConnection! fruitsCustomResolverAsyncIterator( nameEndswith: String = null """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitConnection! fruitsCustomResolverAsyncGenerator( nameEndswith: String = null """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitConnection! fruitAlikeConnectionCustomResolver( nameEndswith: String = null """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitAlikeConnection! someFruits( """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitConnection! }strawberry-graphql-0.287.0/tests/relay/snapshots/schema_future_annotations.gql000066400000000000000000000217771511033167500300240ustar00rootroot00000000000000type CreateFruitPayload { fruit: Fruit! } type Fruit implements Node { """The Globally Unique ID of this object""" id: ID! name: String! color: String! } type FruitAlikeConnection { """Pagination data for this connection""" pageInfo: PageInfo! """Contains the nodes in this connection""" edges: [FruitEdge!]! } type FruitAsync implements Node { """The Globally Unique ID of this object""" id: ID! name: String! color: String! } """A connection to a list of items.""" type FruitAsyncConnection { """Pagination data for this connection""" pageInfo: PageInfo! """Contains the nodes in this connection""" edges: [FruitAsyncEdge!]! } """An edge in a connection.""" type FruitAsyncEdge { """A cursor for use in pagination""" cursor: String! """The item at the end of the edge""" node: FruitAsync! } """A connection to a list of items.""" type FruitConnection { """Pagination data for this connection""" pageInfo: PageInfo! """Contains the nodes in this connection""" edges: [FruitEdge!]! } type FruitCustomPaginationConnection { """Pagination data for this connection""" pageInfo: PageInfo! """Contains the nodes in this connection""" edges: [FruitEdge!]! something: String! } """An edge in a connection.""" type FruitEdge { """A cursor for use in pagination""" cursor: String! """The item at the end of the edge""" node: Fruit! } type Mutation { createFruit(name: String!, color: String!): CreateFruitPayload! } """An object with a Globally Unique ID""" interface Node { """The Globally Unique ID of this object""" id: ID! } """Information to aid in pagination.""" type PageInfo { """When paginating forwards, are there more items?""" hasNextPage: Boolean! """When paginating backwards, are there more items?""" hasPreviousPage: Boolean! """When paginating backwards, the cursor to continue.""" startCursor: String """When paginating forwards, the cursor to continue.""" endCursor: String } type Query { node( """The ID of the object.""" id: ID! ): Node! nodeWithAsyncPermissions( """The ID of the object.""" id: ID! ): Node! nodes( """The IDs of the objects.""" ids: [ID!]! ): [Node!]! nodeOptional( """The ID of the object.""" id: ID! ): Node nodesOptional( """The IDs of the objects.""" ids: [ID!]! ): [Node]! fruits( """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitConnection! fruitsLazy( """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitConnection! fruitsAsync( """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitAsyncConnection! fruitsCustomPagination( """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitCustomPaginationConnection! fruitsConcreteResolver( nameEndswith: String = null """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitConnection! fruitsCustomResolver( nameEndswith: String = null """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitConnection! fruitsCustomResolverLazy( nameEndswith: String = null """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitConnection! fruitsCustomResolverIterator( nameEndswith: String = null """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitConnection! fruitsCustomResolverIterable( nameEndswith: String = null """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitConnection! fruitsCustomResolverGenerator( nameEndswith: String = null """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitConnection! fruitsCustomResolverAsyncIterable( nameEndswith: String = null """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitConnection! fruitsCustomResolverAsyncIterator( nameEndswith: String = null """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitConnection! fruitsCustomResolverAsyncGenerator( nameEndswith: String = null """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitConnection! fruitAlikeConnectionCustomResolver( nameEndswith: String = null """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitAlikeConnection! someFruits( """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitConnection! }strawberry-graphql-0.287.0/tests/relay/test_connection.py000066400000000000000000000150301511033167500235570ustar00rootroot00000000000000from collections.abc import Iterable from typing import Annotated, Any, Optional from typing_extensions import Self import pytest import strawberry from strawberry.permission import BasePermission from strawberry.relay import Connection, Node, PageInfo, to_base64 from strawberry.relay.types import Edge, ListConnection from strawberry.schema.config import StrawberryConfig @strawberry.type class User(Node): id: strawberry.relay.NodeID name: str = "John" @classmethod def resolve_nodes( cls, *, info: strawberry.Info, node_ids: list[Any], required: bool ) -> list[Self]: return [cls() for _ in node_ids] @strawberry.type class EmptyUserConnection(Connection[User]): @classmethod def resolve_connection( cls, nodes: Iterable[User], *, info: Any, after: str | None = None, before: str | None = None, first: int | None = None, last: int | None = None, max_results: int | None = None, **kwargs: Any, ) -> Self | None: return None @strawberry.type class UserConnection(Connection[User]): @classmethod def resolve_connection( cls, nodes: Iterable[User], *, info: Any, after: str | None = None, before: str | None = None, first: int | None = None, last: int | None = None, max_results: int | None = None, **kwargs: Any, ) -> Self | None: user_node_id = to_base64(User, "1") return cls( page_info=PageInfo( has_next_page=False, has_previous_page=False, start_cursor=None, end_cursor=None, ), edges=[Edge(cursor=user_node_id, node=User(id=user_node_id))], ) class TestPermission(BasePermission): message = "Not allowed" def has_permission(self, source, info, **kwargs: Any): return False def test_nullable_connection_with_optional(): @strawberry.type class Query: @strawberry.relay.connection(Optional[EmptyUserConnection]) def users(self) -> list[User] | None: return None schema = strawberry.Schema(query=Query) query = """ query { users { edges { node { name } } } } """ result = schema.execute_sync(query) assert result.data == {"users": None} assert not result.errors def test_lazy_connection(): @strawberry.type class Query: @strawberry.relay.connection( Optional[ Annotated[ "UserConnection", strawberry.lazy("tests.relay.test_connection") ] ] ) def users(self) -> list[User] | None: return None schema = strawberry.Schema(query=Query) query = """ query { users { edges { node { name } } } } """ result = schema.execute_sync(query) assert result.data == {"users": {"edges": [{"node": {"name": "John"}}]}} assert not result.errors def test_lazy_optional_connection(): @strawberry.type class Query: @strawberry.relay.connection( Optional[ Annotated[ "EmptyUserConnection", strawberry.lazy("tests.relay.test_connection"), ] ] ) def users(self) -> list[User] | None: return None schema = strawberry.Schema(query=Query) query = """ query { users { edges { node { name } } } } """ result = schema.execute_sync(query) assert result.data == {"users": None} assert not result.errors def test_nullable_connection_with_pipe(): @strawberry.type class Query: @strawberry.relay.connection(EmptyUserConnection | None) def users(self) -> list[User] | None: return None schema = strawberry.Schema(query=Query) query = """ query { users { edges { node { name } } } } """ result = schema.execute_sync(query) assert result.data == {"users": None} assert not result.errors def test_nullable_connection_with_permission(): @strawberry.type class Query: @strawberry.relay.connection( Optional[EmptyUserConnection], permission_classes=[TestPermission] ) def users(self) -> list[User] | None: # pragma: no cover pytest.fail("Should not have been called...") schema = strawberry.Schema(query=Query) query = """ query { users { edges { node { name } } } } """ result = schema.execute_sync(query) assert result.data == {"users": None} assert result.errors[0].message == "Not allowed" @pytest.mark.parametrize( ("field_max_results", "schema_max_results", "results", "expected"), [ (5, 100, 5, 5), (5, 2, 5, 5), (5, 100, 10, 5), (5, 2, 10, 5), (5, 100, 0, 0), (5, 2, 0, 0), (None, 100, 5, 5), (None, 2, 5, 2), ], ) def test_max_results( field_max_results: int | None, schema_max_results: int, results: int, expected: int, ): @strawberry.type class User(Node): id: strawberry.relay.NodeID[str] @strawberry.type class Query: @strawberry.relay.connection( ListConnection[User], max_results=field_max_results, ) def users(self) -> list[User]: return [User(id=str(i)) for i in range(results)] schema = strawberry.Schema( query=Query, config=StrawberryConfig(relay_max_results=schema_max_results), ) query = """ query { users { edges { node { id } } } } """ result = schema.execute_sync(query) assert result.data is not None assert isinstance(result.data["users"]["edges"], list) assert len(result.data["users"]["edges"]) == expected strawberry-graphql-0.287.0/tests/relay/test_custom_edge.py000066400000000000000000000120671511033167500237250ustar00rootroot00000000000000import textwrap from typing import Any from typing_extensions import Self import pytest import strawberry from strawberry import Schema, relay from strawberry.relay import NodeType, to_base64 @pytest.fixture def schema() -> Schema: @strawberry.type class Fruit(relay.Node): code: relay.NodeID[int] @strawberry.type(name="Edge", description="An edge in a connection.") class CustomEdge(relay.Edge[NodeType]): CURSOR_PREFIX = "customprefix" index: int @classmethod def resolve_edge( cls, node: NodeType, *, cursor: Any = None, **kwargs: Any ) -> Self: assert isinstance(cursor, int) return super().resolve_edge(node, cursor=cursor, index=cursor, **kwargs) @strawberry.type(name="Connection", description="A connection to a list of items.") class CustomListConnection(relay.ListConnection[NodeType]): edges: list[CustomEdge[NodeType]] = strawberry.field( description="Contains the nodes in this connection" ) @strawberry.type class Query: @relay.connection(CustomListConnection[Fruit]) def fruits(self) -> list[Fruit]: return [Fruit(code=i) for i in range(10)] return strawberry.Schema(query=Query) def test_schema_with_custom_edge_class(schema: Schema): expected = textwrap.dedent( ''' type Fruit implements Node { """The Globally Unique ID of this object""" id: ID! } """A connection to a list of items.""" type FruitConnection { """Pagination data for this connection""" pageInfo: PageInfo! """Contains the nodes in this connection""" edges: [FruitEdge!]! } """An edge in a connection.""" type FruitEdge { """A cursor for use in pagination""" cursor: String! """The item at the end of the edge""" node: Fruit! index: Int! } """An object with a Globally Unique ID""" interface Node { """The Globally Unique ID of this object""" id: ID! } """Information to aid in pagination.""" type PageInfo { """When paginating forwards, are there more items?""" hasNextPage: Boolean! """When paginating backwards, are there more items?""" hasPreviousPage: Boolean! """When paginating backwards, the cursor to continue.""" startCursor: String """When paginating forwards, the cursor to continue.""" endCursor: String } type Query { fruits( """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitConnection! } ''' ).strip() assert str(schema) == expected def test_custom_cursor_prefix_used(schema: Schema): result = schema.execute_sync( """ query { fruits { edges { cursor node { id } } } } """ ) assert result.errors is None assert result.data == { "fruits": { "edges": [ { "cursor": to_base64("customprefix", i), "node": {"id": to_base64("Fruit", i)}, } for i in range(10) ] } } def test_custom_cursor_prefix_can_be_parsed(schema: Schema): result = schema.execute_sync( """ query TestQuery($after: String) { fruits(after: $after) { edges { cursor node { id } } } } """, {"after": to_base64("customprefix", 2)}, ) assert result.errors is None assert result.data == { "fruits": { "edges": [ { "cursor": to_base64("customprefix", i), "node": {"id": to_base64("Fruit", i)}, } for i in range(3, 10) ] } } def test_custom_edge_class_fields_can_be_resolved(schema: Schema): result = schema.execute_sync( """ query TestQuery($after: String) { fruits(after: $after) { edges { index node { id } } } } """, {"after": to_base64("customprefix", 2)}, ) assert result.errors is None assert result.data == { "fruits": { "edges": [ {"index": i, "node": {"id": to_base64("Fruit", i)}} for i in range(3, 10) ] } } strawberry-graphql-0.287.0/tests/relay/test_exceptions.py000066400000000000000000000100031511033167500235740ustar00rootroot00000000000000import pytest import strawberry from strawberry import Info, relay from strawberry.relay import GlobalID from strawberry.relay.exceptions import ( NodeIDAnnotationError, RelayWrongAnnotationError, RelayWrongResolverAnnotationError, ) @strawberry.type class NonNodeType: foo: str def test_raises_error_on_unknown_node_type_in_global_id(): @strawberry.type class Query: @strawberry.field() def test(self, info: Info) -> GlobalID: _id = GlobalID("foo", "bar") _id.resolve_type(info) return _id schema = strawberry.Schema(query=Query) result = schema.execute_sync(""" query TestQuery { test } """) assert len(result.errors) == 1 assert ( result.errors[0].message == "Cannot resolve. GlobalID requires a GraphQL type, received `foo`." ) def test_raises_error_on_non_node_type_in_global_id(): @strawberry.type class Query: @strawberry.field() def test(self, info: Info) -> GlobalID: _id = GlobalID("NonNodeType", "bar") _id.resolve_type(info) return _id schema = strawberry.Schema(query=Query, types=(NonNodeType,)) result = schema.execute_sync(""" query TestQuery { test } """) assert len(result.errors) == 1 assert ( result.errors[0].message == "Cannot resolve. GlobalID requires a GraphQL Node " "type, received `NonNodeType`." ) @pytest.mark.raises_strawberry_exception( NodeIDAnnotationError, match='No field annotated with `NodeID` found in "Fruit"', ) def test_raises_error_on_missing_node_id_annotation(): @strawberry.type class Fruit(relay.Node): code: str @strawberry.type class Query: @relay.connection(relay.ListConnection[Fruit]) def fruits(self) -> list[Fruit]: ... # pragma: no cover strawberry.Schema(query=Query) @pytest.mark.raises_strawberry_exception( NodeIDAnnotationError, match='More than one field annotated with `NodeID` found in "Fruit"', ) def test_raises_error_on_multiple_node_id_annotation(): @strawberry.type class Fruit(relay.Node): pk: relay.NodeID[str] code: relay.NodeID[str] @strawberry.type class Query: @relay.connection(relay.ListConnection[Fruit]) def fruits(self) -> list[Fruit]: ... # pragma: no cover strawberry.Schema(query=Query) @pytest.mark.raises_strawberry_exception( RelayWrongAnnotationError, match=( 'Wrong annotation used on field "fruits_conn". ' 'It should be annotated with a "Connection" subclass.' ), ) def test_raises_error_on_connection_missing_annotation(): @strawberry.type class Fruit(relay.Node): pk: relay.NodeID[str] @strawberry.type class Query: fruits_conn: list[Fruit] = relay.connection() strawberry.Schema(query=Query) @pytest.mark.raises_strawberry_exception( RelayWrongAnnotationError, match=( 'Wrong annotation used on field "custom_resolver". ' 'It should be annotated with a "Connection" subclass.' ), ) def test_raises_error_on_connection_wrong_annotation(): @strawberry.type class Fruit(relay.Node): pk: relay.NodeID[str] @strawberry.type class Query: @relay.connection(list[Fruit]) # type: ignore def custom_resolver(self) -> list[Fruit]: ... # pragma: no cover strawberry.Schema(query=Query) @pytest.mark.raises_strawberry_exception( RelayWrongResolverAnnotationError, match=( 'Wrong annotation used on "custom_resolver" resolver. ' "It should be return an iterable or async iterable object." ), ) def test_raises_error_on_connection_resolver_wrong_annotation(): @strawberry.type class Fruit(relay.Node): pk: relay.NodeID[str] @strawberry.type class Query: @relay.connection(relay.Connection[Fruit]) # type: ignore def custom_resolver(self): ... # pragma: no cover strawberry.Schema(query=Query) strawberry-graphql-0.287.0/tests/relay/test_fields.py000066400000000000000000001407561511033167500227040ustar00rootroot00000000000000import dataclasses import textwrap from collections.abc import Iterable from typing_extensions import Self import pytest from pytest_mock import MockerFixture import strawberry from strawberry import relay from strawberry.annotation import StrawberryAnnotation from strawberry.relay.fields import ConnectionExtension from strawberry.relay.utils import to_base64 from strawberry.types.arguments import StrawberryArgument from strawberry.types.field import StrawberryField from strawberry.types.fields.resolver import StrawberryResolver from .schema import FruitAsync, schema def test_query_node(): result = schema.execute_sync( """ query TestQuery ($id: ID!) { node (id: $id) { ... on Node { id } ... on Fruit { name color } } } """, variable_values={ "id": to_base64("Fruit", 2), }, ) assert result.errors is None assert result.data == { "node": { "id": to_base64("Fruit", 2), "color": "red", "name": "Apple", }, } async def test_query_node_with_async_permissions(): result = await schema.execute( """ query TestQuery ($id: ID!) { nodeWithAsyncPermissions (id: $id) { ... on Node { id } ... on Fruit { name color } } } """, variable_values={ "id": to_base64("Fruit", 2), }, ) assert result.errors is None assert result.data == { "nodeWithAsyncPermissions": { "id": to_base64("Fruit", 2), "color": "red", "name": "Apple", }, } def test_query_node_optional(): result = schema.execute_sync( """ query TestQuery ($id: ID!) { nodeOptional (id: $id) { ... on Node { id } ... on Fruit { name color } } } """, variable_values={ "id": to_base64("Fruit", 999), }, ) assert result.errors is None assert result.data == {"nodeOptional": None} async def test_query_node_async(): result = await schema.execute( """ query TestQuery ($id: ID!) { node (id: $id) { ... on Node { id } ... on Fruit { name color } } } """, variable_values={ "id": to_base64("Fruit", 2), }, ) assert result.errors is None assert result.data == { "node": { "id": to_base64("Fruit", 2), "color": "red", "name": "Apple", }, } async def test_query_node_optional_async(): result = await schema.execute( """ query TestQuery ($id: ID!) { nodeOptional (id: $id) { ... on Node { id } ... on Fruit { name color } } } """, variable_values={ "id": to_base64("Fruit", 999), }, ) assert result.errors is None assert result.data == {"nodeOptional": None} def test_query_nodes(): result = schema.execute_sync( """ query TestQuery ($ids: [ID!]!) { nodes (ids: $ids) { ... on Node { id } ... on Fruit { name color } } } """, variable_values={ "ids": [to_base64("Fruit", 2), to_base64("Fruit", 4)], }, ) assert result.errors is None assert result.data == { "nodes": [ { "id": to_base64("Fruit", 2), "name": "Apple", "color": "red", }, { "id": to_base64("Fruit", 4), "name": "Grape", "color": "purple", }, ], } def test_query_nodes_optional(): result = schema.execute_sync( """ query TestQuery ($ids: [ID!]!) { nodesOptional (ids: $ids) { ... on Node { id } ... on Fruit { name color } } } """, variable_values={ "ids": [ to_base64("Fruit", 2), to_base64("Fruit", 999), to_base64("Fruit", 4), ], }, ) assert result.errors is None assert result.data == { "nodesOptional": [ { "id": to_base64("Fruit", 2), "name": "Apple", "color": "red", }, None, { "id": to_base64("Fruit", 4), "name": "Grape", "color": "purple", }, ], } async def test_query_nodes_async(): result = await schema.execute( """ query TestQuery ($ids: [ID!]!) { nodes (ids: $ids) { ... on Node { id } ... on Fruit { name color } ... on FruitAsync { name color } } } """, variable_values={ "ids": [ to_base64("Fruit", 2), to_base64("Fruit", 4), to_base64("FruitAsync", 2), ], }, ) assert result.errors is None assert result.data == { "nodes": [ { "id": to_base64("Fruit", 2), "name": "Apple", "color": "red", }, { "id": to_base64("Fruit", 4), "name": "Grape", "color": "purple", }, { "id": to_base64("FruitAsync", 2), "name": "Apple", "color": "red", }, ], } async def test_query_nodes_optional_async(): result = await schema.execute( """ query TestQuery ($ids: [ID!]!) { nodesOptional (ids: $ids) { ... on Node { id } ... on Fruit { name color } ... on FruitAsync { name color } } } """, variable_values={ "ids": [ to_base64("Fruit", 2), to_base64("FruitAsync", 999), to_base64("Fruit", 4), to_base64("Fruit", 999), to_base64("FruitAsync", 2), ], }, ) assert result.errors is None assert result.data == { "nodesOptional": [ { "id": to_base64("Fruit", 2), "name": "Apple", "color": "red", }, None, { "id": to_base64("Fruit", 4), "name": "Grape", "color": "purple", }, None, { "id": to_base64("FruitAsync", 2), "name": "Apple", "color": "red", }, ], } fruits_query = """ query TestQuery ( $first: Int = null $last: Int = null $before: String = null, $after: String = null, ) {{ {} ( first: $first last: $last before: $before after: $after ) {{ pageInfo {{ hasNextPage hasPreviousPage startCursor endCursor }} edges {{ cursor node {{ id name color }} }} }} }} """ attrs = [ "fruits", "fruitsLazy", "fruitsAlias", "fruitsAliasLazy", "fruitsConcreteResolver", "fruitsCustomResolver", "fruitsCustomResolverLazy", "fruitsCustomResolverIterator", "fruitsCustomResolverIterable", "fruitsCustomResolverGenerator", "fruitAlikeConnectionCustomResolver", ] async_attrs = [ *attrs, "fruitsAsync", "fruitsCustomResolverAsyncIterator", "fruitsCustomResolverAsyncIterable", "fruitsCustomResolverAsyncGenerator", ] @pytest.mark.parametrize("query_attr", attrs) def test_query_connection(query_attr: str): result = schema.execute_sync( fruits_query.format(query_attr), variable_values={}, ) assert result.errors is None assert result.data == { query_attr: { "edges": [ { "cursor": "YXJyYXljb25uZWN0aW9uOjA=", "node": { "id": to_base64("Fruit", 1), "color": "yellow", "name": "Banana", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjE=", "node": { "id": to_base64("Fruit", 2), "color": "red", "name": "Apple", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjI=", "node": { "id": to_base64("Fruit", 3), "color": "yellow", "name": "Pineapple", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjM=", "node": { "id": to_base64("Fruit", 4), "color": "purple", "name": "Grape", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjQ=", "node": { "id": to_base64("Fruit", 5), "color": "orange", "name": "Orange", }, }, ], "pageInfo": { "hasNextPage": False, "hasPreviousPage": False, "startCursor": to_base64("arrayconnection", "0"), "endCursor": to_base64("arrayconnection", "4"), }, } } @pytest.mark.parametrize("query_attr", async_attrs) async def test_query_connection_async(mocker, query_attr: str): mocker.patch.object(FruitAsync, "resolve_typename", return_value="Fruit") result = await schema.execute( fruits_query.format(query_attr), variable_values={}, ) assert result.errors is None assert result.data == { query_attr: { "edges": [ { "cursor": "YXJyYXljb25uZWN0aW9uOjA=", "node": { "id": to_base64("Fruit", 1), "color": "yellow", "name": "Banana", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjE=", "node": { "id": to_base64("Fruit", 2), "color": "red", "name": "Apple", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjI=", "node": { "id": to_base64("Fruit", 3), "color": "yellow", "name": "Pineapple", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjM=", "node": { "id": to_base64("Fruit", 4), "color": "purple", "name": "Grape", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjQ=", "node": { "id": to_base64("Fruit", 5), "color": "orange", "name": "Orange", }, }, ], "pageInfo": { "hasNextPage": False, "hasPreviousPage": False, "startCursor": to_base64("arrayconnection", "0"), "endCursor": to_base64("arrayconnection", "4"), }, } } @pytest.mark.parametrize("query_attr", attrs) def test_query_connection_filtering_first(query_attr: str): result = schema.execute_sync( fruits_query.format(query_attr), variable_values={"first": 2}, ) assert result.errors is None assert result.data == { query_attr: { "edges": [ { "cursor": "YXJyYXljb25uZWN0aW9uOjA=", "node": { "id": to_base64("Fruit", 1), "color": "yellow", "name": "Banana", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjE=", "node": { "id": to_base64("Fruit", 2), "color": "red", "name": "Apple", }, }, ], "pageInfo": { "hasNextPage": True, "hasPreviousPage": False, "startCursor": to_base64("arrayconnection", "0"), "endCursor": to_base64("arrayconnection", "1"), }, } } @pytest.mark.parametrize("query_attr", async_attrs) async def test_query_connection_filtering_first_async(mocker, query_attr: str): mocker.patch.object(FruitAsync, "resolve_typename", return_value="Fruit") result = await schema.execute( fruits_query.format(query_attr), variable_values={"first": 2}, ) assert result.errors is None assert result.data == { query_attr: { "edges": [ { "cursor": "YXJyYXljb25uZWN0aW9uOjA=", "node": { "id": to_base64("Fruit", 1), "color": "yellow", "name": "Banana", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjE=", "node": { "id": to_base64("Fruit", 2), "color": "red", "name": "Apple", }, }, ], "pageInfo": { "hasNextPage": True, "hasPreviousPage": False, "startCursor": to_base64("arrayconnection", "0"), "endCursor": to_base64("arrayconnection", "1"), }, } } def test_query_connection_filtering_after_without_first(): result = schema.execute_sync( """{ someFruits { edges { node { id } } pageInfo { endCursor } } }""" ) assert not result.errors assert len(result.data["someFruits"]["edges"]) == 100 assert ( relay.from_base64(result.data["someFruits"]["edges"][99]["node"]["id"])[1] == "99" ) result = schema.execute_sync( """query ($after: String!){ someFruits(after: $after, first: 100) { edges { node { id } } } }""", variable_values={"after": result.data["someFruits"]["pageInfo"]["endCursor"]}, ) assert not result.errors assert len(result.data["someFruits"]["edges"]) == 100 assert ( relay.from_base64(result.data["someFruits"]["edges"][-1]["node"]["id"])[1] == "199" ) @pytest.mark.parametrize("query_attr", attrs) def test_query_connection_filtering_first_with_after(query_attr: str): result = schema.execute_sync( fruits_query.format(query_attr), variable_values={"first": 2, "after": to_base64("arrayconnection", "1")}, ) assert result.errors is None assert result.data == { query_attr: { "edges": [ { "cursor": "YXJyYXljb25uZWN0aW9uOjI=", "node": { "id": to_base64("Fruit", 3), "color": "yellow", "name": "Pineapple", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjM=", "node": { "id": to_base64("Fruit", 4), "color": "purple", "name": "Grape", }, }, ], "pageInfo": { "hasNextPage": True, "hasPreviousPage": True, "startCursor": to_base64("arrayconnection", "2"), "endCursor": to_base64("arrayconnection", "3"), }, } } @pytest.mark.parametrize("query_attr", async_attrs) async def test_query_connection_filtering_first_with_after_async( mocker, query_attr: str ): mocker.patch.object(FruitAsync, "resolve_typename", return_value="Fruit") result = await schema.execute( fruits_query.format(query_attr), variable_values={"first": 2, "after": to_base64("arrayconnection", "1")}, ) assert result.errors is None assert result.data == { query_attr: { "edges": [ { "cursor": "YXJyYXljb25uZWN0aW9uOjI=", "node": { "id": to_base64("Fruit", 3), "color": "yellow", "name": "Pineapple", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjM=", "node": { "id": to_base64("Fruit", 4), "color": "purple", "name": "Grape", }, }, ], "pageInfo": { "hasNextPage": True, "hasPreviousPage": True, "startCursor": to_base64("arrayconnection", "2"), "endCursor": to_base64("arrayconnection", "3"), }, } } @pytest.mark.parametrize("query_attr", attrs) def test_query_connection_filtering_last(query_attr: str): result = schema.execute_sync( fruits_query.format(query_attr), variable_values={"last": 2}, ) assert result.errors is None assert result.data == { query_attr: { "edges": [ { "cursor": "YXJyYXljb25uZWN0aW9uOjM=", "node": { "id": to_base64("Fruit", 4), "color": "purple", "name": "Grape", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjQ=", "node": { "id": to_base64("Fruit", 5), "color": "orange", "name": "Orange", }, }, ], "pageInfo": { "hasNextPage": False, "hasPreviousPage": True, "startCursor": to_base64("arrayconnection", "3"), "endCursor": to_base64("arrayconnection", "4"), }, } } @pytest.mark.parametrize("query_attr", async_attrs) async def test_query_connection_filtering_last_async(mocker, query_attr: str): mocker.patch.object(FruitAsync, "resolve_typename", return_value="Fruit") result = await schema.execute( fruits_query.format(query_attr), variable_values={"last": 2}, ) assert result.errors is None assert result.data == { query_attr: { "edges": [ { "cursor": "YXJyYXljb25uZWN0aW9uOjM=", "node": { "id": to_base64("Fruit", 4), "color": "purple", "name": "Grape", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjQ=", "node": { "id": to_base64("Fruit", 5), "color": "orange", "name": "Orange", }, }, ], "pageInfo": { "hasNextPage": False, "hasPreviousPage": True, "startCursor": to_base64("arrayconnection", "3"), "endCursor": to_base64("arrayconnection", "4"), }, } } @pytest.mark.parametrize("query_attr", attrs) def test_query_connection_filtering_first_with_before(query_attr: str): result = schema.execute_sync( fruits_query.format(query_attr), variable_values={"first": 1, "before": to_base64("arrayconnection", "3")}, ) assert result.errors is None assert result.data == { query_attr: { "edges": [ { "cursor": "YXJyYXljb25uZWN0aW9uOjI=", "node": { "id": to_base64("Fruit", 3), "color": "yellow", "name": "Pineapple", }, }, ], "pageInfo": { "hasNextPage": True, "hasPreviousPage": True, "startCursor": to_base64("arrayconnection", "2"), "endCursor": to_base64("arrayconnection", "2"), }, } } @pytest.mark.parametrize("query_attr", async_attrs) async def test_query_connection_filtering_first_with_before_async( mocker, query_attr: str ): mocker.patch.object(FruitAsync, "resolve_typename", return_value="Fruit") result = await schema.execute( fruits_query.format(query_attr), variable_values={"first": 1, "before": to_base64("arrayconnection", "3")}, ) assert result.errors is None assert result.data == { query_attr: { "edges": [ { "cursor": "YXJyYXljb25uZWN0aW9uOjI=", "node": { "id": to_base64("Fruit", 3), "color": "yellow", "name": "Pineapple", }, }, ], "pageInfo": { "hasNextPage": True, "hasPreviousPage": True, "startCursor": to_base64("arrayconnection", "2"), "endCursor": to_base64("arrayconnection", "2"), }, } } @pytest.mark.parametrize("query_attr", attrs) def test_query_connection_filtering_last_with_before(query_attr: str): result = schema.execute_sync( fruits_query.format(query_attr), variable_values={"last": 2, "before": to_base64("arrayconnection", "4")}, ) assert result.errors is None assert result.data == { query_attr: { "edges": [ { "cursor": "YXJyYXljb25uZWN0aW9uOjI=", "node": { "id": to_base64("Fruit", 3), "color": "yellow", "name": "Pineapple", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjM=", "node": { "id": to_base64("Fruit", 4), "color": "purple", "name": "Grape", }, }, ], "pageInfo": { "hasNextPage": True, "hasPreviousPage": True, "startCursor": to_base64("arrayconnection", "2"), "endCursor": to_base64("arrayconnection", "3"), }, } } @pytest.mark.parametrize("query_attr", async_attrs) async def test_query_connection_filtering_last_with_before_async( mocker, query_attr: str ): mocker.patch.object(FruitAsync, "resolve_typename", return_value="Fruit") result = await schema.execute( fruits_query.format(query_attr), variable_values={"last": 2, "before": to_base64("arrayconnection", "4")}, ) assert result.errors is None assert result.data == { query_attr: { "edges": [ { "cursor": "YXJyYXljb25uZWN0aW9uOjI=", "node": { "id": to_base64("Fruit", 3), "color": "yellow", "name": "Pineapple", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjM=", "node": { "id": to_base64("Fruit", 4), "color": "purple", "name": "Grape", }, }, ], "pageInfo": { "hasNextPage": True, "hasPreviousPage": True, "startCursor": to_base64("arrayconnection", "2"), "endCursor": to_base64("arrayconnection", "3"), }, } } fruits_custom_query = """ query TestQuery ( $first: Int = null $last: Int = null $before: String = null, $after: String = null, ) { fruitsCustomPagination ( first: $first last: $last before: $before after: $after ) { something pageInfo { hasNextPage hasPreviousPage startCursor endCursor } edges { cursor node { id name color } } } } """ def test_query_custom_connection(): result = schema.execute_sync( fruits_custom_query, variable_values={}, ) assert result.errors is None assert result.data == { "fruitsCustomPagination": { "something": "foobar", "edges": [ { "cursor": "ZnJ1aXRfbmFtZTpBcHBsZQ==", "node": { "id": to_base64("Fruit", 2), "color": "red", "name": "Apple", }, }, { "cursor": "ZnJ1aXRfbmFtZTpCYW5hbmE=", "node": { "id": to_base64("Fruit", 1), "color": "yellow", "name": "Banana", }, }, { "cursor": "ZnJ1aXRfbmFtZTpHcmFwZQ==", "node": { "id": to_base64("Fruit", 4), "color": "purple", "name": "Grape", }, }, { "cursor": "ZnJ1aXRfbmFtZTpPcmFuZ2U=", "node": { "id": to_base64("Fruit", 5), "color": "orange", "name": "Orange", }, }, { "cursor": "ZnJ1aXRfbmFtZTpQaW5lYXBwbGU=", "node": { "id": to_base64("Fruit", 3), "color": "yellow", "name": "Pineapple", }, }, ], "pageInfo": { "startCursor": to_base64("fruit_name", "Apple"), "endCursor": to_base64("fruit_name", "Pineapple"), "hasNextPage": False, "hasPreviousPage": False, }, } } def test_query_custom_connection_filtering_first(): result = schema.execute_sync( fruits_custom_query, variable_values={"first": 2}, ) assert result.errors is None assert result.data == { "fruitsCustomPagination": { "something": "foobar", "edges": [ { "cursor": "ZnJ1aXRfbmFtZTpBcHBsZQ==", "node": { "id": to_base64("Fruit", 2), "color": "red", "name": "Apple", }, }, { "cursor": "ZnJ1aXRfbmFtZTpCYW5hbmE=", "node": { "id": to_base64("Fruit", 1), "color": "yellow", "name": "Banana", }, }, ], "pageInfo": { "startCursor": to_base64("fruit_name", "Apple"), "endCursor": to_base64("fruit_name", "Banana"), "hasNextPage": True, "hasPreviousPage": False, }, } } def test_query_custom_connection_filtering_first_with_after(): result = schema.execute_sync( fruits_custom_query, variable_values={"first": 2, "after": to_base64("fruit_name", "Banana")}, ) assert result.errors is None assert result.data == { "fruitsCustomPagination": { "something": "foobar", "edges": [ { "cursor": "ZnJ1aXRfbmFtZTpHcmFwZQ==", "node": { "id": to_base64("Fruit", 4), "color": "purple", "name": "Grape", }, }, { "cursor": "ZnJ1aXRfbmFtZTpPcmFuZ2U=", "node": { "id": to_base64("Fruit", 5), "color": "orange", "name": "Orange", }, }, ], "pageInfo": { "hasNextPage": True, "hasPreviousPage": True, "startCursor": to_base64("fruit_name", "Grape"), "endCursor": to_base64("fruit_name", "Orange"), }, } } def test_query_custom_connection_filtering_last(): result = schema.execute_sync( fruits_custom_query, variable_values={"last": 2}, ) assert result.errors is None assert result.data == { "fruitsCustomPagination": { "something": "foobar", "edges": [ { "cursor": "ZnJ1aXRfbmFtZTpPcmFuZ2U=", "node": { "id": to_base64("Fruit", 5), "color": "orange", "name": "Orange", }, }, { "cursor": "ZnJ1aXRfbmFtZTpQaW5lYXBwbGU=", "node": { "id": to_base64("Fruit", 3), "color": "yellow", "name": "Pineapple", }, }, ], "pageInfo": { "hasNextPage": False, "hasPreviousPage": True, "startCursor": to_base64("fruit_name", "Orange"), "endCursor": to_base64("fruit_name", "Pineapple"), }, } } def test_query_custom_connection_filtering_last_with_before(): result = schema.execute_sync( fruits_custom_query, variable_values={ "last": 2, "before": to_base64("fruit_name", "Pineapple"), }, ) assert result.errors is None assert result.data == { "fruitsCustomPagination": { "something": "foobar", "edges": [ { "cursor": "ZnJ1aXRfbmFtZTpHcmFwZQ==", "node": { "id": to_base64("Fruit", 4), "color": "purple", "name": "Grape", }, }, { "cursor": "ZnJ1aXRfbmFtZTpPcmFuZ2U=", "node": { "id": to_base64("Fruit", 5), "color": "orange", "name": "Orange", }, }, ], "pageInfo": { "hasNextPage": True, "hasPreviousPage": True, "startCursor": to_base64("fruit_name", "Grape"), "endCursor": to_base64("fruit_name", "Orange"), }, } } fruits_query_custom_resolver = """ query TestQuery ( $first: Int = null $last: Int = null $before: String = null, $after: String = null, $nameEndswith: String = null ) {{ {} ( first: $first last: $last before: $before after: $after nameEndswith: $nameEndswith ) {{ pageInfo {{ hasNextPage hasPreviousPage startCursor endCursor }} edges {{ cursor node {{ id name color }} }} }} }} """ custom_attrs = [ "fruitsConcreteResolver", "fruitsCustomResolver", "fruitsCustomResolverIterator", "fruitsCustomResolverIterable", "fruitsCustomResolverGenerator", "fruitAlikeConnectionCustomResolver", ] custom_async_attrs = [ *attrs, "fruitsCustomResolverAsyncIterator", "fruitsCustomResolverAsyncIterable", "fruitsCustomResolverAsyncGenerator", ] @pytest.mark.parametrize("query_attr", custom_attrs) def test_query_connection_custom_resolver(query_attr: str): result = schema.execute_sync( fruits_query_custom_resolver.format(query_attr), variable_values={"nameEndswith": "e"}, ) assert result.errors is None assert result.data == { query_attr: { "edges": [ { "cursor": "YXJyYXljb25uZWN0aW9uOjA=", "node": { "id": to_base64("Fruit", 2), "color": "red", "name": "Apple", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjE=", "node": { "id": to_base64("Fruit", 3), "color": "yellow", "name": "Pineapple", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjI=", "node": { "id": to_base64("Fruit", 4), "color": "purple", "name": "Grape", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjM=", "node": { "id": to_base64("Fruit", 5), "color": "orange", "name": "Orange", }, }, ], "pageInfo": { "hasNextPage": False, "hasPreviousPage": False, "startCursor": to_base64("arrayconnection", "0"), "endCursor": to_base64("arrayconnection", "3"), }, } } @pytest.mark.parametrize("query_attr", custom_attrs) def test_query_connection_custom_resolver_filtering_first(query_attr: str): result = schema.execute_sync( fruits_query_custom_resolver.format(query_attr), variable_values={"first": 2, "nameEndswith": "e"}, ) assert result.errors is None assert result.data == { query_attr: { "edges": [ { "cursor": "YXJyYXljb25uZWN0aW9uOjA=", "node": { "id": to_base64("Fruit", 2), "color": "red", "name": "Apple", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjE=", "node": { "id": to_base64("Fruit", 3), "color": "yellow", "name": "Pineapple", }, }, ], "pageInfo": { "hasNextPage": True, "hasPreviousPage": False, "startCursor": to_base64("arrayconnection", "0"), "endCursor": to_base64("arrayconnection", "1"), }, } } @pytest.mark.parametrize("query_attr", custom_attrs) def test_query_connection_custom_resolver_filtering_first_with_after(query_attr: str): result = schema.execute_sync( fruits_query_custom_resolver.format(query_attr), variable_values={ "first": 2, "after": to_base64("arrayconnection", "1"), "nameEndswith": "e", }, ) assert result.errors is None assert result.data == { query_attr: { "edges": [ { "cursor": "YXJyYXljb25uZWN0aW9uOjI=", "node": { "id": to_base64("Fruit", 4), "color": "purple", "name": "Grape", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjM=", "node": { "id": to_base64("Fruit", 5), "color": "orange", "name": "Orange", }, }, ], "pageInfo": { "hasNextPage": False, "hasPreviousPage": True, "startCursor": to_base64("arrayconnection", "2"), "endCursor": to_base64("arrayconnection", "3"), }, } } @pytest.mark.parametrize("query_attr", custom_attrs) def test_query_connection_custom_resolver_filtering_last(query_attr: str): result = schema.execute_sync( fruits_query_custom_resolver.format(query_attr), variable_values={"last": 2, "nameEndswith": "e"}, ) assert result.errors is None assert result.data == { query_attr: { "edges": [ { "cursor": "YXJyYXljb25uZWN0aW9uOjI=", "node": { "id": to_base64("Fruit", 4), "color": "purple", "name": "Grape", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjM=", "node": { "id": to_base64("Fruit", 5), "color": "orange", "name": "Orange", }, }, ], "pageInfo": { "hasNextPage": False, "hasPreviousPage": True, "startCursor": to_base64("arrayconnection", "2"), "endCursor": to_base64("arrayconnection", "3"), }, } } @pytest.mark.parametrize("query_attr", custom_attrs) def test_query_connection_custom_resolver_filtering_last_with_before(query_attr: str): result = schema.execute_sync( fruits_query_custom_resolver.format(query_attr), variable_values={ "last": 2, "before": to_base64("arrayconnection", "3"), "nameEndswith": "e", }, ) assert result.errors is None assert result.data == { query_attr: { "edges": [ { "cursor": "YXJyYXljb25uZWN0aW9uOjE=", "node": { "id": to_base64("Fruit", 3), "color": "yellow", "name": "Pineapple", }, }, { "cursor": "YXJyYXljb25uZWN0aW9uOjI=", "node": { "id": to_base64("Fruit", 4), "color": "purple", "name": "Grape", }, }, ], "pageInfo": { "hasNextPage": True, "hasPreviousPage": True, "startCursor": to_base64("arrayconnection", "1"), "endCursor": to_base64("arrayconnection", "2"), }, } } @pytest.mark.parametrize("query_attr", custom_attrs) def test_query_first_negative(query_attr: str): result = schema.execute_sync( fruits_query_custom_resolver.format(query_attr), variable_values={"first": -1}, ) assert result.errors is not None assert ( result.errors[0].message == "Argument 'first' must be a non-negative integer." ) @pytest.mark.parametrize("query_attr", custom_attrs) def test_query_first_higher_than_max_results(query_attr: str): result = schema.execute_sync( fruits_query_custom_resolver.format(query_attr), variable_values={"first": 500}, ) assert result.errors is not None assert result.errors[0].message == "Argument 'first' cannot be higher than 100." @pytest.mark.parametrize("query_attr", custom_attrs) def test_query_last_negative(query_attr: str): result = schema.execute_sync( fruits_query_custom_resolver.format(query_attr), variable_values={"last": -1}, ) assert result.errors is not None assert result.errors[0].message == "Argument 'last' must be a non-negative integer." @pytest.mark.parametrize("query_attr", custom_attrs) def test_query_last_higher_than_max_results(query_attr: str): result = schema.execute_sync( fruits_query_custom_resolver.format(query_attr), variable_values={"last": 500}, ) assert result.errors is not None assert result.errors[0].message == "Argument 'last' cannot be higher than 100." def test_parameters(mocker: MockerFixture): class CustomField(StrawberryField): @property def arguments(self) -> list[StrawberryArgument]: return [ *super().arguments, StrawberryArgument( python_name="foo", graphql_name=None, type_annotation=StrawberryAnnotation(str), default=None, ), ] @arguments.setter def arguments(self, value: list[StrawberryArgument]): cls = self.__class__ return super(cls, cls).arguments.fset(self, value) @strawberry.type class Fruit(relay.Node): code: relay.NodeID[str] def resolver(info: strawberry.Info) -> list[Fruit]: ... @strawberry.type class Query: fruit: relay.ListConnection[Fruit] = relay.connection( resolver=resolver, extensions=[ConnectionExtension()], ) fruit_custom_field: relay.ListConnection[Fruit] = CustomField( base_resolver=StrawberryResolver(resolver), extensions=[ConnectionExtension()], ) schema = strawberry.Schema(query=Query) expected = ''' type Fruit implements Node { """The Globally Unique ID of this object""" id: ID! } """A connection to a list of items.""" type FruitConnection { """Pagination data for this connection""" pageInfo: PageInfo! """Contains the nodes in this connection""" edges: [FruitEdge!]! } """An edge in a connection.""" type FruitEdge { """A cursor for use in pagination""" cursor: String! """The item at the end of the edge""" node: Fruit! } """An object with a Globally Unique ID""" interface Node { """The Globally Unique ID of this object""" id: ID! } """Information to aid in pagination.""" type PageInfo { """When paginating forwards, are there more items?""" hasNextPage: Boolean! """When paginating backwards, are there more items?""" hasPreviousPage: Boolean! """When paginating backwards, the cursor to continue.""" startCursor: String """When paginating forwards, the cursor to continue.""" endCursor: String } type Query { fruit( """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitConnection! fruitCustomField( foo: String! """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitConnection! } ''' assert str(schema) == textwrap.dedent(expected).strip() before_after_test_query = """ query fruitsBeforeAfterTest ( $before: String = null, $after: String = null, ) { fruits ( before: $before after: $after ) { edges { cursor node { id } } } } """ async def test_query_before_error(): """Verify if the error raised on a non-existing before hash raises the correct error. """ # with pytest.raises(ValueError): index = to_base64("Fake", 9292292) result = await schema.execute( before_after_test_query, variable_values={"before": index}, ) assert result.errors is not None assert "Argument 'before' contains a non-existing value" in str(result.errors) def test_query_after_error(): """Verify if the error raised on a non-existing before hash raises the correct error. """ index = to_base64("Fake", 9292292) result = schema.execute_sync( before_after_test_query, variable_values={"after": index}, ) assert result.errors is not None assert "Argument 'after' contains a non-existing value" in str(result.errors) @pytest.mark.parametrize( ("type_name", "should_have_name"), [("Fruit", False), ("PublicFruit", True)], ) @pytest.mark.django_db(transaction=True) def test_correct_model_returned(type_name: str, should_have_name: bool): @dataclasses.dataclass class FruitModel: id: str name: str fruits: dict[str, FruitModel] = {"1": FruitModel(id="1", name="Strawberry")} @strawberry.type class Fruit(relay.Node): id: relay.NodeID[int] @classmethod def resolve_nodes( cls, *, info: strawberry.Info | None = None, node_ids: Iterable[str], required: bool = False, ) -> Iterable[Self | FruitModel | None]: return [fruits[nid] if required else fruits.get(nid) for nid in node_ids] @strawberry.type class PublicFruit(relay.Node): id: relay.NodeID[int] name: str @classmethod def resolve_nodes( cls, *, info: strawberry.Info | None = None, node_ids: Iterable[str], required: bool = False, ) -> Iterable[Self | FruitModel | None]: return [fruits[nid] if required else fruits.get(nid) for nid in node_ids] @strawberry.type class Query: node: relay.Node = relay.node() schema = strawberry.Schema(query=Query, types=[Fruit, PublicFruit]) node_id = relay.to_base64(type_name, "1") result = schema.execute_sync( """ query NodeQuery($id: ID!) { node(id: $id) { __typename id ... on PublicFruit { name } } } """, {"id": node_id}, ) assert result.errors is None assert isinstance(result.data, dict) assert result.data["node"]["__typename"] == type_name assert result.data["node"]["id"] == node_id if should_have_name: assert result.data["node"]["name"] == "Strawberry" else: assert "name" not in result.data["node"] strawberry-graphql-0.287.0/tests/relay/test_id.py000066400000000000000000000143071511033167500220220ustar00rootroot00000000000000from __future__ import annotations from inline_snapshot import snapshot import strawberry from strawberry.relay import GlobalID from strawberry.schema.config import StrawberryConfig @strawberry.type class Book: id: GlobalID title: str debug: str @strawberry.type class Query: @strawberry.field def books(self) -> list[Book]: return [] @strawberry.field def book(self, id: GlobalID) -> Book | None: return Book(id=id, title="Title", debug=str(type(id))) def test_uses_id_by_default() -> None: schema = strawberry.Schema(query=Query) assert str(schema) == snapshot("""\ type Book { id: ID! title: String! debug: String! } type Query { books: [Book!]! book(id: ID!): Book }\ """) result = schema.execute_sync('query { book(id: "Qm9vazox") { id, debug } }') assert result.errors is None assert result.data == snapshot( { "book": { "id": "Qm9vazox", "debug": "", } } ) def test_we_can_still_use_global_id() -> None: schema = strawberry.Schema( query=Query, config=StrawberryConfig(relay_use_legacy_global_id=True) ) assert str(schema) == snapshot('''\ type Book { id: GlobalID! title: String! debug: String! } """ The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted as an ID. """ scalar GlobalID @specifiedBy(url: "https://relay.dev/graphql/objectidentification.htm") type Query { books: [Book!]! book(id: GlobalID!): Book }\ ''') result = schema.execute_sync('query { book(id: "Qm9vazox") { id, debug } }') assert result.errors is None assert result.data == snapshot( { "book": { "id": "Qm9vazox", "debug": "", } } ) def test_can_use_both_global_id_and_id() -> None: @strawberry.type class Query: @strawberry.field def hello(self, id: GlobalID) -> str: return id.node_id @strawberry.field def hello2(self, id: strawberry.ID) -> str: return id schema = strawberry.Schema(Query) assert str(schema) == snapshot("""\ type Query { hello(id: ID!): String! hello2(id: ID!): String! }\ """) result = schema.execute_sync( """ query ($globalId: ID!, $id: ID!) { a: hello(id: "Qm9vazox") b: hello2(id: "1") c: hello(id: $globalId) d: hello2(id: $id) } """, variable_values={"globalId": "Qm9vazox", "id": "1"}, ) assert result.errors is None assert result.data == snapshot( { "a": "1", "b": "1", "c": "1", "d": "1", } ) def test_can_use_both_global_id_and_id_legacy() -> None: @strawberry.type class Query: @strawberry.field def hello(self, id: GlobalID) -> str: return id.node_id @strawberry.field def hello2(self, id: strawberry.ID) -> str: return id schema = strawberry.Schema( query=Query, config=StrawberryConfig(relay_use_legacy_global_id=True) ) assert str(schema) == snapshot('''\ """ The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted as an ID. """ scalar GlobalID @specifiedBy(url: "https://relay.dev/graphql/objectidentification.htm") type Query { hello(id: GlobalID!): String! hello2(id: ID!): String! }\ ''') result = schema.execute_sync( """ query ($globalId: GlobalID!, $id: ID!) { a: hello(id: $globalId) b: hello2(id: $id) c: hello(id: "Qm9vazox") d: hello2(id: "1") } """, variable_values={"globalId": "Qm9vazox", "id": "1"}, ) assert result.errors is None assert result.data == snapshot( { "a": "1", "b": "1", "c": "1", "d": "1", } ) def test_can_return_id() -> None: @strawberry.type class Query: @strawberry.field def hello(self, id: GlobalID) -> GlobalID: return id @strawberry.field def hello2(self, id: strawberry.ID) -> strawberry.ID: return id schema = strawberry.Schema(Query) result = schema.execute_sync( """ query ($globalId: ID!, $id: ID!) { a: hello(id: "Qm9vazox") b: hello2(id: "1") c: hello(id: $globalId) d: hello2(id: $id) } """, variable_values={"globalId": "Qm9vazox", "id": "1"}, ) assert result.errors is None assert result.data == snapshot( { "a": "Qm9vazox", "b": "1", "c": "Qm9vazox", "d": "1", } ) def test_can_return_global_id_legacy() -> None: @strawberry.type class Query: @strawberry.field def hello(self, id: GlobalID) -> GlobalID: return id @strawberry.field def hello2(self, id: strawberry.ID) -> strawberry.ID: return id schema = strawberry.Schema( query=Query, config=StrawberryConfig(relay_use_legacy_global_id=True) ) result = schema.execute_sync( """ query ($globalId: GlobalID!, $id: ID!) { a: hello(id: $globalId) b: hello2(id: $id) c: hello(id: "Qm9vazox") d: hello2(id: "1") } """, variable_values={"globalId": "Qm9vazox", "id": "1"}, ) assert result.errors is None assert result.data == snapshot( { "a": "Qm9vazox", "b": "1", "c": "Qm9vazox", "d": "1", } ) strawberry-graphql-0.287.0/tests/relay/test_schema.py000066400000000000000000000224471511033167500226720ustar00rootroot00000000000000import pathlib import textwrap from pytest_snapshot.plugin import Snapshot import strawberry from strawberry import relay from strawberry.relay.utils import to_base64 from .schema import schema from .schema_future_annotations import schema as schema_future_annotations SNAPSHOTS_DIR = pathlib.Path(__file__).parent / "snapshots" def test_schema(snapshot: Snapshot): snapshot.snapshot_dir = SNAPSHOTS_DIR snapshot.assert_match(str(schema), "schema.gql") def test_schema_future_annotations(snapshot: Snapshot): snapshot.snapshot_dir = SNAPSHOTS_DIR snapshot.assert_match( str(schema_future_annotations), "schema_future_annotations.gql", ) def test_node_id_annotation(): @strawberry.type class Fruit(relay.Node): code: relay.NodeID[int] @strawberry.type class Query: @relay.connection(relay.ListConnection[Fruit]) def fruits(self) -> list[Fruit]: return [Fruit(code=i) for i in range(10)] schema = strawberry.Schema(query=Query) expected = textwrap.dedent( ''' type Fruit implements Node { """The Globally Unique ID of this object""" id: ID! } """A connection to a list of items.""" type FruitConnection { """Pagination data for this connection""" pageInfo: PageInfo! """Contains the nodes in this connection""" edges: [FruitEdge!]! } """An edge in a connection.""" type FruitEdge { """A cursor for use in pagination""" cursor: String! """The item at the end of the edge""" node: Fruit! } """An object with a Globally Unique ID""" interface Node { """The Globally Unique ID of this object""" id: ID! } """Information to aid in pagination.""" type PageInfo { """When paginating forwards, are there more items?""" hasNextPage: Boolean! """When paginating backwards, are there more items?""" hasPreviousPage: Boolean! """When paginating backwards, the cursor to continue.""" startCursor: String """When paginating forwards, the cursor to continue.""" endCursor: String } type Query { fruits( """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitConnection! } ''' ).strip() assert str(schema) == expected result = schema.execute_sync( """ query { fruits { edges { node { id } } } } """ ) assert result.errors is None assert result.data == { "fruits": { "edges": [{"node": {"id": to_base64("Fruit", i)}} for i in range(10)] } } def test_node_id_annotation_in_superclass(): @strawberry.type class BaseFruit(relay.Node): code: relay.NodeID[int] @strawberry.type class Fruit(BaseFruit): ... @strawberry.type class Query: @relay.connection(relay.ListConnection[Fruit]) def fruits(self) -> list[Fruit]: return [Fruit(code=i) for i in range(10)] schema = strawberry.Schema(query=Query) expected = textwrap.dedent( ''' type Fruit implements Node { """The Globally Unique ID of this object""" id: ID! } """A connection to a list of items.""" type FruitConnection { """Pagination data for this connection""" pageInfo: PageInfo! """Contains the nodes in this connection""" edges: [FruitEdge!]! } """An edge in a connection.""" type FruitEdge { """A cursor for use in pagination""" cursor: String! """The item at the end of the edge""" node: Fruit! } """An object with a Globally Unique ID""" interface Node { """The Globally Unique ID of this object""" id: ID! } """Information to aid in pagination.""" type PageInfo { """When paginating forwards, are there more items?""" hasNextPage: Boolean! """When paginating backwards, are there more items?""" hasPreviousPage: Boolean! """When paginating backwards, the cursor to continue.""" startCursor: String """When paginating forwards, the cursor to continue.""" endCursor: String } type Query { fruits( """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitConnection! } ''' ).strip() assert str(schema) == expected result = schema.execute_sync( """ query { fruits { edges { node { id } } } } """ ) assert result.errors is None assert result.data == { "fruits": { "edges": [{"node": {"id": to_base64("Fruit", i)}} for i in range(10)] } } def test_node_id_annotation_in_superclass_and_subclass(): @strawberry.type class BaseFruit(relay.Node): code: relay.NodeID[int] @strawberry.type class Fruit(BaseFruit): other_code: relay.NodeID[int] @strawberry.type class Query: @relay.connection(relay.ListConnection[Fruit]) def fruits(self) -> list[Fruit]: return [Fruit(code=i, other_code=i) for i in range(10)] schema = strawberry.Schema(query=Query) expected = textwrap.dedent( ''' type Fruit implements Node { """The Globally Unique ID of this object""" id: ID! } """A connection to a list of items.""" type FruitConnection { """Pagination data for this connection""" pageInfo: PageInfo! """Contains the nodes in this connection""" edges: [FruitEdge!]! } """An edge in a connection.""" type FruitEdge { """A cursor for use in pagination""" cursor: String! """The item at the end of the edge""" node: Fruit! } """An object with a Globally Unique ID""" interface Node { """The Globally Unique ID of this object""" id: ID! } """Information to aid in pagination.""" type PageInfo { """When paginating forwards, are there more items?""" hasNextPage: Boolean! """When paginating backwards, are there more items?""" hasPreviousPage: Boolean! """When paginating backwards, the cursor to continue.""" startCursor: String """When paginating forwards, the cursor to continue.""" endCursor: String } type Query { fruits( """Returns the items in the list that come before the specified cursor.""" before: String = null """Returns the items in the list that come after the specified cursor.""" after: String = null """Returns the first n items from the list.""" first: Int = null """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitConnection! } ''' ).strip() assert str(schema) == expected result = schema.execute_sync( """ query { fruits { edges { node { id } } } } """ ) assert result.errors is None assert result.data == { "fruits": { "edges": [{"node": {"id": to_base64("Fruit", i)}} for i in range(10)] } } def test_overwrite_resolve_id_and_no_node_id(): @strawberry.type class Fruit(relay.Node): color: str @classmethod def resolve_id(cls, root) -> str: return "test" # pragma: no cover @strawberry.type class Query: fruit: Fruit expected_type = textwrap.dedent( ''' type Fruit implements Node { """The Globally Unique ID of this object""" id: ID! color: String! } """An object with a Globally Unique ID""" interface Node { """The Globally Unique ID of this object""" id: ID! } type Query { fruit: Fruit! } ''' ) schema = strawberry.Schema(query=Query) assert str(schema) == textwrap.dedent(expected_type).strip() strawberry-graphql-0.287.0/tests/relay/test_types.py000066400000000000000000000217601511033167500225730ustar00rootroot00000000000000from collections.abc import AsyncGenerator, AsyncIterable from typing import Any, Optional, Union, cast from typing_extensions import assert_type from unittest.mock import MagicMock import pytest import strawberry from strawberry import relay from strawberry.relay.utils import to_base64 from strawberry.types.info import Info from .schema import Fruit, FruitAsync, fruits_resolver, schema class FakeInfo: schema = schema # We only need that info contains the schema for the tests fake_info = cast("Info", FakeInfo()) @pytest.mark.parametrize("type_name", [None, 1, 1.1]) def test_global_id_wrong_type_name(type_name: Any): with pytest.raises(relay.GlobalIDValueError): relay.GlobalID(type_name=type_name, node_id="foobar") @pytest.mark.parametrize("node_id", [None, 1, 1.1]) def test_global_id_wrong_type_node_id(node_id: Any): with pytest.raises(relay.GlobalIDValueError): relay.GlobalID(type_name="foobar", node_id=node_id) def test_global_id_from_id(): gid = relay.GlobalID.from_id("Zm9vYmFyOjE=") assert gid.type_name == "foobar" assert gid.node_id == "1" @pytest.mark.parametrize("value", ["foobar", ["Zm9vYmFy"], 123]) def test_global_id_from_id_error(value: Any): with pytest.raises(relay.GlobalIDValueError): relay.GlobalID.from_id(value) def test_global_id_resolve_type(): gid = relay.GlobalID(type_name="Fruit", node_id="1") type_ = gid.resolve_type(fake_info) assert type_ is Fruit def test_global_id_resolve_node_sync(): gid = relay.GlobalID(type_name="Fruit", node_id="1") fruit = gid.resolve_node_sync(fake_info) assert isinstance(fruit, Fruit) assert fruit.id == 1 assert fruit.name == "Banana" def test_global_id_resolve_node_sync_non_existing(): gid = relay.GlobalID(type_name="Fruit", node_id="999") fruit = gid.resolve_node_sync(fake_info) assert_type(fruit, Optional[relay.Node]) assert fruit is None def test_global_id_resolve_node_sync_non_existing_but_required(): gid = relay.GlobalID(type_name="Fruit", node_id="999") with pytest.raises(KeyError): gid.resolve_node_sync(fake_info, required=True) def test_global_id_resolve_node_sync_ensure_type(): gid = relay.GlobalID(type_name="Fruit", node_id="1") fruit = gid.resolve_node_sync(fake_info, ensure_type=Fruit) assert_type(fruit, Fruit) assert isinstance(fruit, Fruit) assert fruit.id == 1 assert fruit.name == "Banana" def test_global_id_resolve_node_sync_ensure_type_with_union(): class Foo: ... gid = relay.GlobalID(type_name="Fruit", node_id="1") fruit = gid.resolve_node_sync(fake_info, ensure_type=Union[Fruit, Foo]) assert_type(fruit, Union[Fruit, Foo]) assert isinstance(fruit, Fruit) assert fruit.id == 1 assert fruit.name == "Banana" def test_global_id_resolve_node_sync_ensure_type_wrong_type(): class Foo: ... gid = relay.GlobalID(type_name="Fruit", node_id="1") with pytest.raises(TypeError): gid.resolve_node_sync(fake_info, ensure_type=Foo) async def test_global_id_resolve_node(): gid = relay.GlobalID(type_name="FruitAsync", node_id="1") fruit = await gid.resolve_node(fake_info) assert_type(fruit, Optional[relay.Node]) assert isinstance(fruit, FruitAsync) assert fruit.id == 1 assert fruit.name == "Banana" async def test_global_id_resolve_node_non_existing(): gid = relay.GlobalID(type_name="FruitAsync", node_id="999") fruit = await gid.resolve_node(fake_info) assert_type(fruit, Optional[relay.Node]) assert fruit is None async def test_global_id_resolve_node_non_existing_but_required(): gid = relay.GlobalID(type_name="FruitAsync", node_id="999") with pytest.raises(KeyError): await gid.resolve_node(fake_info, required=True) async def test_global_id_resolve_node_ensure_type(): gid = relay.GlobalID(type_name="FruitAsync", node_id="1") fruit = await gid.resolve_node(fake_info, ensure_type=FruitAsync) assert_type(fruit, FruitAsync) assert isinstance(fruit, FruitAsync) assert fruit.id == 1 assert fruit.name == "Banana" async def test_global_id_resolve_node_ensure_type_with_union(): class Foo: ... gid = relay.GlobalID(type_name="FruitAsync", node_id="1") fruit = await gid.resolve_node(fake_info, ensure_type=Union[FruitAsync, Foo]) assert_type(fruit, Union[FruitAsync, Foo]) assert isinstance(fruit, FruitAsync) assert fruit.id == 1 assert fruit.name == "Banana" async def test_global_id_resolve_node_ensure_type_wrong_type(): class Foo: ... gid = relay.GlobalID(type_name="FruitAsync", node_id="1") with pytest.raises(TypeError): await gid.resolve_node(fake_info, ensure_type=Foo) async def test_resolve_async_list_connection(): @strawberry.type class SomeType(relay.Node): id: relay.NodeID[int] @strawberry.type class Query: @relay.connection(relay.ListConnection[SomeType]) async def some_type_conn(self) -> AsyncGenerator[SomeType, None]: yield SomeType(id=0) yield SomeType(id=1) yield SomeType(id=2) schema = strawberry.Schema(query=Query) ret = await schema.execute( """\ query { someTypeConn { edges { node { id } } } } """ ) assert ret.errors is None assert ret.data == { "someTypeConn": { "edges": [ {"node": {"id": to_base64("SomeType", 0)}}, {"node": {"id": to_base64("SomeType", 1)}}, {"node": {"id": to_base64("SomeType", 2)}}, ], } } async def test_resolve_async_list_connection_but_sync_after_sliced(): # We are mimicking an object which is async iterable, but when sliced # returns something that is not anymore. This is similar to an already # prefetched django QuerySet, which is async iterable by default, but # when sliced, since it is already prefetched, will return a list. class Slicer: def __init__(self, nodes) -> None: self.nodes = nodes async def __aiter__(self): for n in self.nodes: yield n def __getitem__(self, key): return self.nodes[key] @strawberry.type class SomeType(relay.Node): id: relay.NodeID[int] @strawberry.type class Query: @relay.connection(relay.ListConnection[SomeType]) async def some_type_conn(self) -> AsyncIterable[SomeType]: return Slicer([SomeType(id=0), SomeType(id=1), SomeType(id=2)]) schema = strawberry.Schema(query=Query) ret = await schema.execute( """\ query { someTypeConn { edges { node { id } } } } """ ) assert ret.errors is None assert ret.data == { "someTypeConn": { "edges": [ {"node": {"id": to_base64("SomeType", 0)}}, {"node": {"id": to_base64("SomeType", 1)}}, {"node": {"id": to_base64("SomeType", 2)}}, ], } } def test_overwrite_resolve_id_and_no_node_id(): @strawberry.type class Fruit(relay.Node): color: str @classmethod def resolve_id(cls, root) -> str: return "test" # pragma: no cover @strawberry.type class Query: @strawberry.field def fruit(self) -> Fruit: return Fruit(color="red") # pragma: no cover strawberry.Schema(query=Query) def test_list_connection_without_edges_or_page_info(mocker: MagicMock): @strawberry.type(name="Connection", description="A connection to a list of items.") class DummyListConnectionWithTotalCount(relay.ListConnection[relay.NodeType]): @strawberry.field(description="Total quantity of existing nodes.") def total_count(self) -> int: return -1 @strawberry.type class Query: fruits: DummyListConnectionWithTotalCount[Fruit] = relay.connection( resolver=fruits_resolver ) mock = mocker.patch("strawberry.relay.types.Edge.resolve_edge") schema = strawberry.Schema(query=Query) ret = schema.execute_sync( """ query { fruits { totalCount } } """ ) mock.assert_not_called() assert ret.errors is None assert ret.data == { "fruits": { "totalCount": -1, } } def test_list_connection_with_nested_fragments(): ret = schema.execute_sync( """ query { fruits { ...FruitFragment } } fragment FruitFragment on FruitConnection { edges { node { id } } } """ ) assert ret.errors is None assert ret.data == { "fruits": { "edges": [ {"node": {"id": "RnJ1aXQ6MQ=="}}, {"node": {"id": "RnJ1aXQ6Mg=="}}, {"node": {"id": "RnJ1aXQ6Mw=="}}, {"node": {"id": "RnJ1aXQ6NA=="}}, {"node": {"id": "RnJ1aXQ6NQ=="}}, ] } } strawberry-graphql-0.287.0/tests/relay/test_utils.py000066400000000000000000000077341511033167500225740ustar00rootroot00000000000000from __future__ import annotations import sys from typing import Any from unittest import mock import pytest from strawberry.relay.types import PREFIX from strawberry.relay.utils import ( SliceMetadata, from_base64, to_base64, ) from strawberry.schema.config import StrawberryConfig from strawberry.types.base import get_object_definition from .schema import Fruit def test_from_base64(): type_name, node_id = from_base64("Zm9vYmFyOjE=") # foobar:1 assert type_name == "foobar" assert node_id == "1" def test_from_base64_with_extra_colon(): type_name, node_id = from_base64("Zm9vYmFyOjE6Mjoz") # foobar:1:2:3 assert type_name == "foobar" assert node_id == "1:2:3" @pytest.mark.parametrize("value", [None, 1, 1.1, "dsadfas"]) def test_from_base64_non_base64(value: Any): with pytest.raises(ValueError): _type_name, _node_id = from_base64(value) @pytest.mark.parametrize( "value", [ "Zm9vYmFy", # foobar "Zm9vYmFyLDE=", # foobar,1 "Zm9vYmFyOzE=", # foobar;1 ], ) def test_from_base64_wrong_number_of_args(value: Any): with pytest.raises(ValueError): _type_name, _node_id = from_base64(value) def test_to_base64(): value = to_base64("foobar", "1") assert value == "Zm9vYmFyOjE=" def test_to_base64_with_type(): value = to_base64(Fruit, "1") assert value == "RnJ1aXQ6MQ==" def test_to_base64_with_typedef(): value = to_base64( get_object_definition(Fruit, strict=True), "1", ) assert value == "RnJ1aXQ6MQ==" @pytest.mark.parametrize("value", [None, 1, 1.1, object()]) def test_to_base64_with_invalid_type(value: Any): with pytest.raises(ValueError): value = to_base64(value, "1") @pytest.mark.parametrize( ( "before", "after", "first", "last", "max_results", "expected", "expected_overfetch", ), [ ( None, None, None, None, 100, SliceMetadata(start=0, end=100, expected=100), 101, ), ( None, None, None, None, 200, SliceMetadata(start=0, end=200, expected=200), 201, ), ( None, None, 10, None, 100, SliceMetadata(start=0, end=10, expected=10), 11, ), ( None, None, None, 10, 100, SliceMetadata(start=0, end=sys.maxsize, expected=None), sys.maxsize, ), ( 10, None, None, None, 100, SliceMetadata(start=0, end=10, expected=10), 11, ), ( None, 10, None, None, 100, SliceMetadata(start=11, end=111, expected=100), 112, ), ( 15, None, 10, None, 100, SliceMetadata(start=14, end=24, expected=10), 25, ), ( None, 15, None, 10, 100, SliceMetadata(start=16, end=sys.maxsize, expected=None), sys.maxsize, ), ], ) def test_get_slice_metadata( before: str | None, after: str | None, first: int | None, last: int | None, max_results: int, expected: SliceMetadata, expected_overfetch: int, ): info = mock.Mock() info.schema.config = StrawberryConfig(relay_max_results=max_results) slice_metadata = SliceMetadata.from_arguments( info, before=before and to_base64(PREFIX, before), after=after and to_base64(PREFIX, after), first=first, last=last, ) assert slice_metadata == expected assert slice_metadata.overfetch == expected_overfetch strawberry-graphql-0.287.0/tests/sanic/000077500000000000000000000000001511033167500177715ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/sanic/__init__.py000066400000000000000000000000001511033167500220700ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/sanic/test_file_upload.py000066400000000000000000000045261511033167500236740ustar00rootroot00000000000000from __future__ import annotations from io import BytesIO from typing import TYPE_CHECKING import pytest import strawberry from strawberry.file_uploads import Upload if TYPE_CHECKING: from sanic import Sanic @strawberry.type class Query: @strawberry.field def index(self) -> str: return "Hello there" @strawberry.type class Mutation: @strawberry.mutation def file_upload(self, file: Upload) -> str: return file.name @pytest.fixture def app(): from sanic import Sanic from strawberry.sanic.views import GraphQLView sanic_app = Sanic("sanic_testing") sanic_app.add_route( GraphQLView.as_view( schema=strawberry.Schema(query=Query, mutation=Mutation), multipart_uploads_enabled=True, ), "/graphql", ) return sanic_app def test_file_cast(app: Sanic): """Tests that the list of files in a sanic Request gets correctly turned into a dictionary""" from sanic.request import File from strawberry.sanic import utils file_name = "test.txt" file_content = b"Hello, there!." in_memory_file = BytesIO(file_content) in_memory_file.name = file_name form_data = { "operations": '{ "query": "mutation($file: Upload!){ fileUpload(file: $file) }", "variables": { "file": null } }', "map": '{ "file": ["variables.file"] }', } files = { "file": in_memory_file, } request, _ = app.test_client.post("/graphql", data=form_data, files=files) files = utils.convert_request_to_files_dict(request) # type: ignore file = files["file"] assert isinstance(file, File) assert file.name == file_name assert file.body == file_content def test_endpoint(app: Sanic): """Tests that the graphql api correctly handles file upload and processing""" file_name = "test.txt" file_content = b"Hello, there!" in_memory_file = BytesIO(file_content) in_memory_file.name = file_name form_data = { "operations": '{ "query": "mutation($file: Upload!){ fileUpload(file: $file) }", "variables": { "file": null } }', "map": '{ "file": ["variables.file"] }', } files = { "file": in_memory_file, } _, response = app.test_client.post("/graphql", data=form_data, files=files) assert response.json["data"]["fileUpload"] == file_name # type: ignore strawberry-graphql-0.287.0/tests/schema/000077500000000000000000000000001511033167500201345ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/schema/__init__.py000066400000000000000000000000001511033167500222330ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/schema/extensions/000077500000000000000000000000001511033167500223335ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/schema/extensions/__init__.py000066400000000000000000000000001511033167500244320ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/schema/extensions/schema_extensions/000077500000000000000000000000001511033167500260525ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/schema/extensions/schema_extensions/__init__.py000066400000000000000000000000001511033167500301510ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/schema/extensions/schema_extensions/conftest.py000066400000000000000000000061521511033167500302550ustar00rootroot00000000000000import contextlib import dataclasses import enum from collections.abc import AsyncGenerator from typing import Any import pytest import strawberry from strawberry.extensions import SchemaExtension @dataclasses.dataclass class SchemaHelper: query_type: type subscription_type: type query: str subscription: str class ExampleExtension(SchemaExtension): def __init_subclass__(cls, **kwargs: Any): super().__init_subclass__(**kwargs) cls.called_hooks = [] expected = [ "on_operation Entered", "on_parse Entered", "on_parse Exited", "on_validate Entered", "on_validate Exited", "on_execute Entered", "resolve", "resolve", "on_execute Exited", "on_operation Exited", "get_results", ] called_hooks: list[str] @classmethod def assert_expected(cls) -> None: assert cls.called_hooks == cls.expected @pytest.fixture def default_query_types_and_query() -> SchemaHelper: @strawberry.type class Person: name: str = "Jess" @strawberry.type class Query: @strawberry.field def person(self) -> Person: return Person() @strawberry.type class Subscription: @strawberry.subscription async def count(self) -> AsyncGenerator[int, None]: for i in range(5): yield i subscription = "subscription TestSubscribe { count }" query = "query TestQuery { person { name } }" return SchemaHelper( query_type=Query, query=query, subscription_type=Subscription, subscription=subscription, ) class ExecType(enum.Enum): SYNC = enum.auto() ASYNC = enum.auto() def is_async(self) -> bool: return self == ExecType.ASYNC @pytest.fixture(params=[ExecType.ASYNC, ExecType.SYNC]) def exec_type(request: pytest.FixtureRequest) -> ExecType: return request.param @contextlib.contextmanager def hook_wrap(list_: list[str], hook_name: str): list_.append(f"{hook_name} Entered") try: yield finally: list_.append(f"{hook_name} Exited") @pytest.fixture def async_extension() -> type[ExampleExtension]: class MyExtension(ExampleExtension): async def on_operation(self): with hook_wrap(self.called_hooks, SchemaExtension.on_operation.__name__): yield async def on_validate(self): with hook_wrap(self.called_hooks, SchemaExtension.on_validate.__name__): yield async def on_parse(self): with hook_wrap(self.called_hooks, SchemaExtension.on_parse.__name__): yield async def on_execute(self): with hook_wrap(self.called_hooks, SchemaExtension.on_execute.__name__): yield async def get_results(self): self.called_hooks.append("get_results") return {"example": "example"} async def resolve(self, _next, root, info, *args: str, **kwargs: Any): self.called_hooks.append("resolve") return _next(root, info, *args, **kwargs) return MyExtension strawberry-graphql-0.287.0/tests/schema/extensions/schema_extensions/test_defer_extensions.py000066400000000000000000000042411511033167500330300ustar00rootroot00000000000000import pytest from inline_snapshot import snapshot import strawberry from strawberry.schema.config import StrawberryConfig from tests.conftest import skip_if_gql_32 from tests.schema.extensions.schema_extensions.conftest import ( ExampleExtension, ) pytestmark = skip_if_gql_32("GraphQL 3.3.0 is required for incremental execution") @pytest.mark.xfail(reason="Not fully supported just yet") async def test_basic_extension_with_defer( async_extension: type[ExampleExtension], ): @strawberry.type class Hero: id: strawberry.ID @strawberry.field async def name(self) -> str: return "Luke Skywalker" @strawberry.type class Query: @strawberry.field def hero(self) -> Hero: return Hero(id=strawberry.ID("1")) extension = async_extension() schema = strawberry.Schema( query=Query, extensions=[extension], config=StrawberryConfig(enable_experimental_incremental_execution=True), ) result = await schema.execute( query=""" query HeroNameQuery { hero { id ...NameFragment @defer } } fragment NameFragment on Hero { name } """, ) initial = result.initial_result.formatted assert initial == snapshot( { "data": {"hero": {"id": "1"}}, "hasNext": True, "pending": [{"path": ["hero"], "id": "0"}], } ) async for subsequent in result.subsequent_results: assert subsequent.formatted == snapshot( { "completed": [ { "id": "0", "errors": [ { "message": "String cannot represent value: ", "locations": [{"line": 9, "column": 13}], "path": ["hero", "name"], } ], } ], "hasNext": False, } ) extension.assert_expected() strawberry-graphql-0.287.0/tests/schema/extensions/schema_extensions/test_extensions.py000066400000000000000000001025731511033167500316720ustar00rootroot00000000000000import contextlib import json import warnings from typing import Any from unittest.mock import patch import pytest from graphql import ExecutionResult as GraphQLExecutionResult from graphql import GraphQLError from graphql import execute as original_execute import strawberry from strawberry.exceptions import StrawberryGraphQLError from strawberry.extensions import SchemaExtension from .conftest import ExampleExtension, ExecType, SchemaHelper, hook_wrap def test_base_extension(): @strawberry.type class Person: name: str = "Jess" @strawberry.type class Query: @strawberry.field def person(self) -> Person: return Person() schema = strawberry.Schema(query=Query, extensions=[SchemaExtension]) query = """ query { person { name } } """ result = schema.execute_sync(query) assert not result.errors assert result.extensions == {} def test_called_only_if_overriden(monkeypatch: pytest.MonkeyPatch): called = False def dont_call_me(self_): nonlocal called called = True class ExtensionNoHooks(SchemaExtension): ... for hook in ( ExtensionNoHooks.on_parse, ExtensionNoHooks.on_operation, ExtensionNoHooks.on_execute, ExtensionNoHooks.on_validate, ): monkeypatch.setattr(SchemaExtension, hook.__name__, dont_call_me) @strawberry.type class Person: name: str = "Jess" @strawberry.type class Query: @strawberry.field def person(self) -> Person: return Person() schema = strawberry.Schema(query=Query, extensions=[ExtensionNoHooks]) query = """ query { person { name } } """ result = schema.execute_sync(query) assert not result.errors assert result.extensions == {} assert not called def test_extension_access_to_parsed_document(): query_name = "" class MyExtension(SchemaExtension): def on_parse(self): nonlocal query_name yield query_definition = self.execution_context.graphql_document.definitions[0] query_name = query_definition.name.value @strawberry.type class Person: name: str = "Jess" @strawberry.type class Query: @strawberry.field def person(self) -> Person: return Person() schema = strawberry.Schema(query=Query, extensions=[MyExtension]) query = """ query TestQuery { person { name } } """ result = schema.execute_sync(query) assert not result.errors assert query_name == "TestQuery" def test_extension_access_to_errors(): execution_errors = [] class MyExtension(SchemaExtension): def on_operation(self): nonlocal execution_errors yield execution_errors = self.execution_context.pre_execution_errors @strawberry.type class Person: name: str = "Jess" @strawberry.type class Query: @strawberry.field def person(self) -> Person: return None # type: ignore schema = strawberry.Schema(query=Query, extensions=[MyExtension]) query = """ query TestQuery { person { name } } """ result = schema.execute_sync(query) assert len(result.errors) == 1 assert execution_errors == result.errors def test_extension_access_to_root_value(): root_value = None class MyExtension(SchemaExtension): def on_operation(self): nonlocal root_value yield root_value = self.execution_context.root_value @strawberry.type class Query: @strawberry.field def hi(self) -> str: return "👋" schema = strawberry.Schema(query=Query, extensions=[MyExtension]) query = "{ hi }" result = schema.execute_sync(query, root_value="ROOT") assert not result.errors assert root_value == "ROOT" def test_extension_access_to_operation_extensions(): operation_extensions = None class MyExtension(SchemaExtension): def on_operation(self): nonlocal operation_extensions yield operation_extensions = self.execution_context.operation_extensions @strawberry.type class Query: @strawberry.field def hi(self) -> str: return "👋" schema = strawberry.Schema(query=Query, extensions=[MyExtension]) query = "{ hi }" result = schema.execute_sync(query, operation_extensions={"MyExtension": {}}) assert not result.errors assert operation_extensions == {"MyExtension": {}} def test_can_initialize_extension(default_query_types_and_query): class CustomizableExtension(SchemaExtension): def __init__(self, arg: int): self.arg = arg def on_operation(self): yield self.execution_context.result.data = {"override": self.arg} schema = strawberry.Schema( query=default_query_types_and_query.query_type, extensions=[ CustomizableExtension(20), ], ) res = schema.execute_sync(query=default_query_types_and_query.query) assert not res.errors assert res.data == {"override": 20} @pytest.fixture def sync_extension() -> type[ExampleExtension]: class MyExtension(ExampleExtension): def on_operation(self): with hook_wrap(self.called_hooks, SchemaExtension.on_operation.__name__): yield def on_validate(self): with hook_wrap(self.called_hooks, SchemaExtension.on_validate.__name__): yield def on_parse(self): with hook_wrap(self.called_hooks, SchemaExtension.on_parse.__name__): yield def on_execute(self): with hook_wrap(self.called_hooks, SchemaExtension.on_execute.__name__): yield def get_results(self): self.called_hooks.append("get_results") return {"example": "example"} def resolve(self, _next, root, info, *args: str, **kwargs: Any): self.called_hooks.append("resolve") return _next(root, info, *args, **kwargs) return MyExtension @pytest.mark.asyncio async def test_async_extension_hooks( default_query_types_and_query: SchemaHelper, async_extension: type[ExampleExtension] ): schema = strawberry.Schema( query=default_query_types_and_query.query_type, extensions=[async_extension] ) result = await schema.execute(default_query_types_and_query.query) assert result.errors is None async_extension.assert_expected() @pytest.mark.asyncio async def test_mixed_sync_and_async_extension_hooks( default_query_types_and_query, sync_extension ): class MyExtension(sync_extension): async def on_operation(self): with hook_wrap(self.called_hooks, SchemaExtension.on_operation.__name__): yield async def on_parse(self): with hook_wrap(self.called_hooks, SchemaExtension.on_parse.__name__): yield @strawberry.type class Person: name: str = "Jess" @strawberry.type class Query: @strawberry.field def person(self) -> Person: return Person() schema = strawberry.Schema( query=default_query_types_and_query.query_type, extensions=[MyExtension] ) result = await schema.execute(default_query_types_and_query.query) assert result.errors is None MyExtension.assert_expected() async def test_execution_order(default_query_types_and_query): called_hooks = [] @contextlib.contextmanager def register_hook(hook_name: str, klass: type): called_hooks.append(f"{klass.__name__}, {hook_name} Entered") yield called_hooks.append(f"{klass.__name__}, {hook_name} Exited") class ExtensionA(ExampleExtension): async def on_operation(self): with register_hook(SchemaExtension.on_operation.__name__, ExtensionA): yield async def on_parse(self): with register_hook(SchemaExtension.on_parse.__name__, ExtensionA): yield def on_validate(self): with register_hook(SchemaExtension.on_validate.__name__, ExtensionA): yield def on_execute(self): with register_hook(SchemaExtension.on_execute.__name__, ExtensionA): yield class ExtensionB(ExampleExtension): async def on_operation(self): with register_hook(SchemaExtension.on_operation.__name__, ExtensionB): yield def on_parse(self): with register_hook(SchemaExtension.on_parse.__name__, ExtensionB): yield def on_validate(self): with register_hook(SchemaExtension.on_validate.__name__, ExtensionB): yield async def on_execute(self): with register_hook(SchemaExtension.on_execute.__name__, ExtensionB): yield schema = strawberry.Schema( query=default_query_types_and_query.query_type, extensions=[ExtensionA, ExtensionB], ) result = await schema.execute(default_query_types_and_query.query) assert result.errors is None assert called_hooks == [ "ExtensionA, on_operation Entered", "ExtensionB, on_operation Entered", "ExtensionA, on_parse Entered", "ExtensionB, on_parse Entered", "ExtensionB, on_parse Exited", "ExtensionA, on_parse Exited", "ExtensionA, on_validate Entered", "ExtensionB, on_validate Entered", "ExtensionB, on_validate Exited", "ExtensionA, on_validate Exited", "ExtensionA, on_execute Entered", "ExtensionB, on_execute Entered", "ExtensionB, on_execute Exited", "ExtensionA, on_execute Exited", "ExtensionB, on_operation Exited", "ExtensionA, on_operation Exited", ] async def test_sync_extension_hooks(default_query_types_and_query, sync_extension): schema = strawberry.Schema( query=default_query_types_and_query.query_type, extensions=[ sync_extension, ], ) result = schema.execute_sync(default_query_types_and_query.query) assert result.errors is None sync_extension.assert_expected() async def test_extension_no_yield(default_query_types_and_query): class SyncExt(ExampleExtension): expected = [ f"{SchemaExtension.on_operation.__name__} Entered", f"{SchemaExtension.on_parse.__name__} Entered", ] def on_operation(self): self.called_hooks.append(self.__class__.expected[0]) async def on_parse(self): self.called_hooks.append(self.__class__.expected[1]) schema = strawberry.Schema( query=default_query_types_and_query.query_type, extensions=[SyncExt] ) result = await schema.execute(default_query_types_and_query.query) assert result.errors is None SyncExt.assert_expected() def test_raise_if_defined_both_legacy_and_new_style(default_query_types_and_query): class WrongUsageExtension(SchemaExtension): def on_execute(self): yield def on_executing_start(self): ... schema = strawberry.Schema( query=default_query_types_and_query.query_type, extensions=[WrongUsageExtension] ) result = schema.execute_sync(default_query_types_and_query.query) assert len(result.errors) == 1 assert isinstance(result.errors[0].original_error, ValueError) async def test_legacy_extension_supported(): with warnings.catch_warnings(record=True) as w: warnings.filterwarnings( "ignore", category=DeprecationWarning, message=r"'.*' is deprecated and slated for removal in Python 3\.\d+", ) class CompatExtension(ExampleExtension): async def on_request_start(self): self.called_hooks.append( f"{SchemaExtension.on_operation.__name__} Entered" ) async def on_request_end(self): self.called_hooks.append( f"{SchemaExtension.on_operation.__name__} Exited" ) async def on_validation_start(self): self.called_hooks.append( f"{SchemaExtension.on_validate.__name__} Entered" ) async def on_validation_end(self): self.called_hooks.append( f"{SchemaExtension.on_validate.__name__} Exited" ) async def on_parsing_start(self): self.called_hooks.append(f"{SchemaExtension.on_parse.__name__} Entered") async def on_parsing_end(self): self.called_hooks.append(f"{SchemaExtension.on_parse.__name__} Exited") def on_executing_start(self): self.called_hooks.append( f"{SchemaExtension.on_execute.__name__} Entered" ) def on_executing_end(self): self.called_hooks.append( f"{SchemaExtension.on_execute.__name__} Exited" ) @strawberry.type class Person: name: str = "Jess" @strawberry.type class Query: @strawberry.field def person(self) -> Person: return Person() schema = strawberry.Schema(query=Query, extensions=[CompatExtension]) query = "query TestQuery { person { name } }" result = await schema.execute(query) assert result.errors is None assert CompatExtension.called_hooks == list( filter(lambda x: x.startswith("on_"), ExampleExtension.expected) ) assert "Event driven styled extensions for" in w[0].message.args[0] async def test_legacy_only_start(): with warnings.catch_warnings(record=True) as w: warnings.filterwarnings( "ignore", category=DeprecationWarning, message=r"'.*' is deprecated and slated for removal in Python 3\.\d+", ) class CompatExtension(ExampleExtension): expected = list( filter(lambda x: x.endswith(" Entered"), ExampleExtension.expected) ) async def on_request_start(self): self.called_hooks.append( f"{SchemaExtension.on_operation.__name__} Entered" ) async def on_validation_start(self): self.called_hooks.append( f"{SchemaExtension.on_validate.__name__} Entered" ) async def on_parsing_start(self): self.called_hooks.append(f"{SchemaExtension.on_parse.__name__} Entered") def on_executing_start(self): self.called_hooks.append( f"{SchemaExtension.on_execute.__name__} Entered" ) @strawberry.type class Person: name: str = "Jess" @strawberry.type class Query: @strawberry.field def person(self) -> Person: return Person() schema = strawberry.Schema(query=Query, extensions=[CompatExtension]) query = "query TestQuery { person { name } }" result = await schema.execute(query) assert result.errors is None CompatExtension.assert_expected() assert "Event driven styled extensions for" in w[0].message.args[0] async def test_legacy_only_end(): with warnings.catch_warnings(record=True) as w: warnings.filterwarnings( "ignore", category=DeprecationWarning, message=r"'.*' is deprecated and slated for removal in Python 3\.\d+", ) class CompatExtension(ExampleExtension): expected = list( filter(lambda x: x.endswith(" Exited"), ExampleExtension.expected) ) async def on_request_end(self): self.called_hooks.append( f"{SchemaExtension.on_operation.__name__} Exited" ) async def on_validation_end(self): self.called_hooks.append( f"{SchemaExtension.on_validate.__name__} Exited" ) async def on_parsing_end(self): self.called_hooks.append(f"{SchemaExtension.on_parse.__name__} Exited") def on_executing_end(self): self.called_hooks.append( f"{SchemaExtension.on_execute.__name__} Exited" ) @strawberry.type class Person: name: str = "Jess" @strawberry.type class Query: @strawberry.field def person(self) -> Person: return Person() schema = strawberry.Schema(query=Query, extensions=[CompatExtension]) query = "query TestQuery { person { name } }" result = await schema.execute(query) assert result.errors is None CompatExtension.assert_expected() assert "Event driven styled extensions for" in w[0].message.args[0] def test_warning_about_async_get_results_hooks_in_sync_context(): class MyExtension(SchemaExtension): async def get_results(self): pass @strawberry.type class Query: @strawberry.field def string(self) -> str: return "" schema = strawberry.Schema(query=Query, extensions=[MyExtension]) query = "query { string }" with pytest.raises( RuntimeError, match="Cannot use async extension hook during sync execution" ): schema.execute_sync(query) class ExceptionTestingExtension(SchemaExtension): def __init__(self, failing_hook: str): self.failing_hook = failing_hook self.called_hooks = set() def on_operation(self): if self.failing_hook == "on_operation_start": raise Exception(self.failing_hook) self.called_hooks.add(1) with contextlib.suppress(Exception): yield if self.failing_hook == "on_operation_end": raise Exception(self.failing_hook) self.called_hooks.add(8) def on_parse(self): if self.failing_hook == "on_parse_start": raise Exception(self.failing_hook) self.called_hooks.add(2) with contextlib.suppress(Exception): yield if self.failing_hook == "on_parse_end": raise Exception(self.failing_hook) self.called_hooks.add(3) def on_validate(self): if self.failing_hook == "on_validate_start": raise Exception(self.failing_hook) self.called_hooks.add(4) with contextlib.suppress(Exception): yield if self.failing_hook == "on_validate_end": raise Exception(self.failing_hook) self.called_hooks.add(5) def on_execute(self): if self.failing_hook == "on_execute_start": raise Exception(self.failing_hook) self.called_hooks.add(6) with contextlib.suppress(Exception): yield if self.failing_hook == "on_execute_end": raise Exception(self.failing_hook) self.called_hooks.add(7) @pytest.mark.parametrize( "failing_hook", [ "on_operation_start", "on_operation_end", "on_parse_start", "on_parse_end", "on_validate_start", "on_validate_end", "on_execute_start", "on_execute_end", ], ) @pytest.mark.asyncio async def test_exceptions_are_included_in_the_execution_result(failing_hook): @strawberry.type class Query: @strawberry.field def ping(self) -> str: return "pong" schema = strawberry.Schema( query=Query, extensions=[ExceptionTestingExtension(failing_hook)], ) document = "query { ping }" sync_result = schema.execute_sync(document) assert sync_result.errors is not None assert len(sync_result.errors) == 1 assert sync_result.errors[0].message == failing_hook async_result = await schema.execute(document) assert async_result.errors is not None assert len(async_result.errors) == 1 assert sync_result.errors[0].message == failing_hook @pytest.mark.parametrize( ("failing_hook", "expected_hooks"), [ ("on_operation_start", set()), ("on_parse_start", {1, 8}), ("on_parse_end", {1, 2, 8}), ("on_validate_start", {1, 2, 3, 8}), ("on_validate_end", {1, 2, 3, 4, 8}), ("on_execute_start", {1, 2, 3, 4, 5, 8}), ("on_execute_end", {1, 2, 3, 4, 5, 6, 8}), ("on_operation_end", {1, 2, 3, 4, 5, 6, 7}), ], ) @pytest.mark.asyncio async def test_exceptions_abort_evaluation(failing_hook, expected_hooks): @strawberry.type class Query: @strawberry.field def ping(self) -> str: return "pong" extension = ExceptionTestingExtension(failing_hook) schema = strawberry.Schema(query=Query, extensions=[extension]) document = "query { ping }" extension.called_hooks = set() schema.execute_sync(document) assert extension.called_hooks == expected_hooks extension.called_hooks = set() await schema.execute(document) assert extension.called_hooks == expected_hooks async def test_generic_exceptions_get_wrapped_in_a_graphql_error( exec_type: ExecType, ) -> None: exception = Exception("This should be wrapped in a GraphQL error") class MyExtension(SchemaExtension): def on_parse(self): raise exception @strawberry.type class Query: ping: str = "pong" schema = strawberry.Schema(query=Query, extensions=[MyExtension]) query = "query { ping }" if exec_type.is_async(): res = await schema.execute(query) else: res = schema.execute_sync(query) res = await schema.execute(query) assert len(res.errors) == 1 assert isinstance(res.errors[0], GraphQLError) assert res.errors[0].original_error == exception async def test_graphql_errors_get_not_wrapped_in_a_graphql_error( exec_type: ExecType, ) -> None: exception = GraphQLError("This should not be wrapped in a GraphQL error") class MyExtension(SchemaExtension): def on_parse(self): raise exception @strawberry.type class Query: ping: str = "pong" schema = strawberry.Schema(query=Query, extensions=[MyExtension]) query = "query { ping }" if exec_type.is_async(): res = await schema.execute(query) else: res = schema.execute_sync(query) assert len(res.errors) == 1 assert res.errors[0] == exception assert res.errors[0].original_error is None @pytest.mark.asyncio async def test_dont_swallow_errors_in_parsing_hooks(): class MyExtension(SchemaExtension): def on_parse(self): raise Exception("This shouldn't be swallowed") @strawberry.type class Query: @strawberry.field def ping(self) -> str: return "pong" schema = strawberry.Schema(query=Query, extensions=[MyExtension]) query = "query { string }" sync_result = schema.execute_sync(query) assert len(sync_result.errors) == 1 assert sync_result.errors[0].message == "This shouldn't be swallowed" async_result = await schema.execute(query) assert len(async_result.errors) == 1 assert async_result.errors[0].message == "This shouldn't be swallowed" def test_on_parsing_end_is_called_with_parsing_errors(): execution_errors = False class MyExtension(SchemaExtension): def on_parse(self): nonlocal execution_errors yield execution_context = self.execution_context execution_errors = execution_context.pre_execution_errors @strawberry.type class Query: @strawberry.field def ping(self) -> str: return "pong" schema = strawberry.Schema(query=Query, extensions=[MyExtension]) query = "query { string" # Invalid query result = schema.execute_sync(query) assert result.errors assert result.errors == execution_errors def test_extension_execution_order_sync(): """Ensure mixed hooks (async & sync) are called correctly.""" execution_order: list[type[SchemaExtension]] = [] class ExtensionB(SchemaExtension): def on_execute(self): execution_order.append(type(self)) yield execution_order.append(type(self)) class ExtensionC(SchemaExtension): def on_execute(self): execution_order.append(type(self)) yield execution_order.append(type(self)) @strawberry.type class Query: food: str = "strawberry" extensions = [ExtensionB, ExtensionC] schema = strawberry.Schema(query=Query, extensions=extensions) query = """ query TestQuery { food } """ result = schema.execute_sync(query, root_value=Query()) assert not result.errors assert result.data == {"food": "strawberry"} assert execution_order == [ExtensionB, ExtensionC, ExtensionC, ExtensionB] def test_async_extension_in_sync_context(): class ExtensionA(SchemaExtension): async def on_execute(self): yield @strawberry.type class Query: food: str = "strawberry" schema = strawberry.Schema(query=Query, extensions=[ExtensionA]) result = schema.execute_sync("query { food }") assert len(result.errors) == 1 assert result.errors[0].message.endswith("failed to complete synchronously.") def test_extension_override_execution(): class MyExtension(SchemaExtension): def on_execute(self): # Always return a static response self.execution_context.result = GraphQLExecutionResult( data={ "surprise": "data", }, errors=[], ) @strawberry.type class Query: @strawberry.field def ping(self) -> str: return "pong" schema = strawberry.Schema(query=Query, extensions=[MyExtension]) query = """ query TestQuery { ping } """ result = schema.execute_sync(query) assert not result.errors assert result.data == { "surprise": "data", } @pytest.mark.asyncio async def test_extension_override_execution_async(): class MyExtension(SchemaExtension): def on_execute(self): # Always return a static response self.execution_context.result = GraphQLExecutionResult( data={ "surprise": "data", }, errors=[], ) @strawberry.type class Query: @strawberry.field def ping(self) -> str: return "pong" schema = strawberry.Schema(query=Query, extensions=[MyExtension]) query = """ query TestQuery { ping } """ result = await schema.execute(query) assert not result.errors assert result.data == { "surprise": "data", } @patch("strawberry.schema.schema.execute", wraps=original_execute) def test_execution_cache_example(mock_original_execute): # Test that the example of how to use the on_executing_start hook in the # docs actually works response_cache = {} class ExecutionCache(SchemaExtension): def on_execute(self): # Check if we've come across this query before execution_context = self.execution_context self.cache_key = ( f"{execution_context.query}:{json.dumps(execution_context.variables)}" ) if self.cache_key in response_cache: self.execution_context.result = response_cache[self.cache_key] yield if self.cache_key not in response_cache: response_cache[self.cache_key] = execution_context.result @strawberry.type class Query: @strawberry.field def ping(self, return_value: str | None = None) -> str: if return_value is not None: return return_value return "pong" schema = strawberry.Schema( Query, extensions=[ ExecutionCache, ], ) query = """ query TestQuery($returnValue: String) { ping(returnValue: $returnValue) } """ result = schema.execute_sync(query) assert not result.errors assert result.data == { "ping": "pong", } assert mock_original_execute.call_count == 1 # This should be cached result = schema.execute_sync(query) assert not result.errors assert result.data == { "ping": "pong", } assert mock_original_execute.call_count == 1 # Calling with different variables should not be cached result = schema.execute_sync( query, variable_values={ "returnValue": "plong", }, ) assert not result.errors assert result.data == { "ping": "plong", } assert mock_original_execute.call_count == 2 @patch("strawberry.schema.schema.execute", wraps=original_execute) def test_execution_reject_example(mock_original_execute): # Test that the example of how to use the on_executing_start hook in the # docs actually works class RejectSomeQueries(SchemaExtension): def on_execute(self): # Reject all operations called "RejectMe" execution_context = self.execution_context if execution_context.operation_name == "RejectMe": self.execution_context.result = GraphQLExecutionResult( data=None, errors=[GraphQLError("Well you asked for it")], ) @strawberry.type class Query: @strawberry.field def ping(self) -> str: return "pong" schema = strawberry.Schema( Query, extensions=[ RejectSomeQueries, ], ) query = """ query TestQuery { ping } """ result = schema.execute_sync(query, operation_name="TestQuery") assert not result.errors assert result.data == { "ping": "pong", } assert mock_original_execute.call_count == 1 query = """ query RejectMe { ping } """ result = schema.execute_sync(query, operation_name="RejectMe") assert result.errors == [GraphQLError("Well you asked for it")] assert mock_original_execute.call_count == 1 def test_extend_error_format_example(): # Test that the example of how to extend error format class ExtendErrorFormat(SchemaExtension): def on_operation(self): yield result = self.execution_context.result if getattr(result, "errors", None): result.errors = [ StrawberryGraphQLError( extensions={"additional_key": "additional_value"}, nodes=error.nodes, source=error.source, positions=error.positions, path=error.path, original_error=error.original_error, message=error.message, ) for error in result.errors ] @strawberry.type class Query: @strawberry.field def ping(self) -> str: raise Exception("This error occurred while querying the ping field") schema = strawberry.Schema(query=Query, extensions=[ExtendErrorFormat]) query = """ query TestQuery { ping } """ result = schema.execute_sync(query) assert result.errors[0].extensions == {"additional_key": "additional_value"} assert ( result.errors[0].message == "This error occurred while querying the ping field" ) assert result.data is None def test_extension_can_set_query(): class MyExtension(SchemaExtension): def on_operation(self): self.execution_context.query = "{ hi }" yield @strawberry.type class Query: @strawberry.field def hi(self) -> str: return "👋" schema = strawberry.Schema(query=Query, extensions=[MyExtension]) # Query not set on input query = "" result = schema.execute_sync(query) assert not result.errors assert result.data == {"hi": "👋"} @pytest.mark.asyncio async def test_extension_can_set_query_async(): class MyExtension(SchemaExtension): def on_operation(self): self.execution_context.query = "{ hi }" yield @strawberry.type class Query: @strawberry.field async def hi(self) -> str: return "👋" schema = strawberry.Schema(query=Query, extensions=[MyExtension]) # Query not set on input query = "" result = await schema.execute(query) assert not result.errors assert result.data == {"hi": "👋"} def test_raise_if_hook_is_not_callable(default_query_types_and_query: SchemaHelper): class MyExtension(SchemaExtension): on_operation = "ABC" # type: ignore schema = strawberry.Schema( query=default_query_types_and_query.query_type, extensions=[MyExtension] ) result = schema.execute_sync(default_query_types_and_query.query) assert len(result.errors) == 1 assert isinstance(result.errors[0].original_error, ValueError) assert result.errors[0].message.startswith("Hook on_operation on <") assert result.errors[0].message.endswith("> must be callable, received 'ABC'") strawberry-graphql-0.287.0/tests/schema/extensions/schema_extensions/test_subscription.py000066400000000000000000000127071511033167500322160ustar00rootroot00000000000000from collections.abc import AsyncGenerator import pytest import strawberry from strawberry.extensions import SchemaExtension from strawberry.types.execution import ExecutionResult, PreExecutionError from tests.conftest import skip_if_gql_32 from .conftest import ExampleExtension, SchemaHelper pytestmark = skip_if_gql_32( "We only fully support schema extensions in graphql-core 3.3+" ) def assert_agen(obj) -> AsyncGenerator[ExecutionResult, None]: assert isinstance(obj, AsyncGenerator) return obj async def test_subscription_success_many_fields( default_query_types_and_query: SchemaHelper, async_extension: type[ExampleExtension] ) -> None: schema = strawberry.Schema( query=default_query_types_and_query.query_type, subscription=default_query_types_and_query.subscription_type, extensions=[async_extension], ) subscription_per_yield_hooks_exp = [] for _ in range(5): # number of yields in the subscription subscription_per_yield_hooks_exp.extend(["resolve", "get_results"]) async_extension.expected = [ "on_operation Entered", "on_parse Entered", "on_parse Exited", "on_validate Entered", "on_validate Exited", "on_execute Entered", "on_execute Exited", *subscription_per_yield_hooks_exp, # last one doesn't call the "resolve" / "get_results" hooks because # the subscription is done "on_operation Exited", ] async for res in assert_agen( await schema.subscribe(default_query_types_and_query.subscription) ): assert res.data assert not res.errors async_extension.assert_expected() async def test_subscription_extension_handles_immediate_errors( default_query_types_and_query: SchemaHelper, async_extension: type[ExampleExtension] ) -> None: @strawberry.type() class Subscription: @strawberry.subscription() async def count(self) -> AsyncGenerator[int, None]: raise ValueError("This is an error") schema = strawberry.Schema( query=default_query_types_and_query.query_type, subscription=Subscription, extensions=[async_extension], ) async_extension.expected = [ "on_operation Entered", "on_parse Entered", "on_parse Exited", "on_validate Entered", "on_validate Exited", "on_execute Entered", "on_execute Exited", "get_results", "on_operation Exited", ] result = await schema.subscribe(default_query_types_and_query.subscription) results = [result async for result in result] assert len(results) == 1 result = results[0] assert isinstance(result, PreExecutionError) assert result.errors async_extension.assert_expected() async def test_error_after_first_yield_in_subscription( default_query_types_and_query: SchemaHelper, async_extension: type[ExampleExtension] ) -> None: @strawberry.type() class Subscription: @strawberry.subscription() async def count(self) -> AsyncGenerator[int, None]: yield 1 raise ValueError("This is an error") schema = strawberry.Schema( query=default_query_types_and_query.query_type, subscription=Subscription, extensions=[async_extension], ) agen = await schema.subscribe(default_query_types_and_query.subscription) assert isinstance(agen, AsyncGenerator) res1 = await agen.__anext__() assert res1.data assert not res1.errors res2 = await agen.__anext__() assert not res2.data assert res2.errors # close the generator with pytest.raises(StopAsyncIteration): await agen.__anext__() async_extension.expected = [ "on_operation Entered", "on_parse Entered", "on_parse Exited", "on_validate Entered", "on_validate Exited", "on_execute Entered", "on_execute Exited", "resolve", "get_results", "get_results", "on_operation Exited", ] async_extension.assert_expected() async def test_extensions_results_are_cleared_between_subscription_yields( default_query_types_and_query: SchemaHelper, ) -> None: class MyExtension(SchemaExtension): execution_number = 0 def get_results(self): self.execution_number += 1 return {str(self.execution_number): self.execution_number} schema = strawberry.Schema( query=default_query_types_and_query.query_type, subscription=default_query_types_and_query.subscription_type, extensions=[MyExtension], ) res_num = 1 async for res in assert_agen( await schema.subscribe(default_query_types_and_query.subscription) ): assert res.extensions == {str(res_num): res_num} assert not res.errors res_num += 1 async def test_subscription_catches_extension_errors( default_query_types_and_query: SchemaHelper, ) -> None: class MyExtension(SchemaExtension): def on_execute(self): raise ValueError("This is an error") schema = strawberry.Schema( query=default_query_types_and_query.query_type, subscription=default_query_types_and_query.subscription_type, extensions=[MyExtension], ) async for res in assert_agen( await schema.subscribe(default_query_types_and_query.subscription) ): assert res.errors assert not res.data assert res.errors[0].message == "This is an error" strawberry-graphql-0.287.0/tests/schema/extensions/test_apollo.py000066400000000000000000000125211511033167500252330ustar00rootroot00000000000000import pytest from freezegun import freeze_time from graphql.utilities import get_introspection_query import strawberry from strawberry.extensions.tracing.apollo import ( ApolloTracingExtension, ApolloTracingExtensionSync, ) @freeze_time("20120114 12:00:01") def test_tracing_sync(mocker): mocker.patch( "strawberry.extensions.tracing.apollo.time.perf_counter_ns", return_value=0 ) @strawberry.type class Person: name: str = "Jess" @strawberry.type class Query: @strawberry.field def person(self) -> Person: return Person() schema = strawberry.Schema(query=Query, extensions=[ApolloTracingExtensionSync]) query = """ query { person { name } } """ result = schema.execute_sync(query) assert not result.errors assert result.extensions == { "tracing": { "version": 1, "startTime": "2012-01-14T12:00:01.000000Z", "endTime": "2012-01-14T12:00:01.000000Z", "duration": 0, "execution": { "resolvers": [ { "path": ["person"], "field_name": "person", "parentType": "Query", "returnType": "Person!", "startOffset": 0, "duration": 0, }, ] }, "validation": {"startOffset": 0, "duration": 0}, "parsing": {"startOffset": 0, "duration": 0}, } } @pytest.mark.asyncio @freeze_time("20120114 12:00:01") async def test_tracing_async(mocker): mocker.patch( "strawberry.extensions.tracing.apollo.time.perf_counter_ns", return_value=0 ) @strawberry.type class Person: name: str = "Jess" @strawberry.type class Query: @strawberry.field def example(self) -> str: return "Hi" @strawberry.field async def person(self) -> Person: return Person() schema = strawberry.Schema(query=Query, extensions=[ApolloTracingExtension]) query = """ query { example person { name } } """ result = await schema.execute(query) assert not result.errors assert result.extensions == { "tracing": { "version": 1, "startTime": "2012-01-14T12:00:01.000000Z", "endTime": "2012-01-14T12:00:01.000000Z", "duration": 0, "execution": { "resolvers": [ { "duration": 0, "field_name": "example", "parentType": "Query", "path": ["example"], "returnType": "String!", "startOffset": 0, }, { "path": ["person"], "field_name": "person", "parentType": "Query", "returnType": "Person!", "startOffset": 0, "duration": 0, }, ] }, "validation": {"startOffset": 0, "duration": 0}, "parsing": {"startOffset": 0, "duration": 0}, } } @freeze_time("20120114 12:00:01") def test_should_not_trace_introspection_sync_queries(mocker): mocker.patch( "strawberry.extensions.tracing.apollo.time.perf_counter_ns", return_value=0 ) @strawberry.type class Person: name: str = "Jess" @strawberry.type class Query: @strawberry.field def person(self) -> Person: return Person() schema = strawberry.Schema(query=Query, extensions=[ApolloTracingExtensionSync]) result = schema.execute_sync(get_introspection_query()) assert not result.errors assert result.extensions == { "tracing": { "version": 1, "startTime": "2012-01-14T12:00:01.000000Z", "endTime": "2012-01-14T12:00:01.000000Z", "duration": 0, "execution": {"resolvers": []}, "validation": {"startOffset": 0, "duration": 0}, "parsing": {"startOffset": 0, "duration": 0}, } } @pytest.mark.asyncio @freeze_time("20120114 12:00:01") async def test_should_not_trace_introspection_async_queries(mocker): mocker.patch( "strawberry.extensions.tracing.apollo.time.perf_counter_ns", return_value=0 ) @strawberry.type class Person: name: str = "Jess" @strawberry.type class Query: @strawberry.field async def person(self) -> Person: return Person() schema = strawberry.Schema(query=Query, extensions=[ApolloTracingExtension]) result = await schema.execute(get_introspection_query()) assert not result.errors assert result.extensions == { "tracing": { "version": 1, "startTime": "2012-01-14T12:00:01.000000Z", "endTime": "2012-01-14T12:00:01.000000Z", "duration": 0, "execution": {"resolvers": []}, "validation": {"startOffset": 0, "duration": 0}, "parsing": {"startOffset": 0, "duration": 0}, } } strawberry-graphql-0.287.0/tests/schema/extensions/test_datadog.py000066400000000000000000000231111511033167500253450ustar00rootroot00000000000000import typing from collections.abc import AsyncGenerator from typing import Any import pytest import strawberry if typing.TYPE_CHECKING: from strawberry.extensions.tracing.datadog import DatadogTracingExtension @pytest.fixture def ddtrace_version_2(mocker): ddtrace_mock = mocker.MagicMock() ddtrace_mock.__version__ = "2.20.0" mocker.patch.dict("sys.modules", ddtrace=ddtrace_mock) return ddtrace_mock @pytest.fixture def ddtrace_version_3(mocker): ddtrace_mock = mocker.MagicMock() ddtrace_mock.__version__ = "3.0.0" mocker.patch.dict("sys.modules", ddtrace=ddtrace_mock) trace_mock = mocker.MagicMock() mocker.patch.dict("sys.modules", {"ddtrace.trace": trace_mock}) return trace_mock @pytest.fixture(params=["ddtrace_version_2", "ddtrace_version_3"]) def datadog_extension(request) -> tuple[type["DatadogTracingExtension"], Any]: fixture_name = request.param ddtrace_mock = request.getfixturevalue(fixture_name) from strawberry.extensions.tracing.datadog import DatadogTracingExtension return DatadogTracingExtension, ddtrace_mock @pytest.fixture(params=["ddtrace_version_2", "ddtrace_version_3"]) def datadog_extension_sync(request) -> tuple[type["DatadogTracingExtension"], Any]: fixture_name = request.param ddtrace_mock = request.getfixturevalue(fixture_name) from strawberry.extensions.tracing.datadog import DatadogTracingExtensionSync return DatadogTracingExtensionSync, ddtrace_mock @strawberry.type class Person: name: str = "Jack" @strawberry.type class Query: @strawberry.field def person(self) -> Person: return Person() @strawberry.field async def person_async(self) -> Person: return Person() @strawberry.type class Mutation: @strawberry.mutation def say_hi(self) -> str: return "hello" @strawberry.type class Subscription: @strawberry.field async def on_hi(self) -> AsyncGenerator[str, None]: yield "Hello" # TODO: this test could be improved by passing a custom tracer to the datadog extension # and maybe we could unify datadog and opentelemetry extensions by doing that @pytest.mark.asyncio async def test_datadog_tracer(datadog_extension, mocker): extension, mock = datadog_extension schema = strawberry.Schema( query=Query, mutation=Mutation, extensions=[extension], ) query = """ query { personAsync { name } } """ await schema.execute(query) mock.tracer.assert_has_calls( [ mocker.call.trace( "Anonymous Query", resource="63a280256ca4e8514e06cf90b30c8c3a", span_type="graphql", service="strawberry", ), mocker.call.trace().set_tag("graphql.operation_name", None), mocker.call.trace().set_tag("graphql.operation_type", "query"), mocker.call.trace("Parsing", span_type="graphql"), mocker.call.trace().finish(), mocker.call.trace("Validation", span_type="graphql"), mocker.call.trace().finish(), mocker.call.trace("Resolving: Query.personAsync", span_type="graphql"), mocker.call.trace().__enter__(), mocker.call.trace() .__enter__() .set_tag("graphql.field_name", "personAsync"), mocker.call.trace().__enter__().set_tag("graphql.parent_type", "Query"), mocker.call.trace() .__enter__() .set_tag("graphql.field_path", "Query.personAsync"), mocker.call.trace().__enter__().set_tag("graphql.path", "personAsync"), mocker.call.trace().__exit__(None, None, None), mocker.call.trace().finish(), ] ) @pytest.mark.asyncio async def test_uses_operation_name_and_hash(datadog_extension): extension, mock = datadog_extension schema = strawberry.Schema(query=Query, mutation=Mutation, extensions=[extension]) query = """ query MyExampleQuery { person { name } } """ await schema.execute(query, operation_name="MyExampleQuery") mock.tracer.trace.assert_any_call( "MyExampleQuery", resource="MyExampleQuery:efe8d7247ee8136f45e3824c2768b155", span_type="graphql", service="strawberry", ) @pytest.mark.asyncio async def test_uses_operation_type(datadog_extension): extension, mock = datadog_extension schema = strawberry.Schema(query=Query, mutation=Mutation, extensions=[extension]) query = """ mutation MyMutation { sayHi } """ await schema.execute(query, operation_name="MyMutation") mock.tracer.trace().set_tag.assert_any_call("graphql.operation_type", "mutation") @pytest.mark.asyncio async def test_uses_operation_subscription(datadog_extension): extension, mock = datadog_extension schema = strawberry.Schema(query=Query, mutation=Mutation, extensions=[extension]) query = """ subscription MySubscription { onHi } """ await schema.execute(query, operation_name="MySubscription") mock.tracer.trace().set_tag.assert_any_call( "graphql.operation_type", "subscription" ) def test_datadog_tracer_sync(datadog_extension_sync, mocker): extension, mock = datadog_extension_sync schema = strawberry.Schema(query=Query, mutation=Mutation, extensions=[extension]) query = """ query { person { name } } """ schema.execute_sync(query) mock.tracer.assert_has_calls( [ mocker.call.trace( "Anonymous Query", resource="659edba9e6ac9c20d03da1b2d0f9a956", span_type="graphql", service="strawberry", ), mocker.call.trace().set_tag("graphql.operation_name", None), mocker.call.trace().set_tag("graphql.operation_type", "query"), mocker.call.trace("Parsing", span_type="graphql"), mocker.call.trace().finish(), mocker.call.trace("Validation", span_type="graphql"), mocker.call.trace().finish(), mocker.call.trace("Resolving: Query.person", span_type="graphql"), mocker.call.trace().__enter__(), mocker.call.trace().__enter__().set_tag("graphql.field_name", "person"), mocker.call.trace().__enter__().set_tag("graphql.parent_type", "Query"), mocker.call.trace() .__enter__() .set_tag("graphql.field_path", "Query.person"), mocker.call.trace().__enter__().set_tag("graphql.path", "person"), mocker.call.trace().__exit__(None, None, None), mocker.call.trace().finish(), ] ) def test_uses_operation_name_and_hash_sync(datadog_extension_sync): extension, mock = datadog_extension_sync schema = strawberry.Schema(query=Query, mutation=Mutation, extensions=[extension]) query = """ query MyExampleQuery { person { name } } """ schema.execute_sync(query, operation_name="MyExampleQuery") mock.tracer.trace.assert_any_call( "MyExampleQuery", resource="MyExampleQuery:efe8d7247ee8136f45e3824c2768b155", span_type="graphql", service="strawberry", ) def test_uses_operation_type_sync(datadog_extension_sync): extension, mock = datadog_extension_sync schema = strawberry.Schema(query=Query, mutation=Mutation, extensions=[extension]) query = """ mutation MyMutation { sayHi } """ schema.execute_sync(query, operation_name="MyMutation") mock.tracer.trace().set_tag.assert_any_call("graphql.operation_type", "mutation") @pytest.mark.asyncio async def test_create_span_override(datadog_extension): from strawberry.extensions.tracing.datadog import LifecycleStep extension, mock = datadog_extension class CustomExtension(extension): def create_span( self, lifecycle_step: LifecycleStep, name: str, **kwargs, # noqa: ANN003 ): span = super().create_span(lifecycle_step, name, **kwargs) if lifecycle_step == LifecycleStep.OPERATION: span.set_tag("graphql.query", self.execution_context.query) return span schema = strawberry.Schema( query=Query, mutation=Mutation, extensions=[CustomExtension], ) query = """ query { personAsync { name } } """ await schema.execute(query) mock.tracer.trace().set_tag.assert_any_call("graphql.query", query) @pytest.mark.asyncio async def test_uses_query_missing_operation_if_no_query(datadog_extension, mocker): """Avoid regression of https://github.com/strawberry-graphql/strawberry/issues/3150""" extension, mock = datadog_extension schema = strawberry.Schema(query=Query, mutation=Mutation, extensions=[extension]) # A missing query error is expected here, but the extension will run anyways with pytest.raises(strawberry.exceptions.MissingQueryError): await schema.execute(None) mock.tracer.assert_has_calls( [ mocker.call.trace( "Anonymous Query", resource="query_missing", span_type="graphql", service="strawberry", ), mocker.call.trace().set_tag("graphql.operation_name", None), mocker.call.trace().set_tag("graphql.operation_type", "query_missing"), ] ) strawberry-graphql-0.287.0/tests/schema/extensions/test_disable_introspection.py000066400000000000000000000020561511033167500303320ustar00rootroot00000000000000import strawberry from strawberry.extensions import DisableIntrospection def test_disables_introspection(): @strawberry.type class Query: hello: str schema = strawberry.Schema( query=Query, extensions=[DisableIntrospection()], ) result = schema.execute_sync("query { __schema { __typename } }") assert result.data is None assert result.errors is not None formatted_errors = [err.formatted for err in result.errors] assert formatted_errors == [ { "message": "GraphQL introspection has been disabled, but the requested query contained the field '__schema'.", "locations": [{"line": 1, "column": 9}], } ] def test_does_not_affect_non_introspection_queries(): @strawberry.type class Query: hello: str schema = strawberry.Schema( query=Query, extensions=[DisableIntrospection()], ) result = schema.execute_sync("query { __typename }") assert result.data == {"__typename": "Query"} assert result.errors is None strawberry-graphql-0.287.0/tests/schema/extensions/test_field_extensions.py000066400000000000000000000302201511033167500273030ustar00rootroot00000000000000import re from collections.abc import Callable from typing import Annotated, Any import pytest import strawberry from strawberry.extensions.field_extension import ( AsyncExtensionResolver, FieldExtension, SyncExtensionResolver, ) from strawberry.schema.config import StrawberryConfig class UpperCaseExtension(FieldExtension): def resolve( self, next_: Callable[..., Any], source: Any, info: strawberry.Info, **kwargs: Any, ): result = next_(source, info, **kwargs) return str(result).upper() class LowerCaseExtension(FieldExtension): def resolve( self, next_: Callable[..., Any], source: Any, info: strawberry.Info, **kwargs: Any, ): result = next_(source, info, **kwargs) return str(result).lower() class AsyncUpperCaseExtension(FieldExtension): async def resolve_async( self, next_: AsyncExtensionResolver, source: Any, info: strawberry.Info, **kwargs: Any, ): result = await next_(source, info, **kwargs) return str(result).upper() class IdentityExtension(FieldExtension): def resolve( self, next_: SyncExtensionResolver, source: Any, info: strawberry.Info, **kwargs: Any, ) -> Any: return next_(source, info, **kwargs) async def resolve_async( self, next_: AsyncExtensionResolver, source: Any, info: strawberry.Info, **kwargs: Any, ) -> Any: return await next_(source, info, **kwargs) def test_extension_argument_modification(): @strawberry.type class Query: @strawberry.field(extensions=[UpperCaseExtension()]) def string(self) -> str: return "This is a test!!" schema = strawberry.Schema(query=Query) query = "query { string }" result = schema.execute_sync(query) # The result should be lowercase because that is the last extension in the chain assert result.data["string"] == "THIS IS A TEST!!" def test_extension_result_modification_sync(): @strawberry.type class Query: @strawberry.field(extensions=[UpperCaseExtension()]) def string(self) -> str: return "This is a test!!" schema = strawberry.Schema(query=Query) query = "query { string }" result = schema.execute_sync(query) # The result should be lowercase because that is the last extension in the chain assert result.data["string"] == "THIS IS A TEST!!" async def test_async_extension_on_sync_resolver(): @strawberry.type class Query: @strawberry.field(extensions=[AsyncUpperCaseExtension()]) def string(self) -> str: return "This is a test!!" schema = strawberry.Schema(query=Query) query = "query { string }" result = await schema.execute(query) # The result should be lowercase because that is the last extension in the chain assert result.data["string"] == "THIS IS A TEST!!" async def test_extension_result_modification_async(): @strawberry.type class Query: @strawberry.field(extensions=[AsyncUpperCaseExtension()]) async def string(self) -> str: return "This is a test!!" schema = strawberry.Schema(query=Query) query = "query { string }" result = await schema.execute(query) # The result should be lowercase because that is the last extension in the chain assert result.data["string"] == "THIS IS A TEST!!" def test_fail_cannot_use_async_before_sync_extensions(): @strawberry.type class Query: @strawberry.field(extensions=[AsyncUpperCaseExtension(), LowerCaseExtension()]) def string(self) -> str: return "This is a test!!" # LowerCaseExtension should work just fine because it's sync msg = ( "Query fields cannot be resolved. Cannot mix async-only extension(s) " "AsyncUpperCaseExtension with sync-only extension(s) " "LowerCaseExtension on Field string. " "If possible try to change the execution order so that all sync-only " "extensions are executed first." ) with pytest.raises(TypeError, match=re.escape(msg)): strawberry.Schema(query=Query) async def test_can_use_sync_before_async_extensions(): @strawberry.type class Query: @strawberry.field(extensions=[LowerCaseExtension(), AsyncUpperCaseExtension()]) def string(self) -> str: return "This is a test!!" schema = strawberry.Schema(query=Query) query = "query { string }" result = await schema.execute(query) # The result should be lowercase because that is the last extension in the chain assert result.data["string"] == "THIS IS A TEST!!" async def test_can_use_sync_only_and_sync_before_async_extensions(): """Use Sync - Sync + Async - Sync - Async possible.""" @strawberry.type class Query: @strawberry.field( extensions=[ LowerCaseExtension(), IdentityExtension(), LowerCaseExtension(), AsyncUpperCaseExtension(), ] ) def string(self) -> str: return "This is a test!!" schema = strawberry.Schema(query=Query) query = "query { string }" result = await schema.execute(query) # The result should be lowercase because that is the last extension in the chain assert result.data["string"] == "THIS IS A TEST!!" def test_fail_on_missing_async_extensions(): class LowerCaseExtension(FieldExtension): def resolve( self, next_: Callable[..., Any], source: Any, info: strawberry.Info, **kwargs: Any, ): result = next_(source, info, **kwargs) return str(result).lower() class UpperCaseExtension(FieldExtension): async def resolve_async( self, next_: Callable[..., Any], source: Any, info: strawberry.Info, **kwargs: Any, ): result = await next_(source, info, **kwargs) return str(result).upper() @strawberry.type class Query: @strawberry.field(extensions=[UpperCaseExtension(), LowerCaseExtension()]) async def string(self) -> str: return "This is a test!!" # UpperCaseExtension should work just fine because it's sync msg = ( "Query fields cannot be resolved. Cannot add sync-only extension(s) " "LowerCaseExtension to the async resolver of Field string. " "Please add a resolve_async method to the extension(s)." ) with pytest.raises(TypeError, match=re.escape(msg)): strawberry.Schema(query=Query) def test_extension_order_respected(): class LowerCaseExtension(FieldExtension): def resolve( self, next_: Callable[..., Any], source: Any, info: strawberry.Info, **kwargs: Any, ): result = next_(source, info, **kwargs) return str(result).lower() class UpperCaseExtension(FieldExtension): def resolve( self, next_: Callable[..., Any], source: Any, info: strawberry.Info, **kwargs: Any, ): result = next_(source, info, **kwargs) return str(result).upper() @strawberry.type class Query: @strawberry.field(extensions=[UpperCaseExtension(), LowerCaseExtension()]) def string(self) -> str: return "This is a test!!" schema = strawberry.Schema(query=Query) query = "query { string }" result = schema.execute_sync(query) # The result should be lowercase because that is the last extension in the chain assert result.data["string"] == "this is a test!!" def test_extension_argument_parsing(): """Check that kwargs passed to field extensions have been converted into Strawberry types. """ @strawberry.input class StringInput: some_input_value: str = strawberry.field(description="foo") field_kwargs = {} class CustomExtension(FieldExtension): def resolve( self, next_: Callable[..., Any], source: Any, info: strawberry.Info, **kwargs: Any, ): nonlocal field_kwargs field_kwargs = kwargs return next_(source, info, **kwargs) @strawberry.type class Query: @strawberry.field(extensions=[CustomExtension()]) def string(self, some_input: StringInput) -> str: return f"This is a test!! {some_input.some_input_value}" schema = strawberry.Schema(query=Query) query = 'query { string(someInput: { someInputValue: "foo" }) }' result = schema.execute_sync(query) assert result.data, result.errors assert result.data["string"] == "This is a test!! foo" assert isinstance(field_kwargs["some_input"], StringInput) input_value = field_kwargs["some_input"] assert input_value.some_input_value == "foo" assert input_value.__strawberry_definition__.is_input is True def test_extension_mutate_arguments(): class CustomExtension(FieldExtension): def resolve( self, next_: Callable[..., Any], source: Any, info: strawberry.Info, **kwargs: Any, ): kwargs["some_input"] += 10 return next_(source, info, **kwargs) @strawberry.type class Query: @strawberry.field(extensions=[CustomExtension()]) def string(self, some_input: int) -> str: return f"This is a test!! {some_input}" schema = strawberry.Schema(query=Query) query = "query { string(someInput: 3) }" result = schema.execute_sync(query) assert result.data, result.errors assert result.data["string"] == "This is a test!! 13" def test_extension_access_argument_metadata(): field_kwargs = {} argument_metadata = {} class CustomExtension(FieldExtension): def resolve( self, next_: Callable[..., Any], source: Any, info: strawberry.Info, **kwargs: Any, ): nonlocal field_kwargs field_kwargs = kwargs for key in kwargs: argument_def = info.get_argument_definition(key) assert argument_def is not None argument_metadata[key] = argument_def.metadata return next_(source, info, **kwargs) @strawberry.type class Query: @strawberry.field(extensions=[CustomExtension()]) def string( self, some_input: Annotated[str, strawberry.argument(metadata={"test": "foo"})], another_input: str | None = None, ) -> str: return f"This is a test!! {some_input}" schema = strawberry.Schema(query=Query) query = 'query { string(someInput: "foo") }' result = schema.execute_sync(query) assert result.data, result.errors assert result.data["string"] == "This is a test!! foo" assert isinstance(field_kwargs["some_input"], str) assert argument_metadata == { "some_input": { "test": "foo", }, "another_input": {}, } def test_extension_has_custom_info_class(): class CustomInfo(strawberry.Info): test: str = "foo" class CustomExtension(FieldExtension): def resolve( self, next_: Callable[..., Any], source: Any, info: CustomInfo, **kwargs: Any, ): assert isinstance(info, CustomInfo) # Explicitly check it's not Info. assert strawberry.Info in type(info).__bases__ assert info.test == "foo" return next_(source, info, **kwargs) @strawberry.type class Query: @strawberry.field(extensions=[CustomExtension()]) def string(self) -> str: return "This is a test!!" schema = strawberry.Schema( query=Query, config=StrawberryConfig(info_class=CustomInfo) ) query = "query { string }" result = schema.execute_sync(query) assert result.data, result.errors assert result.data["string"] == "This is a test!!" strawberry-graphql-0.287.0/tests/schema/extensions/test_imports.py000066400000000000000000000005111511033167500254360ustar00rootroot00000000000000import pytest def test_can_import(mocker): # mocking sys.modules.ddtrace so we don't get an ImportError mocker.patch.dict("sys.modules", ddtrace=mocker.MagicMock()) def test_fails_if_import_is_not_found(): with pytest.raises(ImportError): from strawberry.extensions.tracing import Blueberry # noqa: F401 strawberry-graphql-0.287.0/tests/schema/extensions/test_input_mutation.py000066400000000000000000000065501511033167500270310ustar00rootroot00000000000000import textwrap from typing import Annotated import strawberry from strawberry.field_extensions import InputMutationExtension from strawberry.schema_directive import Location, schema_directive @schema_directive( locations=[Location.FIELD_DEFINITION], name="some_directive", ) class SomeDirective: some: str directive: str @strawberry.type class Fruit: name: str color: str @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "hi" @strawberry.type class Mutation: @strawberry.mutation(extensions=[InputMutationExtension()]) def create_fruit( self, name: str, color: Annotated[ str, strawberry.argument( description="The color of the fruit", directives=[SomeDirective(some="foo", directive="bar")], ), ], ) -> Fruit: return Fruit( name=name, color=color, ) @strawberry.mutation(extensions=[InputMutationExtension()]) async def create_fruit_async( self, name: str, color: Annotated[str, object()], ) -> Fruit: return Fruit( name=name, color=color, ) schema = strawberry.Schema(query=Query, mutation=Mutation) def test_schema(): expected = ''' directive @some_directive(some: String!, directive: String!) on FIELD_DEFINITION input CreateFruitAsyncInput { name: String! color: String! } input CreateFruitInput { name: String! """The color of the fruit""" color: String! @some_directive(some: "foo", directive: "bar") } type Fruit { name: String! color: String! } type Mutation { createFruit( """Input data for `createFruit` mutation""" input: CreateFruitInput! ): Fruit! createFruitAsync( """Input data for `createFruitAsync` mutation""" input: CreateFruitAsyncInput! ): Fruit! } type Query { hello: String! } ''' assert str(schema).strip() == textwrap.dedent(expected).strip() def test_input_mutation(): result = schema.execute_sync( """ mutation TestQuery ($input: CreateFruitInput!) { createFruit (input: $input) { ... on Fruit { name color } } } """, variable_values={ "input": { "name": "Dragonfruit", "color": "red", } }, ) assert result.errors is None assert result.data == { "createFruit": { "name": "Dragonfruit", "color": "red", }, } async def test_input_mutation_async(): result = await schema.execute( """ mutation TestQuery ($input: CreateFruitAsyncInput!) { createFruitAsync (input: $input) { ... on Fruit { name color } } } """, variable_values={ "input": { "name": "Dragonfruit", "color": "red", } }, ) assert result.errors is None assert result.data == { "createFruitAsync": { "name": "Dragonfruit", "color": "red", }, } strawberry-graphql-0.287.0/tests/schema/extensions/test_input_mutation_federation.py000066400000000000000000000037751511033167500312370ustar00rootroot00000000000000import textwrap from typing import Annotated import strawberry from strawberry.field_extensions import InputMutationExtension @strawberry.federation.type class Fruit: name: str color: str @strawberry.federation.type class Query: @strawberry.field def hello(self) -> str: # pragma: no cover return "hi" @strawberry.federation.type class Mutation: @strawberry.federation.mutation(extensions=[InputMutationExtension()]) def create_fruit( self, name: str, color: Annotated[ str, strawberry.federation.argument( description="The color of the fruit", ), ], ) -> Fruit: return Fruit( name=name, color=color, ) schema = strawberry.federation.Schema(query=Query, mutation=Mutation) def test_schema(): expected = ''' input CreateFruitInput { name: String! """The color of the fruit""" color: String! } type Fruit { name: String! color: String! } type Mutation { createFruit( """Input data for `createFruit` mutation""" input: CreateFruitInput! ): Fruit! } type Query { _service: _Service! hello: String! } scalar _Any type _Service { sdl: String! } ''' assert str(schema).strip() == textwrap.dedent(expected).strip() def test_input_mutation(): result = schema.execute_sync( """ mutation TestQuery ($input: CreateFruitInput!) { createFruit (input: $input) { ... on Fruit { name color } } } """, variable_values={ "input": { "name": "Dragonfruit", "color": "red", } }, ) assert result.errors is None assert result.data == { "createFruit": { "name": "Dragonfruit", "color": "red", }, } strawberry-graphql-0.287.0/tests/schema/extensions/test_input_mutation_future.py000066400000000000000000000016241511033167500304200ustar00rootroot00000000000000from __future__ import annotations import textwrap from uuid import UUID import strawberry from strawberry.field_extensions import InputMutationExtension @strawberry.type class Query: @strawberry.field async def hello(self) -> str: return "hi" @strawberry.type class Mutation: @strawberry.mutation(extensions=[InputMutationExtension()]) async def buggy(self, some_id: UUID) -> None: del some_id def test_schema(): schema = strawberry.Schema(query=Query, mutation=Mutation) expected_schema = ''' input BuggyInput { someId: UUID! } type Mutation { buggy( """Input data for `buggy` mutation""" input: BuggyInput! ): Void } type Query { hello: String! } scalar UUID """Represents NULL values""" scalar Void ''' assert textwrap.dedent(expected_schema).strip() == str(schema).strip() strawberry-graphql-0.287.0/tests/schema/extensions/test_mask_errors.py000066400000000000000000000155421511033167500263020ustar00rootroot00000000000000from unittest.mock import Mock import pytest from graphql.error import GraphQLError import strawberry from strawberry.extensions import MaskErrors def test_mask_all_errors(): @strawberry.type class Query: @strawberry.field def hidden_error(self) -> str: raise KeyError("This error is not visible") schema = strawberry.Schema(query=Query, extensions=[MaskErrors()]) query = "query { hiddenError }" result = schema.execute_sync(query) assert result.errors is not None formatted_errors = [err.formatted for err in result.errors] assert formatted_errors == [ { "locations": [{"column": 9, "line": 1}], "message": "Unexpected error.", "path": ["hiddenError"], } ] @pytest.mark.asyncio async def test_mask_all_errors_async(): @strawberry.type class Query: @strawberry.field def hidden_error(self) -> str: raise KeyError("This error is not visible") schema = strawberry.Schema(query=Query, extensions=[MaskErrors()]) query = "query { hiddenError }" result = await schema.execute(query) assert result.errors is not None formatted_errors = [err.formatted for err in result.errors] assert formatted_errors == [ { "locations": [{"column": 9, "line": 1}], "message": "Unexpected error.", "path": ["hiddenError"], } ] def test_mask_some_errors(): class VisibleError(Exception): pass @strawberry.type class Query: @strawberry.field def visible_error(self) -> str: raise VisibleError("This error is visible") @strawberry.field def hidden_error(self) -> str: raise Exception("This error is not visible") def should_mask_error(error: GraphQLError) -> bool: original_error = error.original_error return not (original_error and isinstance(original_error, VisibleError)) schema = strawberry.Schema( query=Query, extensions=[MaskErrors(should_mask_error=should_mask_error)] ) query = "query { hiddenError }" result = schema.execute_sync(query) assert result.errors is not None formatted_errors = [err.formatted for err in result.errors] assert formatted_errors == [ { "locations": [{"column": 9, "line": 1}], "message": "Unexpected error.", "path": ["hiddenError"], } ] query = "query { visibleError }" result = schema.execute_sync(query) assert result.errors is not None formatted_errors = [err.formatted for err in result.errors] assert formatted_errors == [ { "locations": [{"column": 9, "line": 1}], "message": "This error is visible", "path": ["visibleError"], } ] def test_process_errors_original_error(): @strawberry.type class Query: @strawberry.field def hidden_error(self) -> str: raise ValueError("This error is not visible") mock_process_error = Mock() class CustomSchema(strawberry.Schema): def process_errors(self, errors, execution_context): for error in errors: mock_process_error(error) schema = CustomSchema(query=Query, extensions=[MaskErrors()]) query = "query { hiddenError }" result = schema.execute_sync(query) assert result.errors is not None formatted_errors = [err.formatted for err in result.errors] assert formatted_errors == [ { "locations": [{"column": 9, "line": 1}], "message": "Unexpected error.", "path": ["hiddenError"], } ] assert mock_process_error.call_count == 1 call = mock_process_error.call_args_list[0] assert call[0][0].message == "This error is not visible" assert isinstance(call[0][0].original_error, ValueError) def test_graphql_error_masking(): @strawberry.type class Query: @strawberry.field def graphql_error(self) -> str: return None # type: ignore schema = strawberry.Schema(query=Query, extensions=[MaskErrors()]) query = "query { graphqlError }" result = schema.execute_sync(query) assert result.errors is not None formatted_errors = [err.formatted for err in result.errors] assert formatted_errors == [ { "locations": [{"column": 9, "line": 1}], "message": "Unexpected error.", "path": ["graphqlError"], } ] @pytest.mark.asyncio async def test_mask_errors_with_strawberry_execution_result_async(): """Test that MaskErrors works correctly with async execution (which returns StrawberryExecutionResult).""" @strawberry.type class Query: @strawberry.field def test_field(self) -> str: raise ValueError("Original error message") schema = strawberry.Schema(query=Query, extensions=[MaskErrors()]) query = "query { testField }" # Use async execution to ensure we get StrawberryExecutionResult path result = await schema.execute(query) assert result.errors is not None formatted_errors = [err.formatted for err in result.errors] assert formatted_errors == [ { "locations": [{"column": 9, "line": 1}], "message": "Unexpected error.", "path": ["testField"], } ] @pytest.mark.asyncio async def test_mask_errors_selective_async(): """Test selective error masking with async execution.""" class VisibleError(Exception): pass @strawberry.type class Query: @strawberry.field def visible_error(self) -> str: raise VisibleError("This error is visible") @strawberry.field def hidden_error(self) -> str: raise ValueError("This error is not visible") def should_mask_error(error: GraphQLError) -> bool: original_error = error.original_error return not (original_error and isinstance(original_error, VisibleError)) schema = strawberry.Schema( query=Query, extensions=[MaskErrors(should_mask_error=should_mask_error)] ) # Test hidden error query = "query { hiddenError }" result = await schema.execute(query) assert result.errors is not None formatted_errors = [err.formatted for err in result.errors] assert formatted_errors == [ { "locations": [{"column": 9, "line": 1}], "message": "Unexpected error.", "path": ["hiddenError"], } ] # Test visible error query = "query { visibleError }" result = await schema.execute(query) assert result.errors is not None formatted_errors = [err.formatted for err in result.errors] assert formatted_errors == [ { "locations": [{"column": 9, "line": 1}], "message": "This error is visible", "path": ["visibleError"], } ] strawberry-graphql-0.287.0/tests/schema/extensions/test_max_aliases.py000066400000000000000000000073421511033167500262400ustar00rootroot00000000000000import strawberry from strawberry.extensions.max_aliases import MaxAliasesLimiter @strawberry.type class Human: name: str email: str @strawberry.type class Query: @strawberry.field def user(self, name: str | None = None, email: str | None = None) -> Human: return Human(name="Jane Doe", email="jane@example.com") version: str user1: Human user2: Human user3: Human def test_2_aliases_same_content(): query = """ { matt: user(name: "matt") { email } matt_alias: user(name: "matt") { email } } """ result = _execute_with_max_aliases(query, 1) assert len(result.errors) == 1 assert result.errors[0].message == "2 aliases found. Allowed: 1" def test_2_aliases_different_content(): query = """ query read { matt: user(name: "matt") { email } matt_alias: user(name: "matt42") { email } } """ result = _execute_with_max_aliases(query, 1) assert len(result.errors) == 1 assert result.errors[0].message == "2 aliases found. Allowed: 1" def test_multiple_aliases_some_overlap_in_content(): query = """ query read { matt: user(name: "matt") { email } jane: user(name: "jane") { email } matt_alias: user(name: "matt") { email } } """ result = _execute_with_max_aliases(query, 1) assert len(result.errors) == 1 assert result.errors[0].message == "3 aliases found. Allowed: 1" def test_multiple_arguments(): query = """ query read { matt: user(name: "matt", email: "matt@example.com") { email } jane: user(name: "jane") { email } matt_alias: user(name: "matt", email: "matt@example.com") { email } } """ result = _execute_with_max_aliases(query, 1) assert len(result.errors) == 1 assert result.errors[0].message == "3 aliases found. Allowed: 1" def test_alias_in_nested_field(): query = """ query read { matt: user(name: "matt") { email_address: email } } """ result = _execute_with_max_aliases(query, 1) assert len(result.errors) == 1 assert result.errors[0].message == "2 aliases found. Allowed: 1" def test_alias_in_fragment(): query = """ fragment humanInfo on Human { email_address: email } query read { matt: user(name: "matt") { ...humanInfo } } """ result = _execute_with_max_aliases(query, 1) assert len(result.errors) == 1 assert result.errors[0].message == "2 aliases found. Allowed: 1" def test_2_top_level_1_nested(): query = """{ matt: user(name: "matt") { email_address: email } matt_alias: user(name: "matt") { email } } """ result = _execute_with_max_aliases(query, 2) assert len(result.errors) == 1 assert result.errors[0].message == "3 aliases found. Allowed: 2" def test_no_error_one_aliased_one_without(): query = """ { user(name: "matt") { email } matt_alias: user(name: "matt") { email } } """ result = _execute_with_max_aliases(query, 1) assert not result.errors def test_no_error_for_multiple_but_not_too_many_aliases(): query = """{ matt: user(name: "matt") { email } matt_alias: user(name: "matt") { email } } """ result = _execute_with_max_aliases(query, 2) assert not result.errors def _execute_with_max_aliases(query: str, max_alias_count: int): schema = strawberry.Schema( Query, extensions=[MaxAliasesLimiter(max_alias_count=max_alias_count)] ) return schema.execute_sync(query) strawberry-graphql-0.287.0/tests/schema/extensions/test_max_tokens.py000066400000000000000000000023301511033167500261120ustar00rootroot00000000000000import strawberry from strawberry.extensions.max_tokens import MaxTokensLimiter @strawberry.type class Human: name: str email: str @strawberry.type class Query: @strawberry.field def user(self, name: str | None = None, email: str | None = None) -> Human: return Human(name="Jane Doe", email="jane@example.com") version: str user1: Human user2: Human user3: Human def test_1_more_token_than_allowed(): query = """ { matt: user(name: "matt") { name email } } """ result = _execute_with_max_tokens(query, 13) assert len(result.errors) == 1 assert ( result.errors[0].message == "Syntax Error: Document contains more than 13 tokens. Parsing aborted." ) def test_no_errors_exactly_max_number_of_tokens(): query = """ { matt: user(name: "matt") { name } } """ result = _execute_with_max_tokens(query, 13) assert not result.errors assert result.data def _execute_with_max_tokens(query: str, max_token_count: int): schema = strawberry.Schema( Query, extensions=[MaxTokensLimiter(max_token_count=max_token_count)] ) return schema.execute_sync(query) strawberry-graphql-0.287.0/tests/schema/extensions/test_opentelemetry.py000066400000000000000000000140261511033167500266430ustar00rootroot00000000000000from typing import Any from unittest.mock import MagicMock import pytest from opentelemetry.trace import SpanKind from pytest_mock import MockerFixture import strawberry from strawberry.extensions.tracing.opentelemetry import ( OpenTelemetryExtension, OpenTelemetryExtensionSync, ) @pytest.fixture def global_tracer_mock(mocker: MockerFixture) -> MagicMock: return mocker.patch("strawberry.extensions.tracing.opentelemetry.trace.get_tracer") @strawberry.type class Person: name: str = "Jess" @strawberry.type class Query: @strawberry.field async def person(self) -> Person: return Person() @pytest.mark.asyncio async def test_opentelemetry_uses_global_tracer(global_tracer_mock): schema = strawberry.Schema(query=Query, extensions=[OpenTelemetryExtension]) query = """ query { person { name } } """ await schema.execute(query) global_tracer_mock.assert_called_once_with("strawberry", tracer_provider=None) @pytest.mark.asyncio async def test_opentelemetry_sync_uses_global_tracer(global_tracer_mock): schema = strawberry.Schema(query=Query, extensions=[OpenTelemetryExtensionSync]) query = """ query { person { name } } """ await schema.execute(query) global_tracer_mock.assert_called_once_with("strawberry", tracer_provider=None) def _instrumentation_stages(mocker, query): return [ mocker.call("GraphQL Query", kind=SpanKind.SERVER), mocker.call().set_attribute("component", "graphql"), mocker.call().set_attribute("query", query), mocker.call("GraphQL Parsing", context=mocker.ANY), mocker.call().end(), mocker.call("GraphQL Validation", context=mocker.ANY), mocker.call().end(), mocker.call().end(), ] @pytest.mark.asyncio async def test_open_tracing(global_tracer_mock, mocker): schema = strawberry.Schema(query=Query, extensions=[OpenTelemetryExtension]) query = """ query { person { name } } """ await schema.execute(query) # start_span is called by the Extension framework to instrument # phases of pre-request handling logic; parsing, validation, etc global_tracer_mock.return_value.start_span.assert_has_calls( _instrumentation_stages(mocker, query) ) # start_as_current_span is called at the very start of request handling # it is a context manager, all other spans are a child of this global_tracer_mock.return_value.start_as_current_span.assert_has_calls( [ mocker.call("GraphQL Resolving: person", context=mocker.ANY), mocker.call().__enter__(), mocker.call().__enter__().set_attribute("component", "graphql"), mocker.call().__enter__().set_attribute("graphql.parentType", "Query"), mocker.call().__enter__().set_attribute("graphql.path", "person"), mocker.call().__exit__(None, None, None), ] ) @pytest.mark.asyncio async def test_open_tracing_uses_operation_name(global_tracer_mock, mocker): schema = strawberry.Schema(query=Query, extensions=[OpenTelemetryExtension]) query = """ query Example { person { name } } """ await schema.execute(query, operation_name="Example") global_tracer_mock.return_value.start_span.assert_has_calls( [ # if operation_name is supplied it is added to this span's tag mocker.call("GraphQL Query: Example", kind=SpanKind.SERVER), *_instrumentation_stages(mocker, query)[1:], ] ) @pytest.mark.asyncio async def test_open_tracing_gets_operation_name(global_tracer_mock, mocker): schema = strawberry.Schema(query=Query, extensions=[OpenTelemetryExtension]) query = """ query Example { person { name } } """ tracers = [] def generate_trace(*args: str, **kwargs: Any): nonlocal tracers tracer = mocker.Mock() tracers.append(tracer) return tracer global_tracer_mock.return_value.start_span.side_effect = generate_trace await schema.execute(query) tracers[0].update_name.assert_has_calls( [ # if operation_name is supplied it is added to this span's tag mocker.call("GraphQL Query: Example"), ] ) @pytest.mark.asyncio async def test_tracing_add_kwargs(global_tracer_mock, mocker): @strawberry.type class Query: @strawberry.field def hi(self, name: str) -> str: return f"Hi {name}" schema = strawberry.Schema(query=Query, extensions=[OpenTelemetryExtension]) query = """ query { hi(name: "Patrick") } """ await schema.execute(query) global_tracer_mock.return_value.start_as_current_span.assert_has_calls( [ mocker.call().__enter__().set_attribute("graphql.parentType", "Query"), mocker.call().__enter__().set_attribute("graphql.path", "hi"), mocker.call().__enter__().set_attribute("graphql.param.name", "Patrick"), ] ) @pytest.mark.asyncio async def test_tracing_filter_kwargs(global_tracer_mock, mocker): def arg_filter(kwargs, info): return {"name": "[...]"} @strawberry.type class Query: @strawberry.field def hi(self, name: str) -> str: return f"Hi {name}" schema = strawberry.Schema( query=Query, extensions=[OpenTelemetryExtension(arg_filter=arg_filter)] ) query = """ query { hi(name: "Patrick") } """ await schema.execute(query) global_tracer_mock.return_value.start_as_current_span.assert_has_calls( [ mocker.call().__enter__().set_attribute("graphql.parentType", "Query"), mocker.call().__enter__().set_attribute("graphql.path", "hi"), mocker.call().__enter__().set_attribute("graphql.param.name", "[...]"), ] ) strawberry-graphql-0.287.0/tests/schema/extensions/test_parser_cache.py000066400000000000000000000110141511033167500263600ustar00rootroot00000000000000from unittest.mock import patch import pytest from graphql import SourceLocation, parse import strawberry from strawberry.extensions import MaxTokensLimiter, ParserCache @patch("strawberry.extensions.parser_cache.parse", wraps=parse) def test_parser_cache_extension(mock_parse): @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "world" @strawberry.field def ping(self) -> str: return "pong" schema = strawberry.Schema(query=Query, extensions=[ParserCache()]) query = "query { hello }" result = schema.execute_sync(query) assert not result.errors assert result.data == {"hello": "world"} assert mock_parse.call_count == 1 # Run query multiple times for _ in range(3): result = schema.execute_sync(query) assert not result.errors assert result.data == {"hello": "world"} # validate is still only called once assert mock_parse.call_count == 1 # Running a second query doesn't cache query2 = "query { ping }" result = schema.execute_sync(query2) assert not result.errors assert result.data == {"ping": "pong"} assert mock_parse.call_count == 2 @patch("strawberry.extensions.parser_cache.parse", wraps=parse) def test_parser_cache_extension_arguments(mock_parse): @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "world" @strawberry.field def ping(self) -> str: return "pong" schema = strawberry.Schema( query=Query, extensions=[MaxTokensLimiter(max_token_count=20), ParserCache()] ) query = "query { hello }" result = schema.execute_sync(query) assert not result.errors assert result.data == {"hello": "world"} mock_parse.assert_called_with("query { hello }", max_tokens=20) @patch("strawberry.extensions.parser_cache.parse", wraps=parse) def test_parser_cache_extension_syntax_error(mock_parse): @strawberry.type class Query: @strawberry.field def hello(self) -> str: # pragma: no cover return "world" schema = strawberry.Schema(query=Query, extensions=[ParserCache()]) query = "query { hello" result = schema.execute_sync(query) assert len(result.errors) == 1 assert result.errors[0].message == "Syntax Error: Expected Name, found ." assert result.errors[0].locations == [SourceLocation(line=1, column=14)] assert mock_parse.call_count == 1 @patch("strawberry.extensions.parser_cache.parse", wraps=parse) def test_parser_cache_extension_max_size(mock_parse): @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "world" @strawberry.field def ping(self) -> str: return "pong" schema = strawberry.Schema(query=Query, extensions=[ParserCache(maxsize=1)]) query = "query { hello }" for _ in range(2): result = schema.execute_sync(query) assert not result.errors assert result.data == {"hello": "world"} assert mock_parse.call_count == 1 query2 = "query { ping }" result = schema.execute_sync(query2) assert not result.errors assert mock_parse.call_count == 2 # running the first query again doesn't cache result = schema.execute_sync(query) assert not result.errors # validate is still only called once assert mock_parse.call_count == 3 @pytest.mark.asyncio @patch("strawberry.extensions.parser_cache.parse", wraps=parse) async def test_parser_cache_extension_async(mock_parse): @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "world" @strawberry.field def ping(self) -> str: return "pong" schema = strawberry.Schema(query=Query, extensions=[ParserCache()]) query = "query { hello }" result = await schema.execute(query) assert not result.errors assert result.data == {"hello": "world"} assert mock_parse.call_count == 1 # Run query multiple times for _ in range(3): result = await schema.execute(query) assert not result.errors assert result.data == {"hello": "world"} # validate is still only called once assert mock_parse.call_count == 1 # Running a second query doesn't cache query2 = "query { ping }" result = await schema.execute(query2) assert not result.errors assert result.data == {"ping": "pong"} assert mock_parse.call_count == 2 strawberry-graphql-0.287.0/tests/schema/extensions/test_query_depth_limiter.py000066400000000000000000000223451511033167500300300ustar00rootroot00000000000000import pytest from graphql import ( GraphQLError, get_introspection_query, parse, specified_rules, validate, ) import strawberry from strawberry.extensions import QueryDepthLimiter from strawberry.extensions.query_depth_limiter import ( IgnoreContext, ShouldIgnoreType, create_validator, ) @strawberry.interface class Pet: name: str owner: "Human" @strawberry.type class Cat(Pet): pass @strawberry.type class Dog(Pet): pass @strawberry.type class Address: street: str number: int city: str country: str @strawberry.type class Human: name: str email: str address: Address pets: list[Pet] @strawberry.input class Biography: name: str owner_name: str @strawberry.type class Query: @strawberry.field def user( self, name: str | None, id: int | None, age: float | None, is_cool: bool | None, ) -> Human: pass @strawberry.field def users(self, names: list[str] | None) -> list[Human]: pass @strawberry.field def cat(bio: Biography) -> Cat: pass version: str user1: Human user2: Human user3: Human schema = strawberry.Schema(Query) def run_query( query: str, max_depth: int, should_ignore: ShouldIgnoreType = None ) -> tuple[list[GraphQLError], dict[str, int] | None]: document = parse(query) result = None def callback(query_depths): nonlocal result result = query_depths validation_rule = create_validator(max_depth, should_ignore, callback) errors = validate( schema._schema, document, rules=(*specified_rules, validation_rule), ) return errors, result def test_should_count_depth_without_fragment(): query = """ query read0 { version } query read1 { version user { name } } query read2 { matt: user(name: "matt") { email } andy: user(name: "andy") { email address { city } } } query read3 { matt: user(name: "matt") { email } andy: user(name: "andy") { email address { city } pets { name owner { name } } } } """ expected = {"read0": 0, "read1": 1, "read2": 2, "read3": 3} errors, result = run_query(query, 10) assert not errors assert result == expected def test_should_count_with_fragments(): query = """ query read0 { ... on Query { version } } query read1 { version user { ... on Human { name } } } fragment humanInfo on Human { email } fragment petInfo on Pet { name owner { name } } query read2 { matt: user(name: "matt") { ...humanInfo } andy: user(name: "andy") { ...humanInfo address { city } } } query read3 { matt: user(name: "matt") { ...humanInfo } andy: user(name: "andy") { ... on Human { email } address { city } pets { ...petInfo } } } """ expected = {"read0": 0, "read1": 1, "read2": 2, "read3": 3} errors, result = run_query(query, 10) assert not errors assert result == expected def test_should_ignore_the_introspection_query(): errors, result = run_query(get_introspection_query(), 10) assert not errors assert result == {"IntrospectionQuery": 0} def test_should_catch_query_thats_too_deep(): query = """{ user { pets { owner { pets { owner { pets { name } } } } } } } """ errors, _result = run_query(query, 4) assert len(errors) == 1 assert errors[0].message == "'anonymous' exceeds maximum operation depth of 4" def test_should_raise_invalid_ignore(): with pytest.raises( TypeError, match=r"The `should_ignore` argument to `QueryDepthLimiter` must be a callable.", ): strawberry.Schema( Query, extensions=[QueryDepthLimiter(max_depth=10, should_ignore=True)] ) def test_should_ignore_field_by_name(): query = """ query read1 { user { address { city } } } query read2 { user1 { address { city } } user2 { address { city } } user3 { address { city } } } """ def should_ignore(ignore: IgnoreContext) -> bool: return ignore.field_name in ("user1", "user2", "user3") errors, result = run_query(query, 10, should_ignore=should_ignore) expected = {"read1": 2, "read2": 0} assert not errors assert result == expected def test_should_ignore_field_by_str_argument(): query = """ query read1 { user(name:"matt") { address { city } } } query read2 { user1 { address { city } } user2 { address { city } } user3 { address { city } } } """ def should_ignore(ignore: IgnoreContext) -> bool: return ignore.field_args.get("name") == "matt" errors, result = run_query(query, 10, should_ignore=should_ignore) expected = {"read1": 0, "read2": 2} assert not errors assert result == expected def test_should_ignore_field_by_int_argument(): query = """ query read1 { user(id:1) { address { city } } } query read2 { user1 { address { city } } user2 { address { city } } user3 { address { city } } } """ def should_ignore(ignore: IgnoreContext) -> bool: return ignore.field_args.get("id") == 1 errors, result = run_query(query, 10, should_ignore=should_ignore) expected = {"read1": 0, "read2": 2} assert not errors assert result == expected def test_should_ignore_field_by_float_argument(): query = """ query read1 { user(age:10.5) { address { city } } } query read2 { user1 { address { city } } user2 { address { city } } user3 { address { city } } } """ def should_ignore(ignore: IgnoreContext) -> bool: return ignore.field_args.get("age") == 10.5 errors, result = run_query(query, 10, should_ignore=should_ignore) expected = {"read1": 0, "read2": 2} assert not errors assert result == expected def test_should_ignore_field_by_bool_argument(): query = """ query read1 { user(isCool:false) { address { city } } } query read2 { user1 { address { city } } user2 { address { city } } user3 { address { city } } } """ def should_ignore(ignore: IgnoreContext) -> bool: return ignore.field_args.get("isCool") is False errors, result = run_query(query, 10, should_ignore=should_ignore) expected = {"read1": 0, "read2": 2} assert not errors assert result == expected def test_should_ignore_field_by_name_and_str_argument(): query = """ query read1 { user(name:"matt") { address { city } } } query read2 { user1 { address { city } } user2 { address { city } } user3 { address { city } } } """ def should_ignore(ignore: IgnoreContext) -> bool: return ignore.field_args.get("name") == "matt" errors, result = run_query(query, 10, should_ignore=should_ignore) expected = {"read1": 0, "read2": 2} assert not errors assert result == expected def test_should_ignore_field_by_list_argument(): query = """ query read1 { users(names:["matt","andy"]) { address { city } } } query read2 { user1 { address { city } } user2 { address { city } } user3 { address { city } } } """ def should_ignore(ignore: IgnoreContext) -> bool: return "matt" in ignore.field_args.get("names", []) errors, result = run_query(query, 10, should_ignore=should_ignore) expected = {"read1": 0, "read2": 2} assert not errors assert result == expected def test_should_ignore_field_by_object_argument(): query = """ query read1 { cat(bio:{ name:"Momo", ownerName:"Tommy" }) { name } } query read2 { user1 { address { city } } user2 { address { city } } user3 { address { city } } } """ def should_ignore(ignore: IgnoreContext) -> bool: return ignore.field_args.get("bio", {}).get("name") == "Momo" errors, result = run_query(query, 10, should_ignore=should_ignore) expected = {"read1": 0, "read2": 2} assert not errors assert result == expected def test_should_work_as_extension(): query = """{ user { pets { owner { pets { owner { pets { name } } } } } } } """ def should_ignore(ignore: IgnoreContext) -> bool: return False schema = strawberry.Schema( Query, extensions=[QueryDepthLimiter(max_depth=4, should_ignore=should_ignore)] ) result = schema.execute_sync(query) assert len(result.errors) == 1 assert ( result.errors[0].message == "'anonymous' exceeds maximum operation depth of 4" ) strawberry-graphql-0.287.0/tests/schema/extensions/test_validation_cache.py000066400000000000000000000065211511033167500272250ustar00rootroot00000000000000from unittest.mock import patch import pytest from graphql import validate import strawberry from strawberry.extensions import ValidationCache @patch("strawberry.schema.schema.validate", wraps=validate) def test_validation_cache_extension(mock_validate): @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "world" @strawberry.field def ping(self) -> str: return "pong" schema = strawberry.Schema(query=Query, extensions=[ValidationCache()]) query = "query { hello }" result = schema.execute_sync(query) assert not result.errors assert result.data == {"hello": "world"} assert mock_validate.call_count == 1 # Run query multiple times for _ in range(3): result = schema.execute_sync(query) assert not result.errors assert result.data == {"hello": "world"} # validate is still only called once assert mock_validate.call_count == 1 # Running a second query doesn't cache query2 = "query { ping }" result = schema.execute_sync(query2) assert not result.errors assert result.data == {"ping": "pong"} assert mock_validate.call_count == 2 @patch("strawberry.schema.schema.validate", wraps=validate) def test_validation_cache_extension_max_size(mock_validate): @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "world" @strawberry.field def ping(self) -> str: return "pong" schema = strawberry.Schema(query=Query, extensions=[ValidationCache(maxsize=1)]) query = "query { hello }" for _ in range(2): result = schema.execute_sync(query) assert not result.errors assert result.data == {"hello": "world"} assert mock_validate.call_count == 1 query2 = "query { ping }" result = schema.execute_sync(query2) assert not result.errors assert mock_validate.call_count == 2 # running the first query again doesn't cache result = schema.execute_sync(query) assert not result.errors # validate is still only called once assert mock_validate.call_count == 3 @pytest.mark.asyncio async def test_validation_cache_extension_async(): @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "world" @strawberry.field def ping(self) -> str: return "pong" schema = strawberry.Schema(query=Query, extensions=[ValidationCache()]) query = "query { hello }" with patch("strawberry.schema.schema.validate", wraps=validate) as mock_validate: result = await schema.execute(query) assert not result.errors assert result.data == {"hello": "world"} assert mock_validate.call_count == 1 # Run query multiple times for _ in range(3): result = await schema.execute(query) assert not result.errors assert result.data == {"hello": "world"} # validate is still only called once assert mock_validate.call_count == 1 # Running a second query doesn't cache query2 = "query { ping }" result = await schema.execute(query2) assert not result.errors assert result.data == {"ping": "pong"} assert mock_validate.call_count == 2 strawberry-graphql-0.287.0/tests/schema/test_annotated/000077500000000000000000000000001511033167500231505ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/schema/test_annotated/__init__.py000066400000000000000000000000001511033167500252470ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/schema/test_annotated/type_a.py000066400000000000000000000004711511033167500250050ustar00rootroot00000000000000from __future__ import annotations from typing import Annotated from uuid import UUID import strawberry @strawberry.type class Query: @strawberry.field def get_testing( self, info: strawberry.Info, id_: Annotated[UUID, strawberry.argument(name="id")], ) -> str | None: ... strawberry-graphql-0.287.0/tests/schema/test_annotated/type_b.py000066400000000000000000000004711511033167500250060ustar00rootroot00000000000000from __future__ import annotations from typing import Annotated from uuid import UUID import strawberry @strawberry.type class Query: @strawberry.field def get_testing( self, id_: Annotated[UUID, strawberry.argument(name="id")], info: strawberry.Info, ) -> str | None: ... strawberry-graphql-0.287.0/tests/schema/test_arguments.py000066400000000000000000000134771511033167500235660ustar00rootroot00000000000000import textwrap from textwrap import dedent from typing import Annotated import strawberry from strawberry.types.unset import UNSET def test_argument_descriptions(): @strawberry.type class Query: @strawberry.field def hello( # type: ignore name: Annotated[ str, strawberry.argument(description="Your name") ] = "Patrick", ) -> str: return f"Hi {name}" schema = strawberry.Schema(query=Query) assert str(schema) == dedent( '''\ type Query { hello( """Your name""" name: String! = "Patrick" ): String! }''' ) def test_argument_deprecation_reason(): @strawberry.type class Query: @strawberry.field def hello( # type: ignore name: Annotated[ str, strawberry.argument(deprecation_reason="Your reason") ] = "Patrick", ) -> str: return f"Hi {name}" schema = strawberry.Schema(query=Query) assert str(schema) == dedent( """\ type Query { hello(name: String! = "Patrick" @deprecated(reason: "Your reason")): String! }""" ) def test_argument_names(): @strawberry.input class HelloInput: name: str = strawberry.field(default="Patrick", description="Your name") @strawberry.type class Query: @strawberry.field def hello( self, input_: Annotated[HelloInput, strawberry.argument(name="input")] ) -> str: return f"Hi {input_.name}" schema = strawberry.Schema(query=Query) assert str(schema) == dedent( '''\ input HelloInput { """Your name""" name: String! = "Patrick" } type Query { hello(input: HelloInput!): String! }''' ) def test_argument_with_default_value_none(): @strawberry.type class Query: @strawberry.field def hello(self, name: str | None = None) -> str: return f"Hi {name}" schema = strawberry.Schema(query=Query) assert str(schema) == dedent( """\ type Query { hello(name: String = null): String! }""" ) def test_optional_argument_unset(): @strawberry.type class Query: @strawberry.field def hello(self, name: str | None = UNSET, age: int | None = UNSET) -> str: if name is UNSET: return "Hi there" return f"Hi {name}" schema = strawberry.Schema(query=Query) assert str(schema) == dedent( """\ type Query { hello(name: String, age: Int): String! }""" ) result = schema.execute_sync( """ query { hello } """ ) assert not result.errors assert result.data == {"hello": "Hi there"} def test_optional_input_field_unset(): @strawberry.input class TestInput: name: str | None = UNSET age: int | None = UNSET @strawberry.type class Query: @strawberry.field def hello(self, input: TestInput) -> str: if input.name is UNSET: return "Hi there" return f"Hi {input.name}" schema = strawberry.Schema(query=Query) assert ( str(schema) == dedent( """ type Query { hello(input: TestInput!): String! } input TestInput { name: String age: Int } """ ).strip() ) result = schema.execute_sync( """ query { hello(input: {}) } """ ) assert not result.errors assert result.data == {"hello": "Hi there"} def test_setting_metadata_on_argument(): field_definition = None @strawberry.type class Query: @strawberry.field def hello( self, info: strawberry.Info, input: Annotated[str, strawberry.argument(metadata={"test": "foo"})], ) -> str: nonlocal field_definition field_definition = info._field return f"Hi {input}" schema = strawberry.Schema(query=Query) result = schema.execute_sync( """ query { hello(input: "there") } """ ) assert not result.errors assert result.data == {"hello": "Hi there"} assert field_definition assert field_definition.arguments[0].metadata == { "test": "foo", } def test_argument_parse_order(): """Check early early exit from argument parsing due to finding ``info``. Reserved argument parsing, which interally also resolves annotations, exits early after detecting the ``info`` argumnent. As a result, the annotation of the ``id_`` argument in `tests.schema.test_annotated.type_a.Query` is never resolved. This results in `StrawberryArgument` not being able to detect that ``id_`` makes use of `typing.Annotated` and `strawberry.argument`. This behavior is fixed by by ensuring that `StrawberryArgument` makes use of the new `StrawberryAnnotation.evaluate` method instead of consuming the raw annotation. An added benefit of this fix is that by removing annotation resolving code from `StrawberryResolver` and making it a part of `StrawberryAnnotation`, it makes it possible for `StrawberryArgument` and `StrawberryResolver` to share the same type evaluation cache. Refer to: https://github.com/strawberry-graphql/strawberry/issues/2855 """ from tests.schema.test_annotated import type_a, type_b expected = """ type Query { getTesting(id: UUID!): String } scalar UUID """ schema_a = strawberry.Schema(type_a.Query) schema_b = strawberry.Schema(type_b.Query) assert str(schema_a) == str(schema_b) assert str(schema_a) == textwrap.dedent(expected).strip() strawberry-graphql-0.287.0/tests/schema/test_basic.py000066400000000000000000000323751511033167500226400ustar00rootroot00000000000000import textwrap from dataclasses import InitVar, dataclass from enum import Enum import pytest import strawberry from strawberry import ID from strawberry.scalars import Base64 from strawberry.schema_directive import Location from strawberry.types.base import StrawberryList def test_raises_exception_with_unsupported_types(): class SomeType: ... @strawberry.type class Query: example: SomeType with pytest.raises( TypeError, match=r"Query fields cannot be resolved. Unexpected type '.*'" ): strawberry.Schema(query=Query) def test_basic_schema(): @strawberry.type class Query: example: str = "Example" schema = strawberry.Schema(query=Query) query = "{ example }" result = schema.execute_sync(query, root_value=Query()) assert not result.errors assert result.data["example"] == "Example" def test_basic_schema_optional(): @strawberry.type class Query: example: str | None = None schema = strawberry.Schema(query=Query) query = "{ example }" result = schema.execute_sync(query, root_value=Query()) assert not result.errors assert result.data["example"] is None def test_basic_schema_types(): @strawberry.type class User: name: str @strawberry.type class Query: user: User | None = None schema = strawberry.Schema(query=Query) query = "{ user { name } }" result = schema.execute_sync(query, root_value=Query()) assert not result.errors assert result.data["user"] is None def test_does_camel_case_conversion(): @strawberry.type class Query: @strawberry.field def hello_world(self, query_param: str) -> str: return query_param schema = strawberry.Schema(query=Query) query = """{ helloWorld(queryParam: "hi") }""" result = schema.execute_sync(query) assert not result.errors assert result.data["helloWorld"] == "hi" def test_can_rename_fields(): @strawberry.type class Hello: value: str | None = strawberry.field(name="name") @strawberry.type class Query: @strawberry.field def hello(self) -> Hello: return Hello(value="hi") @strawberry.field(name="example1") def example(self, query_param: str) -> str: return query_param schema = strawberry.Schema(query=Query) query = """{ hello { name } example1(queryParam: "hi") }""" result = schema.execute_sync(query) assert not result.errors assert result.data["hello"]["name"] == "hi" assert result.data["example1"] == "hi" def test_type_description(): @strawberry.type(description="Decorator argument description") class TypeA: a: str @strawberry.type class Query: a: TypeA schema = strawberry.Schema(query=Query) query = """{ __type(name: "TypeA") { name description } }""" result = schema.execute_sync(query) assert not result.errors assert result.data["__type"] == { "name": "TypeA", "description": "Decorator argument description", } def test_field_description(): @strawberry.type class Query: a: str = strawberry.field(description="Example") @strawberry.field def b(self, id: int) -> str: return "I'm a resolver" @strawberry.field(description="Example C") def c(self, id: int) -> str: return "I'm a resolver" schema = strawberry.Schema(query=Query) query = """{ __type(name: "Query") { fields { name description } } }""" result = schema.execute_sync(query) assert not result.errors assert result.data["__type"]["fields"] == [ {"name": "a", "description": "Example"}, {"name": "b", "description": None}, {"name": "c", "description": "Example C"}, ] def test_field_deprecated_reason(): @strawberry.type class Query: a: str = strawberry.field(deprecation_reason="Deprecated A") @strawberry.field def b(self, id: int) -> str: return "I'm a resolver" @strawberry.field(deprecation_reason="Deprecated B") def c(self, id: int) -> str: return "I'm a resolver" schema = strawberry.Schema(query=Query) query = """{ __type(name: "Query") { fields(includeDeprecated: true) { name deprecationReason } } }""" result = schema.execute_sync(query) assert not result.errors assert result.data["__type"]["fields"] == [ {"name": "a", "deprecationReason": "Deprecated A"}, {"name": "b", "deprecationReason": None}, {"name": "c", "deprecationReason": "Deprecated B"}, ] def test_field_deprecated_reason_subscription(): @strawberry.type class Query: a: str @strawberry.type class Subscription: @strawberry.subscription(deprecation_reason="Deprecated A") def a(self) -> str: return "A" schema = strawberry.Schema(query=Query, subscription=Subscription) query = """{ __type(name: "Subscription") { fields(includeDeprecated: true) { name deprecationReason } } }""" result = schema.execute_sync(query) assert not result.errors assert result.data["__type"]["fields"] == [ {"name": "a", "deprecationReason": "Deprecated A"}, ] def test_enum_description(): @strawberry.enum(description="We love ice-creams") class IceCreamFlavour(Enum): VANILLA = "vanilla" STRAWBERRY = "strawberry" CHOCOLATE = "chocolate" @strawberry.enum class PizzaType(Enum): MARGHERITA = "margherita" @strawberry.type class Query: favorite_ice_cream: IceCreamFlavour = IceCreamFlavour.STRAWBERRY pizza: PizzaType = PizzaType.MARGHERITA schema = strawberry.Schema(query=Query) query = """{ iceCreamFlavour: __type(name: "IceCreamFlavour") { description enumValues { name description } } pizzas: __type(name: "PizzaType") { description } }""" result = schema.execute_sync(query) assert not result.errors assert result.data["iceCreamFlavour"]["description"] == "We love ice-creams" assert result.data["iceCreamFlavour"]["enumValues"] == [ {"name": "VANILLA", "description": None}, {"name": "STRAWBERRY", "description": None}, {"name": "CHOCOLATE", "description": None}, ] assert result.data["pizzas"]["description"] is None def test_enum_value_description(): @strawberry.enum class IceCreamFlavour(Enum): VANILLA = "vainilla" STRAWBERRY = strawberry.enum_value("strawberry", description="Our favourite.") CHOCOLATE = "chocolate" @strawberry.type class Query: favorite_ice_cream: IceCreamFlavour = IceCreamFlavour.STRAWBERRY schema = strawberry.Schema(query=Query) query = """{ iceCreamFlavour: __type(name: "IceCreamFlavour") { enumValues { name description } } }""" result = schema.execute_sync(query) assert not result.errors assert result.data["iceCreamFlavour"]["enumValues"] == [ {"name": "VANILLA", "description": None}, {"name": "STRAWBERRY", "description": "Our favourite."}, {"name": "CHOCOLATE", "description": None}, ] def test_parent_class_fields_are_inherited(): @strawberry.type class Parent: cheese: str = "swiss" @strawberry.field def friend(self) -> str: return "food" @strawberry.type class Schema(Parent): cake: str = "made_in_switzerland" @strawberry.field def hello_this_is(self) -> str: return "patrick" @strawberry.type class Query: @strawberry.field def example(self) -> Schema: return Schema() schema = strawberry.Schema(query=Query) query = "{ example { cheese, cake, friend, helloThisIs } }" result = schema.execute_sync(query) assert not result.errors assert result.data["example"]["cheese"] == "swiss" assert result.data["example"]["cake"] == "made_in_switzerland" assert result.data["example"]["friend"] == "food" assert result.data["example"]["helloThisIs"] == "patrick" def test_can_return_compatible_type(): """Test that we can return a different type that has the same fields, for example when returning a Django Model. """ @dataclass class Example: name: str @strawberry.type class Cheese: name: str @strawberry.type class Query: @strawberry.field def assortment(self) -> Cheese: return Example(name="Asiago") # type: ignore schema = strawberry.Schema(query=Query) query = """{ assortment { name } }""" result = schema.execute_sync(query) assert not result.errors assert result.data["assortment"]["name"] == "Asiago" def test_init_var(): @strawberry.type class Category: name: str id: InitVar[str] @strawberry.type class Query: @strawberry.field def category(self) -> Category: return Category(name="example", id="123") schema = strawberry.Schema(query=Query) query = "{ category { name } }" result = schema.execute_sync(query) assert not result.errors assert result.data["category"]["name"] == "example" def test_nested_types(): @strawberry.type class User: name: str @strawberry.type class Query: @strawberry.field def user(self) -> User: return User(name="Patrick") schema = strawberry.Schema(query=Query) query = "{ user { name } }" result = schema.execute_sync(query) assert not result.errors assert result.data["user"]["name"] == "Patrick" def test_multiple_fields_with_same_type(): @strawberry.type class User: name: str @strawberry.type class Query: me: User | None = None you: User | None = None schema = strawberry.Schema(query=Query) query = "{ me { name } you { name } }" result = schema.execute_sync(query, root_value=Query()) assert not result.errors assert result.data["me"] is None assert result.data["you"] is None def test_str_magic_method_prints_schema_sdl(): @strawberry.type class Query: example_bool: bool example_str: str = "Example" example_int: int = 1 schema = strawberry.Schema(query=Query) expected = """ type Query { exampleBool: Boolean! exampleStr: String! exampleInt: Int! } """ assert str(schema) == textwrap.dedent(expected).strip() assert " str: return "ABC" schema = strawberry.Schema( query=Query, config=StrawberryConfig(auto_camel_case=False) ) query = """ { __type(name: "Query") { fields { name } } } """ result = schema.execute_sync(query) assert not result.errors assert result.data["__type"]["fields"] == [{"name": "exampleField"}] def test_camel_case_is_on_by_default_arguments(): @strawberry.type class Query: @strawberry.field def example_field(self, example_input: str) -> str: return example_input schema = strawberry.Schema(query=Query) query = """ { __type(name: "Query") { fields { name args { name } } } } """ result = schema.execute_sync(query, root_value=Query()) assert not result.errors assert result.data["__type"]["fields"] == [ {"args": [{"name": "exampleInput"}], "name": "exampleField"} ] def test_can_turn_camel_case_off_arguments(): @strawberry.type class Query: @strawberry.field def example_field(self, example_input: str) -> str: return example_input schema = strawberry.Schema( query=Query, config=StrawberryConfig(auto_camel_case=False) ) query = """ { __type(name: "Query") { fields { name args { name } } } } """ result = schema.execute_sync(query, root_value=Query()) assert not result.errors assert result.data["__type"]["fields"] == [ {"args": [{"name": "example_input"}], "name": "example_field"} ] def test_can_turn_camel_case_off_arguments_conversion_works(): @strawberry.type class Query: @strawberry.field def example_field(self, example_input: str) -> str: return example_input schema = strawberry.Schema( query=Query, config=StrawberryConfig(auto_camel_case=False) ) query = """ { example_field(example_input: "Hello world") } """ result = schema.execute_sync(query, root_value=Query()) assert not result.errors assert result.data["example_field"] == "Hello world" strawberry-graphql-0.287.0/tests/schema/test_config.py000066400000000000000000000020001511033167500230020ustar00rootroot00000000000000import pytest from strawberry.schema.config import StrawberryConfig from strawberry.types.info import Info def test_config_post_init_auto_camel_case(): config = StrawberryConfig(auto_camel_case=True) assert config.name_converter.auto_camel_case is True def test_config_post_init_no_auto_camel_case(): config = StrawberryConfig(auto_camel_case=False) assert config.name_converter.auto_camel_case is False def test_config_post_init_info_class(): class CustomInfo(Info): test: str = "foo" config = StrawberryConfig(info_class=CustomInfo) assert config.info_class is CustomInfo assert config.info_class.test == "foo" def test_config_post_init_info_class_is_default(): config = StrawberryConfig() assert config.info_class is Info def test_config_post_init_info_class_is_not_subclass(): with pytest.raises(TypeError) as exc_info: StrawberryConfig(info_class=object) assert str(exc_info.value) == "`info_class` must be a subclass of strawberry.Info" strawberry-graphql-0.287.0/tests/schema/test_custom_scalar.py000066400000000000000000000040421511033167500244040ustar00rootroot00000000000000import base64 from typing import NewType import strawberry Base64Encoded = strawberry.scalar( NewType("Base64Encoded", bytes), serialize=base64.b64encode, parse_value=base64.b64decode, ) @strawberry.scalar(serialize=lambda x: 42, parse_value=lambda x: Always42()) class Always42: pass MyStr = strawberry.scalar(NewType("MyStr", str)) def test_custom_scalar_serialization(): @strawberry.type class Query: @strawberry.field def custom_scalar_field(self) -> Base64Encoded: return Base64Encoded(b"decoded value") schema = strawberry.Schema(Query) result = schema.execute_sync("{ customScalarField }") assert not result.errors assert base64.b64decode(result.data["customScalarField"]) == b"decoded value" def test_custom_scalar_deserialization(): @strawberry.type class Query: @strawberry.field def decode_base64(self, encoded: Base64Encoded) -> str: return bytes(encoded).decode("ascii") schema = strawberry.Schema(Query) encoded = Base64Encoded(base64.b64encode(b"decoded")) query = """query decode($encoded: Base64Encoded!) { decodeBase64(encoded: $encoded) }""" result = schema.execute_sync(query, variable_values={"encoded": encoded}) assert not result.errors assert result.data["decodeBase64"] == "decoded" def test_custom_scalar_decorated_class(): @strawberry.type class Query: @strawberry.field def answer(self) -> Always42: return Always42() schema = strawberry.Schema(Query) result = schema.execute_sync("{ answer }") assert not result.errors assert result.data["answer"] == 42 def test_custom_scalar_default_serialization(): @strawberry.type class Query: @strawberry.field def my_str(self, arg: MyStr) -> MyStr: return MyStr(str(arg) + "Suffix") schema = strawberry.Schema(Query) result = schema.execute_sync('{ myStr(arg: "value") }') assert not result.errors assert result.data["myStr"] == "valueSuffix" strawberry-graphql-0.287.0/tests/schema/test_dataloaders.py000066400000000000000000000016311511033167500240310ustar00rootroot00000000000000from dataclasses import dataclass import pytest import strawberry from strawberry.dataloader import DataLoader @pytest.mark.asyncio async def test_can_use_dataloaders(mocker): @dataclass class User: id: str async def idx(keys) -> list[User]: return [User(key) for key in keys] mock_loader = mocker.Mock(side_effect=idx) loader = DataLoader(load_fn=mock_loader) @strawberry.type class Query: @strawberry.field async def get_user(self, id: strawberry.ID) -> str: user = await loader.load(id) return user.id schema = strawberry.Schema(query=Query) query = """{ a: getUser(id: "1") b: getUser(id: "2") }""" result = await schema.execute(query) assert not result.errors assert result.data == { "a": "1", "b": "2", } mock_loader.assert_called_once_with(["1", "2"]) strawberry-graphql-0.287.0/tests/schema/test_directives.py000066400000000000000000000464151511033167500237200ustar00rootroot00000000000000import textwrap from enum import Enum from typing import Any, NoReturn import pytest import strawberry from strawberry import Info from strawberry.directive import DirectiveLocation, DirectiveValue from strawberry.extensions import SchemaExtension from strawberry.schema.config import StrawberryConfig from strawberry.types.base import get_object_definition from strawberry.utils.await_maybe import await_maybe def test_supports_default_directives(): @strawberry.type class Person: name: str = "Jess" points: int = 2000 @strawberry.type class Query: @strawberry.field def person(self) -> Person: return Person() query = """query ($includePoints: Boolean!){ person { name points @include(if: $includePoints) } }""" schema = strawberry.Schema(query=Query) result = schema.execute_sync( query, variable_values={"includePoints": False}, context_value={"username": "foo"}, ) assert not result.errors assert result.data assert result.data["person"] == {"name": "Jess"} query = """query ($skipPoints: Boolean!){ person { name points @skip(if: $skipPoints) } }""" schema = strawberry.Schema(query=Query) result = schema.execute_sync(query, variable_values={"skipPoints": False}) assert not result.errors assert result.data assert result.data["person"] == {"name": "Jess", "points": 2000} @pytest.mark.asyncio async def test_supports_default_directives_async(): @strawberry.type class Person: name: str = "Jess" points: int = 2000 @strawberry.type class Query: @strawberry.field def person(self) -> Person: return Person() query = """query ($includePoints: Boolean!){ person { name points @include(if: $includePoints) } }""" schema = strawberry.Schema(query=Query) result = await schema.execute(query, variable_values={"includePoints": False}) assert not result.errors assert result.data assert result.data["person"] == {"name": "Jess"} query = """query ($skipPoints: Boolean!){ person { name points @skip(if: $skipPoints) } }""" schema = strawberry.Schema(query=Query) result = await schema.execute(query, variable_values={"skipPoints": False}) assert not result.errors assert result.data assert result.data["person"] == {"name": "Jess", "points": 2000} def test_can_declare_directives(): @strawberry.type class Query: @strawberry.field def cake(self) -> str: return "made_in_switzerland" @strawberry.directive( locations=[DirectiveLocation.FIELD], description="Make string uppercase" ) def uppercase(value: DirectiveValue[str], example: str): return value.upper() schema = strawberry.Schema(query=Query, directives=[uppercase]) expected_schema = ''' """Make string uppercase""" directive @uppercase(example: String!) on FIELD type Query { cake: String! } ''' assert schema.as_str() == textwrap.dedent(expected_schema).strip() result = schema.execute_sync('query { cake @uppercase(example: "foo") }') assert result.errors is None assert result.data == {"cake": "MADE_IN_SWITZERLAND"} def test_directive_arguments_without_value_param(): """Regression test for Strawberry Issue #1666. https://github.com/strawberry-graphql/strawberry/issues/1666 """ @strawberry.type class Query: cake: str = "victoria sponge" @strawberry.directive( locations=[DirectiveLocation.FIELD], description="Don't actually like cake? try ice cream instead", ) def ice_cream(flavor: str): return f"{flavor} ice cream" schema = strawberry.Schema(query=Query, directives=[ice_cream]) expected_schema = ''' """Don't actually like cake? try ice cream instead""" directive @iceCream(flavor: String!) on FIELD type Query { cake: String! } ''' assert schema.as_str() == textwrap.dedent(expected_schema).strip() query = 'query { cake @iceCream(flavor: "strawberry") }' result = schema.execute_sync(query, root_value=Query()) assert result.data == {"cake": "strawberry ice cream"} def test_runs_directives(): @strawberry.type class Person: name: str = "Jess" @strawberry.type class Query: @strawberry.field def person(self) -> Person: return Person() @strawberry.directive( locations=[DirectiveLocation.FIELD], description="Make string uppercase" ) def turn_uppercase(value: DirectiveValue[str]): return value.upper() @strawberry.directive(locations=[DirectiveLocation.FIELD]) def replace(value: DirectiveValue[str], old: str, new: str): return value.replace(old, new) schema = strawberry.Schema(query=Query, directives=[turn_uppercase, replace]) query = """query People($identified: Boolean!){ person { name @turnUppercase } jess: person { name @replace(old: "Jess", new: "Jessica") } johnDoe: person { name @replace(old: "Jess", new: "John") @include(if: $identified) } }""" result = schema.execute_sync(query, variable_values={"identified": False}) assert not result.errors assert result.data assert result.data["person"]["name"] == "JESS" assert result.data["jess"]["name"] == "Jessica" assert result.data["johnDoe"].get("name") is None def test_runs_directives_camel_case_off(): @strawberry.type class Person: name: str = "Jess" @strawberry.type class Query: @strawberry.field def person(self) -> Person: return Person() @strawberry.directive( locations=[DirectiveLocation.FIELD], description="Make string uppercase" ) def turn_uppercase(value: DirectiveValue[str]): return value.upper() @strawberry.directive(locations=[DirectiveLocation.FIELD]) def replace(value: DirectiveValue[str], old: str, new: str): return value.replace(old, new) schema = strawberry.Schema( query=Query, directives=[turn_uppercase, replace], config=StrawberryConfig(auto_camel_case=False), ) query = """query People($identified: Boolean!){ person { name @turn_uppercase } jess: person { name @replace(old: "Jess", new: "Jessica") } johnDoe: person { name @replace(old: "Jess", new: "John") @include(if: $identified) } }""" result = schema.execute_sync(query, variable_values={"identified": False}) assert not result.errors assert result.data assert result.data["person"]["name"] == "JESS" assert result.data["jess"]["name"] == "Jessica" assert result.data["johnDoe"].get("name") is None @pytest.mark.asyncio async def test_runs_directives_async(): @strawberry.type class Person: name: str = "Jess" @strawberry.type class Query: @strawberry.field def person(self) -> Person: return Person() @strawberry.directive( locations=[DirectiveLocation.FIELD], description="Make string uppercase" ) async def uppercase(value: DirectiveValue[str]): return value.upper() schema = strawberry.Schema(query=Query, directives=[uppercase]) query = """{ person { name @uppercase } }""" result = await schema.execute(query, variable_values={"identified": False}) assert not result.errors assert result.data assert result.data["person"]["name"] == "JESS" @pytest.mark.xfail def test_runs_directives_with_list_params(): @strawberry.type class Person: name: str = "Jess" @strawberry.type class Query: @strawberry.field def person(self) -> Person: return Person() @strawberry.directive(locations=[DirectiveLocation.FIELD]) def replace(value: DirectiveValue[str], old_list: list[str], new: str): for old in old_list: value = value.replace(old, new) return value schema = strawberry.Schema(query=Query, directives=[replace]) query = """query People { person { name @replace(oldList: ["J", "e", "s", "s"], new: "John") } }""" result = schema.execute_sync(query, variable_values={"identified": False}) assert not result.errors assert result.data assert result.data["person"]["name"] == "JESS" def test_runs_directives_with_extensions(): @strawberry.type class Person: name: str = "Jess" @strawberry.type class Query: @strawberry.field def person(self) -> Person: return Person() @strawberry.directive( locations=[DirectiveLocation.FIELD], description="Make string uppercase" ) def uppercase(value: DirectiveValue[str]): return value.upper() class ExampleExtension(SchemaExtension): def resolve(self, _next, root, info, *args: str, **kwargs: Any): return _next(root, info, *args, **kwargs) schema = strawberry.Schema( query=Query, directives=[uppercase], extensions=[ExampleExtension] ) query = """query { person { name @uppercase } }""" result = schema.execute_sync(query) assert not result.errors assert result.data assert result.data["person"]["name"] == "JESS" @pytest.mark.asyncio async def test_runs_directives_with_extensions_async(): @strawberry.type class Person: name: str = "Jess" @strawberry.type class Query: @strawberry.field async def person(self) -> Person: return Person() @strawberry.directive( locations=[DirectiveLocation.FIELD], description="Make string uppercase" ) def uppercase(value: DirectiveValue[str]): return value.upper() class ExampleExtension(SchemaExtension): async def resolve(self, _next, root, info, *args: str, **kwargs: Any): return await await_maybe(_next(root, info, *args, **kwargs)) schema = strawberry.Schema( query=Query, directives=[uppercase], extensions=[ExampleExtension] ) query = """query { person { name @uppercase } }""" result = await schema.execute(query) assert not result.errors assert result.data assert result.data["person"]["name"] == "JESS" @pytest.fixture def info_directive_schema() -> strawberry.Schema: """Returns a schema with directive that validates if info is recieved.""" @strawberry.enum class Locale(Enum): EN: str = "EN" NL: str = "NL" greetings: dict[Locale, str] = { Locale.EN: "Hello {username}", Locale.NL: "Hallo {username}", } @strawberry.type class Query: @strawberry.field def greeting_template(self, locale: Locale = Locale.EN) -> str: return greetings[locale] field = get_object_definition(Query, strict=True).fields[0] @strawberry.directive( locations=[DirectiveLocation.FIELD], description="Interpolate string on the server from context data", ) def interpolate(value: DirectiveValue[str], info: strawberry.Info): try: assert isinstance(info, strawberry.Info) assert info._field is field return value.format(**info.context["userdata"]) except KeyError: return value return strawberry.Schema(query=Query, directives=[interpolate]) def test_info_directive_schema(info_directive_schema: strawberry.Schema): expected_schema = ''' """Interpolate string on the server from context data""" directive @interpolate on FIELD enum Locale { EN NL } type Query { greetingTemplate(locale: Locale! = EN): String! } ''' assert textwrap.dedent(expected_schema).strip() == str(info_directive_schema) def test_info_directive(info_directive_schema: strawberry.Schema): query = "query { greetingTemplate @interpolate }" result = info_directive_schema.execute_sync( query, context_value={"userdata": {"username": "Foo"}} ) assert result.data == {"greetingTemplate": "Hello Foo"} @pytest.mark.asyncio async def test_info_directive_async(info_directive_schema: strawberry.Schema): query = "query { greetingTemplate @interpolate }" result = await info_directive_schema.execute( query, context_value={"userdata": {"username": "Foo"}} ) assert result.data == {"greetingTemplate": "Hello Foo"} def test_directive_value(): """Tests if directive value is detected by type instead of by arg-name `value`.""" @strawberry.type class Cake: frosting: str | None = None flavor: str = "Chocolate" @strawberry.type class Query: @strawberry.field def cake(self) -> Cake: return Cake() @strawberry.directive( locations=[DirectiveLocation.FIELD], description="Add frostring with ``flavor`` to a cake.", ) def add_frosting(flavor: str, v: DirectiveValue[Cake], value: str): assert isinstance(v, Cake) assert value == "foo" # Check if value can be used as an argument v.frosting = flavor return v schema = strawberry.Schema(query=Query, directives=[add_frosting]) result = schema.execute_sync( """query { cake @addFrosting(flavor: "Vanilla", value: "foo") { frosting flavor } } """ ) assert result.data == {"cake": {"frosting": "Vanilla", "flavor": "Chocolate"}} # Defined in module scope to allow the FowardRef to be resolvable with eval @strawberry.directive( locations=[DirectiveLocation.FIELD], description="Add frostring with ``flavor`` to a cake.", ) def add_frosting(flavor: str, v: DirectiveValue["Cake"], value: str) -> "Cake": assert isinstance(v, Cake) assert value == "foo" v.frosting = flavor return v @strawberry.type class Query: @strawberry.field def cake(self) -> "Cake": return Cake() @strawberry.type class Cake: frosting: str | None = None flavor: str = "Chocolate" def test_directive_value_forward_ref(): """Tests if directive value by type works with PEP-563.""" schema = strawberry.Schema(query=Query, directives=[add_frosting]) result = schema.execute_sync( """query { cake @addFrosting(flavor: "Vanilla", value: "foo") { frosting flavor } } """ ) assert result.data == {"cake": {"frosting": "Vanilla", "flavor": "Chocolate"}} def test_name_first_directive_value(): @strawberry.type class Query: @strawberry.field def greeting(self) -> str: return "Hi" @strawberry.directive(locations=[DirectiveLocation.FIELD]) def personalize_greeting(value: str, v: DirectiveValue[str]): assert v == "Hi" return f"{v} {value}" schema = strawberry.Schema(Query, directives=[personalize_greeting]) result = schema.execute_sync('{ greeting @personalizeGreeting(value: "Bar")}') assert result.data is not None assert not result.errors assert result.data["greeting"] == "Hi Bar" def test_named_based_directive_value_is_deprecated(): with pytest.deprecated_call(match=r"Argument name-based matching of 'value'"): @strawberry.type class Query: hello: str = "hello" @strawberry.directive(locations=[DirectiveLocation.FIELD]) def deprecated_value(value): ... strawberry.Schema(query=Query, directives=[deprecated_value]) @pytest.mark.asyncio async def test_directive_list_argument() -> NoReturn: @strawberry.type class Query: @strawberry.field def greeting(self) -> str: return "Hi" @strawberry.directive(locations=[DirectiveLocation.FIELD]) def append_names(value: DirectiveValue[str], names: list[str]): assert isinstance(names, list) return f"{value} {', '.join(names)}" schema = strawberry.Schema(query=Query, directives=[append_names]) result = await schema.execute( 'query { greeting @appendNames(names: ["foo", "bar"])}' ) assert result.errors is None assert result.data assert result.data["greeting"] == "Hi foo, bar" def test_directives_with_custom_types(): @strawberry.input class DirectiveInput: example: str @strawberry.type class Query: @strawberry.field def cake(self) -> str: return "made_in_switzerland" @strawberry.directive( locations=[DirectiveLocation.FIELD], description="Make string uppercase" ) def uppercase(value: DirectiveValue[str], input: DirectiveInput): return value.upper() schema = strawberry.Schema(query=Query, directives=[uppercase]) expected_schema = ''' """Make string uppercase""" directive @uppercase(input: DirectiveInput!) on FIELD input DirectiveInput { example: String! } type Query { cake: String! } ''' assert schema.as_str() == textwrap.dedent(expected_schema).strip() result = schema.execute_sync('query { cake @uppercase(input: { example: "foo" }) }') assert result.errors is None assert result.data == {"cake": "MADE_IN_SWITZERLAND"} def test_directives_with_scalar(): DirectiveInput = strawberry.scalar(str, name="DirectiveInput") @strawberry.type class Query: @strawberry.field def cake(self) -> str: return "made_in_switzerland" @strawberry.directive( locations=[DirectiveLocation.FIELD], description="Make string uppercase" ) def uppercase(value: DirectiveValue[str], input: DirectiveInput): return value.upper() schema = strawberry.Schema(query=Query, directives=[uppercase]) expected_schema = ''' """Make string uppercase""" directive @uppercase(input: DirectiveInput!) on FIELD scalar DirectiveInput type Query { cake: String! } ''' assert schema.as_str() == textwrap.dedent(expected_schema).strip() result = schema.execute_sync('query { cake @uppercase(input: "foo") }') assert result.errors is None assert result.data == {"cake": "MADE_IN_SWITZERLAND"} @pytest.mark.asyncio async def test_directive_with_custom_info_class() -> NoReturn: @strawberry.type class Query: @strawberry.field def greeting(self) -> str: return "Hi" class CustomInfo(Info): test: str = "foo" @strawberry.directive(locations=[DirectiveLocation.FIELD]) def append_names(value: DirectiveValue[str], names: list[str], info: CustomInfo): assert isinstance(names, list) assert isinstance(info, CustomInfo) assert Info in type(info).__bases__ # Explicitly check it's not Info. assert info.test == "foo" return f"{value} {', '.join(names)}" schema = strawberry.Schema( query=Query, directives=[append_names], config=StrawberryConfig(info_class=CustomInfo), ) result = await schema.execute( 'query { greeting @appendNames(names: ["foo", "bar"])}' ) assert result.errors is None assert result.data assert result.data["greeting"] == "Hi foo, bar" strawberry-graphql-0.287.0/tests/schema/test_duplicated_types.py000066400000000000000000000077121511033167500251160ustar00rootroot00000000000000import textwrap from enum import Enum from typing import Generic, TypeVar import pytest import strawberry from strawberry.exceptions import DuplicatedTypeName from strawberry.schema.config import StrawberryConfig @pytest.mark.raises_strawberry_exception( DuplicatedTypeName, match=r"Type (.*) is defined multiple times in the schema", ) def test_schema_has_no_duplicated_input_types(): @strawberry.input(name="DuplicatedInput") class A: a: int @strawberry.input(name="DuplicatedInput") class B: b: int @strawberry.type class Query: field: int strawberry.Schema(query=Query, types=[A, B]) @pytest.mark.raises_strawberry_exception( DuplicatedTypeName, match=r"Type (.*) is defined multiple times in the schema", ) def test_schema_has_no_duplicated_types(): @strawberry.type(name="DuplicatedType") class A: a: int @strawberry.type(name="DuplicatedType") class B: b: int @strawberry.type class Query: field: int strawberry.Schema(query=Query, types=[A, B]) @pytest.mark.raises_strawberry_exception( DuplicatedTypeName, match=r"Type (.*) is defined multiple times in the schema", ) def test_schema_has_no_duplicated_interfaces(): @strawberry.interface(name="DuplicatedType") class A: a: int @strawberry.interface(name="DuplicatedType") class B: b: int @strawberry.type class Query: pass strawberry.Schema(query=Query, types=[A, B]) @pytest.mark.raises_strawberry_exception( DuplicatedTypeName, match=r"Type (.*) is defined multiple times in the schema", ) def test_schema_has_no_duplicated_enums(): @strawberry.enum(name="DuplicatedType") class A(Enum): A = 1 @strawberry.enum(name="DuplicatedType") class B(Enum): B = 1 @strawberry.type class Query: field: int strawberry.Schema(query=Query, types=[A, B]) @pytest.mark.raises_strawberry_exception( DuplicatedTypeName, match=r"Type (.*) is defined multiple times in the schema", ) def test_schema_has_no_duplicated_names_across_different_types(): @strawberry.interface(name="DuplicatedType") class A: a: int @strawberry.type(name="DuplicatedType") class B: b: int @strawberry.type class Query: field: int strawberry.Schema(query=Query, types=[A, B]) @pytest.mark.raises_strawberry_exception( DuplicatedTypeName, match=r"Type (.*) is defined multiple times in the schema", ) def test_schema_has_no_duplicated_types_between_schema_and_extra_types(): @strawberry.type(name="DuplicatedType") class A: a: int @strawberry.type(name="DuplicatedType") class B: b: int @strawberry.type class Query: field: A strawberry.Schema(query=Query, types=[B]) def test_allows_multiple_instance_of_same_generic(): T = TypeVar("T") @strawberry.type class A(Generic[T]): a: T @strawberry.type class Query: first: A[int] second: A[int] schema = strawberry.Schema(Query) expected_schema = textwrap.dedent( """ type IntA { a: Int! } type Query { first: IntA! second: IntA! } """ ).strip() assert str(schema) == expected_schema def test_allows_duplicated_types_when_validation_disabled(): @strawberry.type(name="DuplicatedType") class A: a: int @strawberry.type(name="DuplicatedType") class B: b: int @strawberry.type class Query: field: int schema = strawberry.Schema( query=Query, types=[A, B], config=StrawberryConfig(_unsafe_disable_same_type_validation=True), ) expected_schema = textwrap.dedent( """ type DuplicatedType { a: Int! } type Query { field: Int! } """ ).strip() assert str(schema) == expected_schema strawberry-graphql-0.287.0/tests/schema/test_enum.py000066400000000000000000000311061511033167500225120ustar00rootroot00000000000000from enum import Enum from textwrap import dedent from typing import Annotated import pytest import strawberry from strawberry.types.lazy_type import lazy def test_enum_resolver(): @strawberry.enum class IceCreamFlavour(Enum): VANILLA = "vanilla" STRAWBERRY = "strawberry" CHOCOLATE = "chocolate" @strawberry.type class Query: @strawberry.field def best_flavour(self) -> IceCreamFlavour: return IceCreamFlavour.STRAWBERRY schema = strawberry.Schema(query=Query) query = "{ bestFlavour }" result = schema.execute_sync(query) assert not result.errors assert result.data["bestFlavour"] == "STRAWBERRY" @strawberry.type class Cone: flavour: IceCreamFlavour @strawberry.type class Query: @strawberry.field def cone(self) -> Cone: return Cone(flavour=IceCreamFlavour.STRAWBERRY) schema = strawberry.Schema(query=Query) query = "{ cone { flavour } }" result = schema.execute_sync(query) assert not result.errors assert result.data["cone"]["flavour"] == "STRAWBERRY" def test_enum_arguments(): @strawberry.enum class IceCreamFlavour(Enum): VANILLA = "vanilla" STRAWBERRY = "strawberry" CHOCOLATE = "chocolate" @strawberry.type class Query: @strawberry.field def flavour_available(self, flavour: IceCreamFlavour) -> bool: return flavour == IceCreamFlavour.STRAWBERRY @strawberry.input class ConeInput: flavour: IceCreamFlavour @strawberry.type class Mutation: @strawberry.mutation def eat_cone(self, input: ConeInput) -> bool: return input.flavour == IceCreamFlavour.STRAWBERRY schema = strawberry.Schema(query=Query, mutation=Mutation) query = "{ flavourAvailable(flavour: VANILLA) }" result = schema.execute_sync(query) assert not result.errors assert result.data["flavourAvailable"] is False query = "{ flavourAvailable(flavour: STRAWBERRY) }" result = schema.execute_sync(query) assert not result.errors assert result.data["flavourAvailable"] is True query = "mutation { eatCone(input: { flavour: VANILLA }) }" result = schema.execute_sync(query) assert not result.errors assert result.data["eatCone"] is False query = "mutation { eatCone(input: { flavour: STRAWBERRY }) }" result = schema.execute_sync(query) assert not result.errors assert result.data["eatCone"] is True def test_lazy_enum_arguments(): LazyEnum = Annotated[ "LazyEnum", lazy("tests.schema.test_lazy_types.test_lazy_enums") ] @strawberry.type class Query: @strawberry.field def something(self, enum: LazyEnum) -> LazyEnum: return enum schema = strawberry.Schema(query=Query) query = "{ something(enum: BREAD) }" result = schema.execute_sync(query) assert not result.errors assert result.data["something"] == "BREAD" def test_enum_falsy_values(): @strawberry.enum class IceCreamFlavour(Enum): VANILLA = "" STRAWBERRY = 0 @strawberry.input class Input: flavour: IceCreamFlavour optional_flavour: IceCreamFlavour | None = None @strawberry.type class Query: @strawberry.field def print_flavour(self, input: Input) -> str: return f"{input.flavour.value}" schema = strawberry.Schema(query=Query) query = "{ printFlavour(input: { flavour: VANILLA }) }" result = schema.execute_sync(query) assert not result.errors assert not result.data["printFlavour"] query = "{ printFlavour(input: { flavour: STRAWBERRY }) }" result = schema.execute_sync(query) assert not result.errors assert result.data["printFlavour"] == "0" def test_enum_in_list(): @strawberry.enum class IceCreamFlavour(Enum): VANILLA = "vanilla" STRAWBERRY = "strawberry" CHOCOLATE = "chocolate" PISTACHIO = "pistachio" @strawberry.type class Query: @strawberry.field def best_flavours(self) -> list[IceCreamFlavour]: return [IceCreamFlavour.STRAWBERRY, IceCreamFlavour.PISTACHIO] schema = strawberry.Schema(query=Query) query = "{ bestFlavours }" result = schema.execute_sync(query) assert not result.errors assert result.data["bestFlavours"] == ["STRAWBERRY", "PISTACHIO"] def test_enum_in_optional_list(): @strawberry.enum class IceCreamFlavour(Enum): VANILLA = "vanilla" STRAWBERRY = "strawberry" CHOCOLATE = "chocolate" PISTACHIO = "pistachio" @strawberry.type class Query: @strawberry.field def best_flavours(self) -> list[IceCreamFlavour] | None: return None schema = strawberry.Schema(query=Query) query = "{ bestFlavours }" result = schema.execute_sync(query) assert not result.errors assert result.data["bestFlavours"] is None @pytest.mark.asyncio async def test_enum_resolver_async(): @strawberry.enum class IceCreamFlavour(Enum): VANILLA = "vanilla" STRAWBERRY = "strawberry" CHOCOLATE = "chocolate" @strawberry.type class Query: @strawberry.field async def best_flavour(self) -> IceCreamFlavour: return IceCreamFlavour.STRAWBERRY schema = strawberry.Schema(query=Query) query = "{ bestFlavour }" result = await schema.execute(query) assert not result.errors assert result.data["bestFlavour"] == "STRAWBERRY" @pytest.mark.asyncio async def test_enum_in_list_async(): @strawberry.enum class IceCreamFlavour(Enum): VANILLA = "vanilla" STRAWBERRY = "strawberry" CHOCOLATE = "chocolate" PISTACHIO = "pistachio" @strawberry.type class Query: @strawberry.field async def best_flavours(self) -> list[IceCreamFlavour]: return [IceCreamFlavour.STRAWBERRY, IceCreamFlavour.PISTACHIO] schema = strawberry.Schema(query=Query) query = "{ bestFlavours }" result = await schema.execute(query) assert not result.errors assert result.data["bestFlavours"] == ["STRAWBERRY", "PISTACHIO"] def test_enum_as_argument(): @strawberry.enum class IceCreamFlavour(Enum): VANILLA = "vanilla" STRAWBERRY = "strawberry" CHOCOLATE = "chocolate" PISTACHIO = "pistachio" @strawberry.type class Query: @strawberry.field def create_flavour(self, flavour: IceCreamFlavour) -> str: return f"{flavour.name}" schema = strawberry.Schema(query=Query) expected = dedent( """ enum IceCreamFlavour { VANILLA STRAWBERRY CHOCOLATE PISTACHIO } type Query { createFlavour(flavour: IceCreamFlavour!): String! } """ ).strip() assert str(schema) == expected query = "{ createFlavour(flavour: CHOCOLATE) }" result = schema.execute_sync(query) assert not result.errors assert result.data["createFlavour"] == "CHOCOLATE" # Explicitly using `variable_values` now so that the enum is parsed using # `CustomGraphQLEnumType.parse_value()` instead of `.parse_literal` query = "query ($flavour: IceCreamFlavour!) { createFlavour(flavour: $flavour) }" result = schema.execute_sync(query, variable_values={"flavour": "VANILLA"}) assert not result.errors assert result.data["createFlavour"] == "VANILLA" def test_enum_as_default_argument(): @strawberry.enum class IceCreamFlavour(Enum): VANILLA = "vanilla" STRAWBERRY = "strawberry" CHOCOLATE = "chocolate" PISTACHIO = "pistachio" @strawberry.type class Query: @strawberry.field def create_flavour( self, flavour: IceCreamFlavour = IceCreamFlavour.STRAWBERRY ) -> str: return f"{flavour.name}" schema = strawberry.Schema(query=Query) expected = dedent( """ enum IceCreamFlavour { VANILLA STRAWBERRY CHOCOLATE PISTACHIO } type Query { createFlavour(flavour: IceCreamFlavour! = STRAWBERRY): String! } """ ).strip() assert str(schema) == expected query = "{ createFlavour }" result = schema.execute_sync(query) assert not result.errors assert result.data["createFlavour"] == "STRAWBERRY" def test_enum_resolver_plain_value(): @strawberry.enum class IceCreamFlavour(Enum): VANILLA = "vanilla" STRAWBERRY = "strawberry" CHOCOLATE = "chocolate" @strawberry.type class Query: @strawberry.field def best_flavour(self) -> IceCreamFlavour: return "strawberry" # type: ignore schema = strawberry.Schema(query=Query) query = "{ bestFlavour }" result = schema.execute_sync(query) assert not result.errors assert result.data["bestFlavour"] == "STRAWBERRY" def test_enum_deprecated_value(): @strawberry.enum class IceCreamFlavour(Enum): VANILLA = "vanilla" STRAWBERRY = strawberry.enum_value( "strawberry", deprecation_reason="We ran out" ) CHOCOLATE = strawberry.enum_value("chocolate") @strawberry.type class Query: @strawberry.field def best_flavour(self) -> IceCreamFlavour: return IceCreamFlavour.STRAWBERRY schema = strawberry.Schema(query=Query) query = """ { __type(name: "IceCreamFlavour") { enumValues(includeDeprecated: true) { name isDeprecated deprecationReason } } } """ result = schema.execute_sync(query) assert not result.errors assert result.data assert result.data["__type"]["enumValues"] == [ {"deprecationReason": None, "isDeprecated": False, "name": "VANILLA"}, {"deprecationReason": "We ran out", "isDeprecated": True, "name": "STRAWBERRY"}, {"deprecationReason": None, "isDeprecated": False, "name": "CHOCOLATE"}, ] def test_can_use_enum_values_in_input(): @strawberry.enum class TestEnum(Enum): A = "A" B = strawberry.enum_value("B") C = strawberry.enum_value("Coconut", deprecation_reason="We ran out") @strawberry.type class Query: @strawberry.field def receive_enum(self, test: TestEnum) -> str: return str(test) schema = strawberry.Schema(query=Query) query = """ query { a: receiveEnum(test: A) b: receiveEnum(test: B) c: receiveEnum(test: C) } """ result = schema.execute_sync(query) assert not result.errors assert result.data assert result.data["a"] == "TestEnum.A" assert result.data["b"] == "TestEnum.B" assert result.data["c"] == "TestEnum.C" def test_can_give_custom_name_to_enum_value(): @strawberry.enum class IceCreamFlavour(Enum): VANILLA = "vanilla" CHOCOLATE_COOKIE = strawberry.enum_value("chocolate", name="chocolateCookie") @strawberry.type class Query: @strawberry.field def best_flavour(self) -> IceCreamFlavour: return IceCreamFlavour.CHOCOLATE_COOKIE schema = strawberry.Schema(query=Query) assert ( str(schema) == dedent( """ enum IceCreamFlavour { VANILLA chocolateCookie } type Query { bestFlavour: IceCreamFlavour! } """ ).strip() ) query = """ { bestFlavour } """ result = schema.execute_sync(query) assert not result.errors assert result.data assert result.data["bestFlavour"] == "chocolateCookie" def test_can_use_enum_with_custom_name_as_input(): @strawberry.enum class IceCreamFlavour(Enum): VANILLA = "vanilla" CHOCOLATE_COOKIE = strawberry.enum_value("chocolate", name="chocolateCookie") @strawberry.type class Query: @strawberry.field def get_flavour(self, flavour: IceCreamFlavour) -> str: return flavour.name schema = strawberry.Schema(query=Query) assert ( str(schema) == dedent( """ enum IceCreamFlavour { VANILLA chocolateCookie } type Query { getFlavour(flavour: IceCreamFlavour!): String! } """ ).strip() ) query = """ { getFlavour(flavour: chocolateCookie) } """ result = schema.execute_sync(query) assert not result.errors assert result.data assert result.data["getFlavour"] == "CHOCOLATE_COOKIE" strawberry-graphql-0.287.0/tests/schema/test_execution.py000066400000000000000000000246641511033167500235640ustar00rootroot00000000000000import textwrap from textwrap import dedent from unittest.mock import patch import pytest from graphql import GraphQLError, ValidationRule, validate import strawberry from strawberry.extensions import AddValidationRules, DisableValidation @pytest.mark.parametrize("validate_queries", [True, False]) @patch("strawberry.schema.schema.validate", wraps=validate) def test_enabling_query_validation_sync(mock_validate, validate_queries): @strawberry.type class Query: example: str | None = None extensions = [] if validate_queries is False: extensions.append(DisableValidation()) schema = strawberry.Schema( query=Query, extensions=extensions, ) query = """ query { example } """ result = schema.execute_sync( query, root_value=Query(), ) assert not result.errors assert mock_validate.called is validate_queries @pytest.mark.asyncio @pytest.mark.parametrize("validate_queries", [True, False]) async def test_enabling_query_validation(validate_queries): @strawberry.type class Query: example: str | None = None extensions = [] if validate_queries is False: extensions.append(DisableValidation()) schema = strawberry.Schema( query=Query, extensions=extensions, ) query = """ query { example } """ with patch("strawberry.schema.schema.validate", wraps=validate) as mock_validate: result = await schema.execute( query, root_value=Query(), ) assert not result.errors assert mock_validate.called is validate_queries @pytest.mark.asyncio async def test_invalid_query_with_validation_enabled(): @strawberry.type class Query: example: str | None = None schema = strawberry.Schema(query=Query) query = """ query { example """ result = await schema.execute(query, root_value=Query()) assert str(result.errors[0]) == ( "Syntax Error: Expected Name, found .\n\n" "GraphQL request:4:5\n" "3 | example\n" "4 | \n" " | ^" ) @pytest.mark.asyncio async def test_asking_for_wrong_field(): @strawberry.type class Query: example: str | None = None schema = strawberry.Schema(query=Query, extensions=[DisableValidation()]) query = """ query { sample } """ result = await schema.execute( query, root_value=Query(), ) assert result.errors is None assert result.data == {} @pytest.mark.asyncio async def test_sending_wrong_variables(): @strawberry.type class Query: @strawberry.field def example(self, value: str) -> int: return 1 schema = strawberry.Schema(query=Query, extensions=[DisableValidation()]) query = """ query { example(value: 123) } """ result = await schema.execute( query, root_value=Query(), ) assert ( str(result.errors[0]) == textwrap.dedent( """ Argument 'value' has invalid value 123. GraphQL request:3:28 2 | query { 3 | example(value: 123) | ^ 4 | } """ ).strip() ) @pytest.mark.asyncio async def test_logging_exceptions(caplog: pytest.LogCaptureFixture): @strawberry.type class Query: @strawberry.field def example(self) -> int: raise ValueError("test") schema = strawberry.Schema(query=Query) query = dedent( """ query { example } """ ).strip() result = await schema.execute( query, root_value=Query(), ) assert result.errors assert len(result.errors) == 1 # Exception was logged assert len(caplog.records) == 1 record = caplog.records[0] assert record.levelname == "ERROR" assert ( record.message == dedent( """ test GraphQL request:2:5 1 | query { 2 | example | ^ 3 | } """ ).strip() ) assert record.name == "strawberry.execution" assert record.exc_info[0] is ValueError @pytest.mark.asyncio async def test_logging_graphql_exceptions(caplog: pytest.LogCaptureFixture): @strawberry.type class Query: @strawberry.field def example(self) -> int: return None # type: ignore schema = strawberry.Schema(query=Query) query = """ query { example } """ result = await schema.execute( query, root_value=Query(), ) assert len(result.errors) == 1 # Exception was logged assert len(caplog.records) == 1 record = caplog.records[0] assert record.levelname == "ERROR" assert record.name == "strawberry.execution" assert record.exc_info[0] is TypeError @pytest.mark.asyncio async def test_logging_parsing_error(caplog: pytest.LogCaptureFixture): @strawberry.type class Query: @strawberry.field def example(self) -> str: return "hi" schema = strawberry.Schema(query=Query) query = """ query { example """ result = await schema.execute( query, root_value=Query(), ) assert result.errors assert len(result.errors) == 1 # Exception was logged assert len(caplog.records) == 1 record = caplog.records[0] assert record.levelname == "ERROR" assert record.name == "strawberry.execution" assert "Syntax Error" in record.message def test_logging_parsing_error_sync(caplog: pytest.LogCaptureFixture): @strawberry.type class Query: @strawberry.field def example(self) -> str: return "hi" schema = strawberry.Schema(query=Query) query = """ query { example """ result = schema.execute_sync( query, root_value=Query(), ) assert result.errors assert len(result.errors) == 1 # Exception was logged assert len(caplog.records) == 1 record = caplog.records[0] assert record.levelname == "ERROR" assert record.name == "strawberry.execution" assert "Syntax Error" in record.message @pytest.mark.asyncio async def test_logging_validation_errors(caplog: pytest.LogCaptureFixture): @strawberry.type class Query: @strawberry.field def example(self) -> str: return "hi" schema = strawberry.Schema(query=Query) query = """ query { example { foo } missingField } """ result = await schema.execute( query, root_value=Query(), ) assert result.errors assert len(result.errors) == 2 # Exception was logged assert len(caplog.records) == 2 record1 = caplog.records[0] assert record1.levelname == "ERROR" assert record1.name == "strawberry.execution" assert "Field 'example' must not have a selection" in record1.message record2 = caplog.records[1] assert record2.levelname == "ERROR" assert record2.name == "strawberry.execution" assert "Cannot query field 'missingField'" in record2.message def test_logging_validation_errors_sync(caplog: pytest.LogCaptureFixture): @strawberry.type class Query: @strawberry.field def example(self) -> str: return "hi" schema = strawberry.Schema(query=Query) query = """ query { example { foo } missingField } """ result = schema.execute_sync( query, root_value=Query(), ) assert result.errors assert len(result.errors) == 2 # Exception was logged assert len(caplog.records) == 2 record1 = caplog.records[0] assert record1.levelname == "ERROR" assert record1.name == "strawberry.execution" assert "Field 'example' must not have a selection" in record1.message record2 = caplog.records[1] assert record2.levelname == "ERROR" assert record2.name == "strawberry.execution" assert "Cannot query field 'missingField'" in record2.message def test_overriding_process_errors(caplog: pytest.LogCaptureFixture): @strawberry.type class Query: @strawberry.field def example(self) -> int: return None # type: ignore execution_errors = [] class CustomSchema(strawberry.Schema): def process_errors(self, errors, execution_context): nonlocal execution_errors execution_errors = errors schema = CustomSchema(query=Query) query = """ query { example } """ result = schema.execute_sync( query, root_value=Query(), ) assert len(result.errors) == 1 assert len(execution_errors) == 1 assert result.errors == execution_errors # Exception wasn't logged assert caplog.records == [] def test_adding_custom_validation_rules(): @strawberry.type class Query: example: str | None = None another_example: str | None = None class CustomRule(ValidationRule): def enter_field(self, node, *args: str) -> None: if node.name.value == "example": self.report_error(GraphQLError("Can't query field 'example'")) schema = strawberry.Schema( query=Query, extensions=[ AddValidationRules([CustomRule]), ], ) result = schema.execute_sync( "{ example }", root_value=Query(), ) assert str(result.errors[0]) == "Can't query field 'example'" result = schema.execute_sync( "{ anotherExample }", root_value=Query(), ) assert not result.errors def test_partial_responses(): @strawberry.type class Query: @strawberry.field def example(self) -> str: return "hi" @strawberry.field def this_fails(self) -> str | None: raise ValueError("this field fails") schema = strawberry.Schema(query=Query) query = """ query { example thisFails } """ result = schema.execute_sync(query) assert result.data == {"example": "hi", "thisFails": None} assert result.errors assert result.errors[0].message == "this field fails" strawberry-graphql-0.287.0/tests/schema/test_execution_errors.py000066400000000000000000000055531511033167500251540ustar00rootroot00000000000000import pytest import strawberry from strawberry.schema.config import StrawberryConfig def test_runs_parsing(): @strawberry.type class Query: name: str schema = strawberry.Schema(query=Query) query = """ query { example """ result = schema.execute_sync(query) assert len(result.errors) == 1 assert result.errors[0].message == "Syntax Error: Expected Name, found ." @pytest.mark.asyncio @pytest.mark.filterwarnings("ignore:.* was never awaited:RuntimeWarning") async def test_errors_when_running_async_in_sync_mode(): @strawberry.type class Query: @strawberry.field async def name(self) -> str: return "Patrick" schema = strawberry.Schema(query=Query) query = """ query { name } """ result = schema.execute_sync(query) assert len(result.errors) == 1 assert isinstance(result.errors[0].original_error, RuntimeError) assert ( result.errors[0].message == "GraphQL execution failed to complete synchronously." ) @pytest.mark.asyncio async def test_runs_parsing_async(): @strawberry.type class Query: example: str schema = strawberry.Schema(query=Query) query = """ query { example """ result = await schema.execute(query) assert len(result.errors) == 1 assert result.errors[0].message == "Syntax Error: Expected Name, found ." def test_suggests_fields_by_default(): @strawberry.type class Query: name: str schema = strawberry.Schema(query=Query) query = """ query { ample } """ result = schema.execute_sync(query) assert len(result.errors) == 1 assert ( result.errors[0].message == "Cannot query field 'ample' on type 'Query'. Did you mean 'name'?" ) def test_can_disable_field_suggestions(): @strawberry.type class Query: name: str schema = strawberry.Schema( query=Query, config=StrawberryConfig(disable_field_suggestions=True) ) query = """ query { ample } """ result = schema.execute_sync(query) assert len(result.errors) == 1 assert result.errors[0].message == "Cannot query field 'ample' on type 'Query'." def test_can_disable_field_suggestions_multiple_fields(): @strawberry.type class Query: name: str age: str schema = strawberry.Schema( query=Query, config=StrawberryConfig(disable_field_suggestions=True) ) query = """ query { ample ag } """ result = schema.execute_sync(query) assert len(result.errors) == 2 assert result.errors[0].message == "Cannot query field 'ample' on type 'Query'." assert result.errors[1].message == "Cannot query field 'ag' on type 'Query'." strawberry-graphql-0.287.0/tests/schema/test_extensions.py000066400000000000000000000125441511033167500237520ustar00rootroot00000000000000from enum import Enum, auto from typing import Annotated, cast from graphql import ( DirectiveLocation, GraphQLEnumType, GraphQLInputType, GraphQLObjectType, GraphQLSchema, ) import strawberry from strawberry.directive import DirectiveValue from strawberry.scalars import JSON from strawberry.schema.schema_converter import GraphQLCoreConverter from strawberry.schema_directive import Location from strawberry.types.base import get_object_definition DEFINITION_BACKREF = GraphQLCoreConverter.DEFINITION_BACKREF def test_extensions_schema_directive(): @strawberry.schema_directive(locations=[Location.OBJECT, Location.INPUT_OBJECT]) class SchemaDirective: name: str @strawberry.type(directives=[SchemaDirective(name="Query")]) class Query: hello: str schema = strawberry.Schema(query=Query) graphql_schema: GraphQLSchema = schema._schema # Schema assert graphql_schema.extensions[DEFINITION_BACKREF] is schema # TODO: Apparently I stumbled on a bug: # SchemaDirective are used on schema.__str__(), # but aren't added to graphql_schema.directives # maybe graphql_schema_directive = graphql_schema.get_directive("schemaDirective") directives = get_object_definition(Query, strict=True).directives assert directives is not None graphql_schema_directive = schema.schema_converter.from_schema_directive( directives[0] ) assert ( graphql_schema_directive.extensions[DEFINITION_BACKREF] is SchemaDirective.__strawberry_directive__ ) def test_directive(): @strawberry.directive(locations=[DirectiveLocation.FIELD]) def uppercase(value: DirectiveValue[str], foo: str): # pragma: no cover return value.upper() @strawberry.type() class Query: hello: str schema = strawberry.Schema(query=Query, directives=[uppercase]) graphql_schema: GraphQLSchema = schema._schema graphql_directive = graphql_schema.get_directive("uppercase") assert graphql_directive.extensions[DEFINITION_BACKREF] is uppercase assert ( graphql_directive.args["foo"].extensions[DEFINITION_BACKREF] is uppercase.arguments[0] ) def test_enum(): @strawberry.enum class ThingType(Enum): JSON = auto() STR = auto() @strawberry.type() class Query: hello: ThingType schema = strawberry.Schema(query=Query) graphql_schema: GraphQLSchema = schema._schema graphql_thing_type = cast("GraphQLEnumType", graphql_schema.get_type("ThingType")) assert ( graphql_thing_type.extensions[DEFINITION_BACKREF] is ThingType.__strawberry_definition__ ) assert ( graphql_thing_type.values["JSON"].extensions[DEFINITION_BACKREF] is ThingType.__strawberry_definition__.values[0] ) assert ( graphql_thing_type.values["STR"].extensions[DEFINITION_BACKREF] is ThingType.__strawberry_definition__.values[1] ) def test_scalar(): @strawberry.type() class Query: hello: JSON hi: str schema = strawberry.Schema(query=Query) graphql_schema: GraphQLSchema = schema._schema assert ( graphql_schema.get_type("JSON").extensions[DEFINITION_BACKREF] is JSON._scalar_definition ) def test_interface(): @strawberry.interface class Thing: name: str @strawberry.type() class Query: hello: Thing schema = strawberry.Schema(query=Query) graphql_schema: GraphQLSchema = schema._schema assert ( graphql_schema.get_type("Thing").extensions[DEFINITION_BACKREF] is Thing.__strawberry_definition__ ) def test_union(): @strawberry.type class JsonThing: value: JSON @strawberry.type class StrThing: value: str SomeThing = Annotated[JsonThing | StrThing, strawberry.union("SomeThing")] @strawberry.type() class Query: hello: SomeThing schema = strawberry.Schema(query=Query) graphql_schema: GraphQLSchema = schema._schema graphql_type = graphql_schema.get_type("SomeThing") assert graphql_type.extensions[DEFINITION_BACKREF].graphql_name == "SomeThing" assert graphql_type.extensions[DEFINITION_BACKREF].description is None def test_object_types(): @strawberry.input class Input: name: str @strawberry.type() class Query: @strawberry.field def hello(self, input: Input) -> str: ... schema = strawberry.Schema(query=Query) graphql_schema: GraphQLSchema = schema._schema assert ( graphql_schema.get_type("Input").extensions[DEFINITION_BACKREF] is Input.__strawberry_definition__ ) assert ( graphql_schema.get_type("Query").extensions[DEFINITION_BACKREF] is Query.__strawberry_definition__ ) graphql_query = cast("GraphQLObjectType", graphql_schema.get_type("Query")) assert graphql_query.fields["hello"].extensions[ DEFINITION_BACKREF ] is Query.__strawberry_definition__.get_field("hello") assert ( graphql_query.fields["hello"].args["input"].extensions[DEFINITION_BACKREF] is Query.__strawberry_definition__.get_field("hello").arguments[0] ) graphql_input = cast("GraphQLInputType", graphql_schema.get_type("Input")) assert graphql_input.fields["name"].extensions[ DEFINITION_BACKREF ] is Input.__strawberry_definition__.get_field("name") strawberry-graphql-0.287.0/tests/schema/test_fields.py000066400000000000000000000071511511033167500230170ustar00rootroot00000000000000import dataclasses import textwrap from operator import getitem import strawberry from strawberry.printer import print_schema from strawberry.schema.config import StrawberryConfig from strawberry.types.field import StrawberryField def test_custom_field(): class CustomField(StrawberryField): def get_result(self, root, info, args, kwargs): return getattr(root, self.python_name) * 2 @strawberry.type class Query: a: str = CustomField(default="Example") # type: ignore schema = strawberry.Schema(query=Query) query = "{ a }" result = schema.execute_sync(query, root_value=Query()) assert not result.errors assert result.data == {"a": "ExampleExample"} def test_default_resolver_gets_attribute(): @strawberry.type class User: name: str @strawberry.type class Query: @strawberry.field def user(self) -> User: return User(name="Patrick") schema = strawberry.Schema(query=Query) query = "{ user { name } }" result = schema.execute_sync(query) assert not result.errors assert result.data assert result.data["user"]["name"] == "Patrick" def test_can_change_default_resolver(): @strawberry.type class User: name: str @strawberry.type class Query: @strawberry.field def user(self) -> User: return {"name": "Patrick"} # type: ignore schema = strawberry.Schema( query=Query, config=StrawberryConfig(default_resolver=getitem), ) query = "{ user { name } }" result = schema.execute_sync(query) assert not result.errors assert result.data assert result.data["user"]["name"] == "Patrick" def test_field_metadata(): @strawberry.type class Query: a: str = strawberry.field(default="Example", metadata={"Foo": "Bar"}) (a,) = dataclasses.fields(Query) assert a.metadata == {"Foo": "Bar"} def test_field_type_priority(): """Prioritise the field annotation on the class over the resolver annotation.""" def my_resolver() -> str: return "1.33" @strawberry.type class Query: a: float = strawberry.field(resolver=my_resolver) schema = strawberry.Schema(Query) expected = """ type Query { a: Float! } """ assert print_schema(schema) == textwrap.dedent(expected).strip() query = "{ a }" result = schema.execute_sync(query, root_value=Query()) assert not result.errors assert result.data == { "a": 1.33, } def test_field_type_override(): @strawberry.type class Query: a: float = strawberry.field(graphql_type=str) b = strawberry.field(graphql_type=int) @strawberry.field(graphql_type=float) def c(self): return "3.4" schema = strawberry.Schema(Query) expected = """ type Query { a: String! b: Int! c: Float! } """ assert print_schema(schema) == textwrap.dedent(expected).strip() query = "{ a, b, c }" result = schema.execute_sync(query, root_value=Query(a=1.33, b=2)) assert not result.errors assert result.data == { "a": "1.33", "b": 2, "c": 3.4, } def test_field_type_default(): @strawberry.type class User: name: str = "James" @strawberry.type class Query: @strawberry.field def a(self) -> User: return User() schema = strawberry.Schema(Query) expected = """ type Query { a: User! } type User { name: String! } """ assert print_schema(schema) == textwrap.dedent(expected).strip() strawberry-graphql-0.287.0/tests/schema/test_generics.py000066400000000000000000000576461511033167500233660ustar00rootroot00000000000000import textwrap from enum import Enum from typing import Any, Generic, TypeVar from typing_extensions import Self import strawberry def test_supports_generic_simple_type(): T = TypeVar("T") @strawberry.type class Edge(Generic[T]): cursor: strawberry.ID node_field: T @strawberry.type class Query: @strawberry.field def example(self) -> Edge[int]: return Edge(cursor=strawberry.ID("1"), node_field=1) schema = strawberry.Schema(query=Query) query = """{ example { __typename cursor nodeField } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == { "example": {"__typename": "IntEdge", "cursor": "1", "nodeField": 1} } def test_supports_generic_specialized(): T = TypeVar("T") @strawberry.type class Edge(Generic[T]): cursor: strawberry.ID node_field: T @strawberry.type class IntEdge(Edge[int]): ... @strawberry.type class Query: @strawberry.field def example(self) -> IntEdge: return IntEdge(cursor=strawberry.ID("1"), node_field=1) schema = strawberry.Schema(query=Query) query = """{ example { __typename cursor nodeField } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == { "example": {"__typename": "IntEdge", "cursor": "1", "nodeField": 1} } def test_supports_generic_specialized_subclass(): T = TypeVar("T") @strawberry.type class Edge(Generic[T]): cursor: strawberry.ID node_field: T @strawberry.type class IntEdge(Edge[int]): ... @strawberry.type class IntEdgeSubclass(IntEdge): ... @strawberry.type class Query: @strawberry.field def example(self) -> IntEdgeSubclass: return IntEdgeSubclass(cursor=strawberry.ID("1"), node_field=1) schema = strawberry.Schema(query=Query) query = """{ example { __typename cursor nodeField } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == { "example": {"__typename": "IntEdgeSubclass", "cursor": "1", "nodeField": 1} } def test_supports_generic_specialized_with_type(): T = TypeVar("T") @strawberry.type class Fruit: name: str @strawberry.type class Edge(Generic[T]): cursor: strawberry.ID node_field: T @strawberry.type class FruitEdge(Edge[Fruit]): ... @strawberry.type class Query: @strawberry.field def example(self) -> FruitEdge: return FruitEdge(cursor=strawberry.ID("1"), node_field=Fruit(name="Banana")) schema = strawberry.Schema(query=Query) query = """{ example { __typename cursor nodeField { name } } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == { "example": { "__typename": "FruitEdge", "cursor": "1", "nodeField": {"name": "Banana"}, } } def test_supports_generic_specialized_with_list_type(): T = TypeVar("T") @strawberry.type class Fruit: name: str @strawberry.type class Edge(Generic[T]): cursor: strawberry.ID nodes: list[T] @strawberry.type class FruitEdge(Edge[Fruit]): ... @strawberry.type class Query: @strawberry.field def example(self) -> FruitEdge: return FruitEdge( cursor=strawberry.ID("1"), nodes=[Fruit(name="Banana"), Fruit(name="Apple")], ) schema = strawberry.Schema(query=Query) query = """{ example { __typename cursor nodes { name } } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == { "example": { "__typename": "FruitEdge", "cursor": "1", "nodes": [ {"name": "Banana"}, {"name": "Apple"}, ], } } def test_supports_generic(): T = TypeVar("T") @strawberry.type class Edge(Generic[T]): cursor: strawberry.ID node: T @strawberry.type class Person: name: str @strawberry.type class Query: @strawberry.field def example(self) -> Edge[Person]: return Edge(cursor=strawberry.ID("1"), node=Person(name="Example")) schema = strawberry.Schema(query=Query) query = """{ example { __typename cursor node { name } } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == { "example": { "__typename": "PersonEdge", "cursor": "1", "node": {"name": "Example"}, } } def test_supports_multiple_generic(): A = TypeVar("A") B = TypeVar("B") @strawberry.type class Multiple(Generic[A, B]): a: A b: B @strawberry.type class Query: @strawberry.field def multiple(self) -> Multiple[int, str]: return Multiple(a=123, b="123") schema = strawberry.Schema(query=Query) query = """{ multiple { __typename a b } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == { "multiple": {"__typename": "IntStrMultiple", "a": 123, "b": "123"} } def test_supports_optional(): T = TypeVar("T") @strawberry.type class User: name: str @strawberry.type class Edge(Generic[T]): node: T | None = None @strawberry.type class Query: @strawberry.field def user(self) -> Edge[User]: return Edge() schema = strawberry.Schema(query=Query) query = """{ user { __typename node { name } } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == {"user": {"__typename": "UserEdge", "node": None}} def test_supports_lists(): T = TypeVar("T") @strawberry.type class User: name: str @strawberry.type class Edge(Generic[T]): nodes: list[T] @strawberry.type class Query: @strawberry.field def user(self) -> Edge[User]: return Edge(nodes=[]) schema = strawberry.Schema(query=Query) query = """{ user { __typename nodes { name } } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == {"user": {"__typename": "UserEdge", "nodes": []}} def test_supports_lists_of_optionals(): T = TypeVar("T") @strawberry.type class User: name: str @strawberry.type class Edge(Generic[T]): nodes: list[T | None] @strawberry.type class Query: @strawberry.field def user(self) -> Edge[User]: return Edge(nodes=[None]) schema = strawberry.Schema(query=Query) query = """{ user { __typename nodes { name } } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == {"user": {"__typename": "UserEdge", "nodes": [None]}} def test_can_extend_generics(): T = TypeVar("T") @strawberry.type class User: name: str @strawberry.type class Edge(Generic[T]): node: T @strawberry.type class Connection(Generic[T]): edges: list[Edge[T]] @strawberry.type class ConnectionWithMeta(Connection[T]): meta: str @strawberry.type class Query: @strawberry.field def users(self) -> ConnectionWithMeta[User]: return ConnectionWithMeta( meta="123", edges=[Edge(node=User(name="Patrick"))] ) schema = strawberry.Schema(query=Query) query = """{ users { __typename meta edges { __typename node { name } } } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == { "users": { "__typename": "UserConnectionWithMeta", "meta": "123", "edges": [{"__typename": "UserEdge", "node": {"name": "Patrick"}}], } } def test_supports_generic_in_unions(): T = TypeVar("T") @strawberry.type class Edge(Generic[T]): cursor: strawberry.ID node: T @strawberry.type class Fallback: node: str @strawberry.type class Query: @strawberry.field def example(self) -> Fallback | Edge[int]: return Edge(cursor=strawberry.ID("1"), node=1) schema = strawberry.Schema(query=Query) query = """{ example { __typename ... on IntEdge { cursor node } } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == { "example": {"__typename": "IntEdge", "cursor": "1", "node": 1} } def test_generic_with_enum_as_param_of_type_inside_unions(): T = TypeVar("T") @strawberry.type class Pet: name: str @strawberry.type class ErrorNode(Generic[T]): code: T @strawberry.enum class Codes(Enum): a = "a" b = "b" @strawberry.type class Query: @strawberry.field def result(self) -> Pet | ErrorNode[Codes]: return ErrorNode(code=Codes.a) schema = strawberry.Schema(query=Query) query = """{ result { __typename ... on CodesErrorNode { code } } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == {"result": {"__typename": "CodesErrorNode", "code": "a"}} def test_generic_with_enum(): T = TypeVar("T") @strawberry.enum class EstimatedValueEnum(Enum): test = "test" testtest = "testtest" @strawberry.type class EstimatedValue(Generic[T]): value: T type: EstimatedValueEnum @strawberry.type class Query: @strawberry.field def estimated_value(self) -> EstimatedValue[int] | None: return EstimatedValue(value=1, type=EstimatedValueEnum.test) schema = strawberry.Schema(query=Query) query = """{ estimatedValue { __typename value type } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == { "estimatedValue": { "__typename": "IntEstimatedValue", "value": 1, "type": "test", } } def test_supports_generic_in_unions_multiple_vars(): A = TypeVar("A") B = TypeVar("B") @strawberry.type class Edge(Generic[A, B]): info: A node: B @strawberry.type class Fallback: node: str @strawberry.type class Query: @strawberry.field def example(self) -> Fallback | Edge[int, str]: return Edge(node="string", info=1) schema = strawberry.Schema(query=Query) query = """{ example { __typename ... on IntStrEdge { node info } } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == { "example": {"__typename": "IntStrEdge", "node": "string", "info": 1} } def test_supports_multiple_generics_in_union(): T = TypeVar("T") @strawberry.type class Edge(Generic[T]): cursor: strawberry.ID node: T @strawberry.type class Query: @strawberry.field def example(self) -> list[Edge[int] | Edge[str]]: return [ Edge(cursor=strawberry.ID("1"), node=1), Edge(cursor=strawberry.ID("2"), node="string"), ] schema = strawberry.Schema(query=Query) expected_schema = """ type IntEdge { cursor: ID! node: Int! } union IntEdgeStrEdge = IntEdge | StrEdge type Query { example: [IntEdgeStrEdge!]! } type StrEdge { cursor: ID! node: String! } """ assert str(schema) == textwrap.dedent(expected_schema).strip() query = """{ example { __typename ... on IntEdge { cursor intNode: node } ... on StrEdge { cursor strNode: node } } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == { "example": [ {"__typename": "IntEdge", "cursor": "1", "intNode": 1}, {"__typename": "StrEdge", "cursor": "2", "strNode": "string"}, ] } def test_generics_via_anonymous_union(): T = TypeVar("T") @strawberry.type class Edge(Generic[T]): cursor: str node: T @strawberry.type class Connection(Generic[T]): edges: list[Edge[T]] @strawberry.type class Entity1: id: int @strawberry.type class Entity2: id: int @strawberry.type class Query: entities: Connection[Entity1 | Entity2] schema = strawberry.Schema(query=Query) expected_schema = textwrap.dedent( """ type Entity1 { id: Int! } union Entity1Entity2 = Entity1 | Entity2 type Entity1Entity2Connection { edges: [Entity1Entity2Edge!]! } type Entity1Entity2Edge { cursor: String! node: Entity1Entity2! } type Entity2 { id: Int! } type Query { entities: Entity1Entity2Connection! } """ ).strip() assert str(schema) == expected_schema def test_generated_names(): T = TypeVar("T") @strawberry.type class EdgeWithCursor(Generic[T]): cursor: strawberry.ID node: T @strawberry.type class SpecialPerson: name: str @strawberry.type class Query: @strawberry.field def person_edge(self) -> EdgeWithCursor[SpecialPerson]: return EdgeWithCursor( cursor=strawberry.ID("1"), node=SpecialPerson(name="Example") ) schema = strawberry.Schema(query=Query) query = """{ personEdge { __typename cursor node { name } } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == { "personEdge": { "__typename": "SpecialPersonEdgeWithCursor", "cursor": "1", "node": {"name": "Example"}, } } def test_supports_lists_within_unions(): T = TypeVar("T") @strawberry.type class User: name: str @strawberry.type class Edge(Generic[T]): nodes: list[T] @strawberry.type class Query: @strawberry.field def user(self) -> User | Edge[User]: return Edge(nodes=[User(name="P")]) schema = strawberry.Schema(query=Query) query = """{ user { __typename ... on UserEdge { nodes { name } } } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == {"user": {"__typename": "UserEdge", "nodes": [{"name": "P"}]}} def test_supports_lists_within_unions_empty_list(): T = TypeVar("T") @strawberry.type class User: name: str @strawberry.type class Edge(Generic[T]): nodes: list[T] @strawberry.type class Query: @strawberry.field def user(self) -> User | Edge[User]: return Edge(nodes=[]) schema = strawberry.Schema(query=Query) query = """{ user { __typename ... on UserEdge { nodes { name } } } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == {"user": {"__typename": "UserEdge", "nodes": []}} def test_raises_error_when_unable_to_find_type(): T = TypeVar("T") @strawberry.type class User: name: str @strawberry.type class Edge(Generic[T]): nodes: list[T] @strawberry.type class Query: @strawberry.field def user(self) -> User | Edge[User]: return Edge(nodes=["bad example"]) # type: ignore schema = strawberry.Schema(query=Query) query = """{ user { __typename ... on UserEdge { nodes { name } } } }""" result = schema.execute_sync(query) assert ( 'of the field "user" is not in the list of the types of the union' in result.errors[0].message ) def test_generic_with_arguments(): T = TypeVar("T") @strawberry.type class Collection(Generic[T]): @strawberry.field def by_id(self, ids: list[int]) -> list[T]: return [] @strawberry.type class Post: name: str @strawberry.type class Query: user: Collection[Post] schema = strawberry.Schema(Query) expected_schema = """ type Post { name: String! } type PostCollection { byId(ids: [Int!]!): [Post!]! } type Query { user: PostCollection! } """ assert str(schema) == textwrap.dedent(expected_schema).strip() def test_generic_argument(): T = TypeVar("T") @strawberry.type class Node(Generic[T]): @strawberry.field def edge(self, arg: T) -> bool: return bool(arg) @strawberry.field def edges(self, args: list[T]) -> int: return len(args) @strawberry.type class Query: i_node: Node[int] b_node: Node[bool] schema = strawberry.Schema(Query) expected_schema = """ type BoolNode { edge(arg: Boolean!): Boolean! edges(args: [Boolean!]!): Int! } type IntNode { edge(arg: Int!): Boolean! edges(args: [Int!]!): Int! } type Query { iNode: IntNode! bNode: BoolNode! } """ assert str(schema) == textwrap.dedent(expected_schema).strip() def test_generic_extra_type(): T = TypeVar("T") @strawberry.type class Node(Generic[T]): field: T @strawberry.type class Query: name: str schema = strawberry.Schema(Query, types=[Node[int]]) expected_schema = """ type IntNode { field: Int! } type Query { name: String! } """ assert str(schema) == textwrap.dedent(expected_schema).strip() def test_generic_extending_with_type_var(): T = TypeVar("T") @strawberry.interface class Node(Generic[T]): id: strawberry.ID def _resolve(self) -> T | None: return None @strawberry.type class Book(Node[str]): name: str @strawberry.type class Query: @strawberry.field def books(self) -> list[Book]: return [] schema = strawberry.Schema(query=Query) expected_schema = """ type Book implements Node { id: ID! name: String! } interface Node { id: ID! } type Query { books: [Book!]! } """ assert str(schema) == textwrap.dedent(expected_schema).strip() def test_self(): @strawberry.interface class INode: field: Self | None fields: list[Self] @strawberry.type class Node(INode): ... schema = strawberry.Schema(query=Node) expected_schema = """ schema { query: Node } interface INode { field: INode fields: [INode!]! } type Node implements INode { field: Node fields: [Node!]! } """ assert str(schema) == textwrap.dedent(expected_schema).strip() query = """{ field { __typename } fields { __typename } }""" result = schema.execute_sync(query, root_value=Node(field=None, fields=[])) assert result.data == {"field": None, "fields": []} def test_supports_generic_input_type(): T = TypeVar("T") @strawberry.input class Input(Generic[T]): field: T @strawberry.type class Query: @strawberry.field def field(self, input: Input[str]) -> str: return input.field schema = strawberry.Schema(query=Query) query = """{ field(input: { field: "data" }) }""" result = schema.execute_sync(query) assert not result.errors assert result.data == {"field": "data"} def test_generic_interface(): @strawberry.interface class ObjectType: obj: strawberry.Private[Any] @strawberry.field def repr(self) -> str: return str(self.obj) T = TypeVar("T") @strawberry.type class GenericObject(ObjectType, Generic[T]): @strawberry.field def value(self) -> T: return self.obj @strawberry.type class Query: @strawberry.field def foo(self) -> GenericObject[str]: return GenericObject(obj="foo") schema = strawberry.Schema(query=Query) query_result = schema.execute_sync( """ query { foo { __typename value repr } } """ ) assert not query_result.errors assert query_result.data == { "foo": { "__typename": "StrGenericObject", "value": "foo", "repr": "foo", } } def test_generic_interface_extra_types(): T = TypeVar("T") @strawberry.interface class Abstract: x: str = "" @strawberry.type class Real(Abstract, Generic[T]): y: T @strawberry.type class Query: @strawberry.field def real(self) -> Abstract: return Real[int](y=0) schema = strawberry.Schema(Query, types=[Real[int]]) assert ( str(schema) == textwrap.dedent( """ interface Abstract { x: String! } type IntReal implements Abstract { x: String! y: Int! } type Query { real: Abstract! } """ ).strip() ) query_result = schema.execute_sync("{ real { __typename x } }") assert not query_result.errors assert query_result.data == {"real": {"__typename": "IntReal", "x": ""}} def test_generic_with_interface(): T = TypeVar("T") @strawberry.type class Pagination(Generic[T]): items: list[T] @strawberry.interface class TestInterface: data: str @strawberry.type class Test1(TestInterface): pass @strawberry.type class TestError: reason: str @strawberry.type class Query: @strawberry.field def hello(self, info: strawberry.Info) -> Pagination[TestInterface] | TestError: return Pagination(items=[Test1(data="test1")]) schema = strawberry.Schema(Query, types=[Test1]) query_result = schema.execute_sync( "{ hello { ... on TestInterfacePagination { items { data }} } }" ) assert not query_result.errors assert query_result.data == {"hello": {"items": [{"data": "test1"}]}} strawberry-graphql-0.287.0/tests/schema/test_generics_nested.py000066400000000000000000000220251511033167500247070ustar00rootroot00000000000000import textwrap from typing import Generic, TypeVar import strawberry from strawberry.scalars import JSON def test_support_nested_generics(): T = TypeVar("T") @strawberry.type class User: name: str @strawberry.type class Edge(Generic[T]): node: T @strawberry.type class Connection(Generic[T]): edge: Edge[T] @strawberry.type class Query: @strawberry.field def users(self) -> Connection[User]: return Connection(edge=Edge(node=User(name="Patrick"))) schema = strawberry.Schema(query=Query) query = """{ users { __typename edge { __typename node { name } } } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == { "users": { "__typename": "UserConnection", "edge": {"__typename": "UserEdge", "node": {"name": "Patrick"}}, } } def test_unions_nested_inside_a_list(): T = TypeVar("T") @strawberry.type class JsonBlock: data: JSON @strawberry.type class BlockRowtype(Generic[T]): total: int items: list[T] @strawberry.type class Query: @strawberry.field def blocks( self, ) -> list[BlockRowtype[int] | BlockRowtype[str] | JsonBlock]: return [ BlockRowtype(total=3, items=["a", "b", "c"]), BlockRowtype(total=1, items=[1, 2, 3, 4]), JsonBlock(data=JSON({"a": 1})), ] schema = strawberry.Schema(query=Query) result = schema.execute_sync( """query { blocks { __typename ... on IntBlockRowtype { a: items } ... on StrBlockRowtype { b: items } ... on JsonBlock { data } } }""" ) assert not result.errors assert result.data == { "blocks": [ {"__typename": "StrBlockRowtype", "b": ["a", "b", "c"]}, {"__typename": "IntBlockRowtype", "a": [1, 2, 3, 4]}, {"__typename": "JsonBlock", "data": {"a": 1}}, ] } def test_unions_nested_inside_a_list_with_no_items(): T = TypeVar("T") @strawberry.type class JsonBlock: data: JSON @strawberry.type class BlockRowtype(Generic[T]): total: int items: list[T] @strawberry.type class Query: @strawberry.field def blocks( self, ) -> list[BlockRowtype[int] | BlockRowtype[str] | JsonBlock]: return [ BlockRowtype(total=3, items=[]), BlockRowtype(total=1, items=[]), JsonBlock(data=JSON({"a": 1})), ] schema = strawberry.Schema(query=Query) result = schema.execute_sync( """query { blocks { __typename ... on IntBlockRowtype { a: items } ... on StrBlockRowtype { b: items } ... on JsonBlock { data } } }""" ) assert not result.errors assert result.data == { "blocks": [ {"__typename": "IntBlockRowtype", "a": []}, {"__typename": "IntBlockRowtype", "a": []}, {"__typename": "JsonBlock", "data": {"a": 1}}, ] } def test_unions_nested_inside_a_list_of_lists(): T = TypeVar("T") @strawberry.type class JsonBlock: data: JSON @strawberry.type class BlockRowtype(Generic[T]): total: int items: list[list[T]] @strawberry.type class Query: @strawberry.field def blocks( self, ) -> list[BlockRowtype[int] | BlockRowtype[str] | JsonBlock]: return [ BlockRowtype(total=3, items=[["a", "b", "c"]]), BlockRowtype(total=1, items=[[1, 2, 3, 4]]), JsonBlock(data=JSON({"a": 1})), ] schema = strawberry.Schema(query=Query) result = schema.execute_sync( """query { blocks { __typename ... on IntBlockRowtype { a: items } ... on StrBlockRowtype { b: items } ... on JsonBlock { data } } }""" ) assert not result.errors assert result.data == { "blocks": [ {"__typename": "StrBlockRowtype", "b": [["a", "b", "c"]]}, {"__typename": "IntBlockRowtype", "a": [[1, 2, 3, 4]]}, {"__typename": "JsonBlock", "data": {"a": 1}}, ] } def test_using_generics_with_an_interface(): T = TypeVar("T") @strawberry.interface class BlockInterface: id: strawberry.ID disclaimer: str | None = strawberry.field(default=None) @strawberry.type class JsonBlock(BlockInterface): data: JSON @strawberry.type class BlockRowtype(BlockInterface, Generic[T]): total: int items: list[T] @strawberry.type class Query: @strawberry.field def blocks(self) -> list[BlockInterface]: return [ BlockRowtype(id=strawberry.ID("3"), total=3, items=["a", "b", "c"]), BlockRowtype(id=strawberry.ID("1"), total=1, items=[1, 2, 3, 4]), JsonBlock(id=strawberry.ID("2"), data=JSON({"a": 1})), ] schema = strawberry.Schema( query=Query, types=[BlockRowtype[int], JsonBlock, BlockRowtype[str]] ) expected_schema = textwrap.dedent( ''' interface BlockInterface { id: ID! disclaimer: String } type IntBlockRowtype implements BlockInterface { id: ID! disclaimer: String total: Int! items: [Int!]! } """ The `JSON` scalar type represents JSON values as specified by [ECMA-404](https://ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf). """ scalar JSON @specifiedBy(url: "https://ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf") type JsonBlock implements BlockInterface { id: ID! disclaimer: String data: JSON! } type Query { blocks: [BlockInterface!]! } type StrBlockRowtype implements BlockInterface { id: ID! disclaimer: String total: Int! items: [String!]! } ''' ).strip() assert str(schema) == expected_schema result = schema.execute_sync( """query { blocks { id __typename ... on IntBlockRowtype { a: items } ... on StrBlockRowtype { b: items } ... on JsonBlock { data } } }""" ) assert not result.errors assert result.data == { "blocks": [ {"id": "3", "__typename": "StrBlockRowtype", "b": ["a", "b", "c"]}, {"id": "1", "__typename": "IntBlockRowtype", "a": [1, 2, 3, 4]}, {"id": "2", "__typename": "JsonBlock", "data": {"a": 1}}, ] } def test_supports_generic_in_unions_with_nesting(): T = TypeVar("T") @strawberry.type class User: name: str @strawberry.type class Edge(Generic[T]): node: T @strawberry.type class Connection(Generic[T]): edge: Edge[T] @strawberry.type class Fallback: node: str @strawberry.type class Query: @strawberry.field def users(self) -> Connection[User] | Fallback: return Connection(edge=Edge(node=User(name="Patrick"))) schema = strawberry.Schema(query=Query) query = """{ users { __typename ... on UserConnection { edge { __typename node { name } } } } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == { "users": { "__typename": "UserConnection", "edge": {"__typename": "UserEdge", "node": {"name": "Patrick"}}, } } def test_does_not_raise_duplicated_type_error(): T = TypeVar("T") @strawberry.type class Wrapper(Generic[T]): value: T @strawberry.type class Query: a: Wrapper[Wrapper[int]] b: Wrapper[Wrapper[int]] schema = strawberry.Schema(query=Query) expected_schema = textwrap.dedent( """ type IntWrapper { value: Int! } type IntWrapperWrapper { value: IntWrapper! } type Query { a: IntWrapperWrapper! b: IntWrapperWrapper! } """ ).strip() assert str(schema) == expected_schema strawberry-graphql-0.287.0/tests/schema/test_get_extensions.py000066400000000000000000000065331511033167500246120ustar00rootroot00000000000000import strawberry from strawberry.directive import DirectiveLocation, DirectiveValue from strawberry.extensions import SchemaExtension from strawberry.extensions.directives import ( DirectivesExtension, DirectivesExtensionSync, ) @strawberry.type class Query: example: str @strawberry.directive(locations=[DirectiveLocation.FIELD]) def uppercase(value: DirectiveValue[str]) -> str: return value.upper() class MyExtension(SchemaExtension): ... def test_returns_empty_list_when_no_custom_directives(): schema = strawberry.Schema(query=Query) assert schema.get_extensions() == [] def test_returns_extension_passed_by_user(): schema = strawberry.Schema(query=Query, extensions=[MyExtension]) assert len(schema.get_extensions()) == 1 assert isinstance(schema.get_extensions()[0], MyExtension) def test_returns_directives_extension_when_passing_directives(): schema = strawberry.Schema(query=Query, directives=[uppercase]) assert len(schema.get_extensions()) == 1 assert isinstance(schema.get_extensions()[0], DirectivesExtension) def test_returns_extension_passed_by_user_and_directives_extension(): schema = strawberry.Schema( query=Query, extensions=[MyExtension], directives=[uppercase] ) for ext, ext_cls in zip( schema.get_extensions(), [MyExtension, DirectivesExtension], strict=True, ): assert isinstance(ext, ext_cls) def test_returns_directives_extension_when_passing_directives_sync(): schema = strawberry.Schema(query=Query, directives=[uppercase]) assert len(schema.get_extensions(sync=True)) == 1 assert isinstance(schema.get_extensions(sync=True)[0], DirectivesExtensionSync) def test_returns_extension_passed_by_user_and_directives_extension_sync(): schema = strawberry.Schema( query=Query, extensions=[MyExtension], directives=[uppercase] ) for ext, ext_cls in zip( schema.get_extensions(sync=True), [MyExtension, DirectivesExtensionSync], strict=True, ): assert isinstance(ext, ext_cls) def test_no_duplicate_extensions_with_directives(): """Test to verify that extensions are not duplicated when directives are present. This test initially fails with the current implementation but passes after fixing the get_extensions method. """ schema = strawberry.Schema( query=Query, extensions=[MyExtension], directives=[uppercase] ) extensions = schema.get_extensions() # Count how many times our extension appears ext_count = sum(1 for e in extensions if isinstance(e, MyExtension)) # With current implementation this fails as ext_count is 2 assert ext_count == 1, f"Extension appears {ext_count} times instead of once" def test_extension_order_preserved(): """Test to verify that extension order is preserved while removing duplicates.""" class Extension1(SchemaExtension): pass class Extension2(SchemaExtension): pass schema = strawberry.Schema( query=Query, extensions=[Extension1, Extension2], directives=[uppercase] ) extensions = schema.get_extensions() extension_types = [ type(ext) for ext in extensions if not isinstance(ext, (DirectivesExtension, DirectivesExtensionSync)) ] assert extension_types == [Extension1, Extension2], "Extension order not preserved" strawberry-graphql-0.287.0/tests/schema/test_info.py000066400000000000000000000242421511033167500225040ustar00rootroot00000000000000import dataclasses import json from typing import Annotated, Optional import pytest import strawberry from strawberry.types.base import StrawberryOptional from strawberry.types.nodes import FragmentSpread, InlineFragment, SelectedField from strawberry.types.unset import UNSET def test_info_has_the_correct_shape(): my_context = "123" root_value = "ABC" @strawberry.type class Result: field_name: str python_name: str selected_field: str operation: str path: str variable_values: str context_equal: bool root_equal: bool return_type: str schema_print: str @strawberry.type class Query: @strawberry.field def hello_world(self, info: strawberry.Info[str, str]) -> Result: return Result( path="".join([str(p) for p in info.path.as_list()]), operation=str(info.operation), field_name=info.field_name, python_name=info.python_name, selected_field=json.dumps(dataclasses.asdict(*info.selected_fields)), variable_values=str(info.variable_values), context_equal=info.context == my_context, root_equal=info.root_value == root_value, return_type=str(info.return_type), schema_print=info.schema.as_str(), ) schema = strawberry.Schema(query=Query) query = """{ helloWorld { fieldName pythonName selectedField contextEqual operation path rootEqual variableValues returnType schemaPrint } }""" result = schema.execute_sync(query, context_value=my_context, root_value=root_value) assert not result.errors assert result.data info = result.data["helloWorld"] assert info.pop("operation").startswith("OperationDefinitionNode at") field = json.loads(info.pop("selectedField")) selections = {selection["name"] for selection in field.pop("selections")} assert selections == { "selectedField", "path", "rootEqual", "operation", "contextEqual", "variableValues", "returnType", "fieldName", "pythonName", "schemaPrint", } assert field == { "name": "helloWorld", "directives": {}, "alias": None, "arguments": {}, } assert info == { "fieldName": "helloWorld", "pythonName": "hello_world", "path": "helloWorld", "contextEqual": True, "rootEqual": True, "variableValues": "{}", "returnType": ".Result'>", "schemaPrint": schema.as_str(), } def test_info_field_fragments(): @strawberry.type class Result: ok: bool selected_fields = None @strawberry.type class Query: @strawberry.field def hello(self, info: strawberry.Info[str, str]) -> Result: nonlocal selected_fields selected_fields = info.selected_fields return Result(ok=True) schema = strawberry.Schema(query=Query) query = """{ hello { ... on Result { k: ok @include(if: true) } ...frag } } fragment frag on Result { ok } """ result = schema.execute_sync(query) assert not result.errors assert selected_fields == [ SelectedField( name="hello", directives={}, alias=None, arguments={}, selections=[ InlineFragment( type_condition="Result", directives={}, selections=[ SelectedField( name="ok", alias="k", arguments={}, directives={ "include": { "if": True, }, }, selections=[], ) ], ), FragmentSpread( name="frag", directives={}, type_condition="Result", selections=[ SelectedField( name="ok", directives={}, arguments={}, selections=[], ) ], ), ], ) ] def test_info_arguments(): @strawberry.input class TestInput: name: str age: int | None = UNSET selected_fields = None @strawberry.type class Query: @strawberry.field def test_arg( self, info: strawberry.Info[str, str], input: TestInput, another_arg: bool = True, ) -> str: nonlocal selected_fields selected_fields = info.selected_fields return "Hi" schema = strawberry.Schema(query=Query) query = """{ testArg(input: {name: "hi"}) } """ result = schema.execute_sync(query) assert not result.errors assert selected_fields == [ SelectedField( name="testArg", directives={}, arguments={ "input": { "name": "hi", }, }, selections=[], ) ] query = """query TestQuery($input: TestInput!) { testArg(input: $input) } """ result = schema.execute_sync( query, variable_values={ "input": { "name": "hi", "age": 10, }, }, ) assert not result.errors assert selected_fields == [ SelectedField( name="testArg", directives={}, arguments={ "input": { "name": "hi", "age": 10, }, }, selections=[], ) ] def test_info_selected_fields_undefined_variable(): @strawberry.type class Result: ok: bool selected_fields = None @strawberry.type class Query: @strawberry.field def hello( self, info: strawberry.Info[str, str], optional_input: str | None = "hi" ) -> Result: nonlocal selected_fields selected_fields = info.selected_fields return Result(ok=True) schema = strawberry.Schema(query=Query) query = """ query MyQuery($optionalInput: String) { hello(optionalInput: $optionalInput) { ok } } """ result = schema.execute_sync(query, variable_values={}) assert not result.errors assert selected_fields == [ SelectedField( name="hello", directives={}, alias=None, arguments={ "optionalInput": None, }, selections=[ SelectedField( name="ok", alias=None, arguments={}, directives={}, selections=[], ) ], ) ] @pytest.mark.parametrize( ("return_type", "return_value"), [ (str, "text"), (list[str], ["text"]), (Optional[list[int]], None), ], ) def test_return_type_from_resolver(return_type, return_value): @strawberry.type class Query: @strawberry.field def field(self, info: strawberry.Info) -> return_type: assert info.return_type == return_type return return_value schema = strawberry.Schema(query=Query) result = schema.execute_sync("{ field }") assert not result.errors assert result.data assert result.data["field"] == return_value def test_return_type_from_field(): def resolver(info: strawberry.Info): assert info.return_type is int return 0 @strawberry.type class Query: field: int = strawberry.field(resolver=resolver) schema = strawberry.Schema(query=Query) result = schema.execute_sync("{ field }") assert not result.errors assert result.data assert result.data["field"] == 0 def test_field_nodes_deprecation(): def resolver(info: strawberry.Info): info.field_nodes return 0 @strawberry.type class Query: field: int = strawberry.field(resolver=resolver) schema = strawberry.Schema(query=Query) with pytest.deprecated_call(): result = schema.execute_sync("{ field }") assert not result.errors assert result.data assert result.data["field"] == 0 def test_get_argument_defintion_helper(): @strawberry.input class TestInput: foo: str arg_1_def = None arg_2_def = None missing_arg_def = None @strawberry.type class Query: @strawberry.field def field( self, info: strawberry.Info, arg_1: Annotated[str, strawberry.argument(description="Some description")], arg_2: TestInput | None = None, ) -> str: nonlocal arg_1_def, arg_2_def, missing_arg_def arg_1_def = info.get_argument_definition("arg_1") arg_2_def = info.get_argument_definition("arg_2") missing_arg_def = info.get_argument_definition("missing_arg_def") return "bar" schema = strawberry.Schema(query=Query) result = schema.execute_sync('{ field(arg1: "hi") }') assert not result.errors assert arg_1_def assert arg_1_def.type is str assert arg_1_def.python_name == "arg_1" assert arg_1_def.description == "Some description" assert arg_2_def assert arg_2_def.python_name == "arg_2" assert arg_2_def.default is None assert isinstance(arg_2_def.type, StrawberryOptional) assert arg_2_def.type.of_type is TestInput assert missing_arg_def is None strawberry-graphql-0.287.0/tests/schema/test_input.py000066400000000000000000000070621511033167500227110ustar00rootroot00000000000000import re import textwrap import pytest import strawberry from strawberry.exceptions import InvalidSuperclassInterfaceError from strawberry.printer import print_schema from tests.conftest import skip_if_gql_32 def test_renaming_input_fields(): @strawberry.input class FilterInput: in_: str | None = strawberry.field(name="in", default=strawberry.UNSET) @strawberry.type class Query: hello: str = "Hello" @strawberry.type class Mutation: @strawberry.mutation def filter(self, input: FilterInput) -> str: return f"Hello {input.in_ or 'nope'}" schema = strawberry.Schema(query=Query, mutation=Mutation) query = "mutation { filter(input: {}) }" result = schema.execute_sync(query) assert not result.errors assert result.data assert result.data["filter"] == "Hello nope" @skip_if_gql_32("formatting is different in gql 3.2") def test_input_with_nonscalar_field_default(): @strawberry.input class NonScalarField: id: int = 10 nullable_field: int | None = None @strawberry.input class Input: non_scalar_field: NonScalarField = strawberry.field( default_factory=lambda: NonScalarField() ) id: int = 10 @strawberry.type class ExampleOutput: input_id: int non_scalar_id: int non_scalar_nullable_field: int | None @strawberry.type class Query: @strawberry.field def example(self, data: Input) -> ExampleOutput: return ExampleOutput( input_id=data.id, non_scalar_id=data.non_scalar_field.id, non_scalar_nullable_field=data.non_scalar_field.nullable_field, ) schema = strawberry.Schema(query=Query) expected = """ type ExampleOutput { inputId: Int! nonScalarId: Int! nonScalarNullableField: Int } input Input { nonScalarField: NonScalarField! = { id: 10 } id: Int! = 10 } input NonScalarField { id: Int! = 10 nullableField: Int = null } type Query { example(data: Input!): ExampleOutput! } """ assert print_schema(schema) == textwrap.dedent(expected).strip() query = """ query($input_data: Input!) { example(data: $input_data) { inputId nonScalarId nonScalarNullableField } } """ result = schema.execute_sync( query, variable_values={"input_data": {"nonScalarField": {}}} ) assert not result.errors expected_result = {"inputId": 10, "nonScalarId": 10, "nonScalarNullableField": None} assert result.data["example"] == expected_result @pytest.mark.raises_strawberry_exception( InvalidSuperclassInterfaceError, match=re.escape( "Input class 'SomeInput' cannot inherit from interface(s): SomeInterface" ), ) def test_input_cannot_inherit_from_interface(): @strawberry.interface class SomeInterface: some_arg: str @strawberry.input class SomeInput(SomeInterface): another_arg: str @pytest.mark.raises_strawberry_exception( InvalidSuperclassInterfaceError, match=re.escape( "Input class 'SomeOtherInput' cannot inherit from interface(s): SomeInterface, SomeOtherInterface" ), ) def test_input_cannot_inherit_from_interfaces(): @strawberry.interface class SomeInterface: some_arg: str @strawberry.interface class SomeOtherInterface: some_other_arg: str @strawberry.input class SomeOtherInput(SomeInterface, SomeOtherInterface): another_arg: str strawberry-graphql-0.287.0/tests/schema/test_interface.py000066400000000000000000000256751511033167500235240ustar00rootroot00000000000000from dataclasses import dataclass from typing import Any import pytest from pytest_mock import MockerFixture import strawberry from strawberry.types.base import StrawberryObjectDefinition def test_query_interface(): @strawberry.interface class Cheese: name: str @strawberry.type class Swiss(Cheese): canton: str @strawberry.type class Italian(Cheese): province: str @strawberry.type class Root: @strawberry.field def assortment(self) -> list[Cheese]: return [ Italian(name="Asiago", province="Friuli"), Swiss(name="Tomme", canton="Vaud"), ] schema = strawberry.Schema(query=Root, types=[Swiss, Italian]) query = """{ assortment { name ... on Italian { province } ... on Swiss { canton } } }""" result = schema.execute_sync(query) assert not result.errors assert result.data is not None assert result.data["assortment"] == [ {"name": "Asiago", "province": "Friuli"}, {"canton": "Vaud", "name": "Tomme"}, ] def test_interfaces_can_implement_other_interfaces(): @strawberry.interface class Error: message: str @strawberry.interface class FieldError(Error): message: str field: str @strawberry.type class PasswordTooShort(FieldError): message: str field: str fix: str @strawberry.type class Query: @strawberry.field def always_error(self) -> Error: return PasswordTooShort( message="Password Too Short", field="Password", fix="Choose more characters", ) schema = strawberry.Schema(Query, types=[PasswordTooShort]) query = """{ alwaysError { ... on Error { message } ... on FieldError { field } ... on PasswordTooShort { fix } } }""" result = schema.execute_sync(query) assert not result.errors assert result.data is not None assert result.data["alwaysError"] == { "message": "Password Too Short", "field": "Password", "fix": "Choose more characters", } def test_interface_duck_typing(): @strawberry.interface class Entity: id: int @strawberry.type class Anime(Entity): name: str @classmethod def is_type_of(cls, obj: Any, _) -> bool: return isinstance(obj, AnimeORM) @dataclass class AnimeORM: id: int name: str @strawberry.type class Query: @strawberry.field def anime(self) -> Entity: return AnimeORM(id=1, name="One Piece") # type: ignore schema = strawberry.Schema(query=Query, types=[Anime]) query = """{ anime { id ... on Anime { name } } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == {"anime": {"id": 1, "name": "One Piece"}} def test_interface_explicit_type_resolution(): @dataclass class AnimeORM: id: int name: str @strawberry.interface class Node: id: int @strawberry.type class Anime(Node): name: str @classmethod def is_type_of(cls, obj: Any, _) -> bool: return isinstance(obj, AnimeORM) @strawberry.type class Query: @strawberry.field def node(self) -> Node: return AnimeORM(id=1, name="One Piece") # type: ignore schema = strawberry.Schema(query=Query, types=[Anime]) query = "{ node { __typename, id ... on Anime { name }} }" result = schema.execute_sync(query) assert not result.errors assert result.data == { "node": { "__typename": "Anime", "id": 1, "name": "One Piece", } } @pytest.mark.xfail(reason="We don't support returning dictionaries yet") def test_interface_duck_typing_returning_dict(): @strawberry.interface class Entity: id: int @strawberry.type class Anime(Entity): name: str @strawberry.type class Query: @strawberry.field def anime(self) -> Anime: return {"id": 1, "name": "One Piece"} # type: ignore schema = strawberry.Schema(query=Query) query = """{ anime { name } }""" result = schema.execute_sync(query) assert not result.errors assert result.data == {"anime": {"name": "One Piece"}} def test_duplicated_interface_in_multi_inheritance(): """Test that interfaces are gathered properly via CPython's MRO. Previously interfaces were duplicated within a "Diamond Problem" inheritance scenario which is tested here. Using the MRO instead of the `__bases__` attribute of a class in :py:func:`strawberry.object_type._get_interfaces` allows Python's C3 linearization algorithm to create a consistent precedents graph without duplicates. """ @strawberry.interface class Base: id: str @strawberry.interface class InterfaceA(Base): id: str field_a: str @strawberry.interface class InterfaceB(Base): id: str field_b: str @strawberry.type class MyType(InterfaceA, InterfaceB): id: str field_a: str field_b: str @strawberry.type class Query: my_type: MyType type_definition: StrawberryObjectDefinition = MyType.__strawberry_definition__ origins = [i.origin for i in type_definition.interfaces] assert origins == [InterfaceA, InterfaceB, Base] strawberry.Schema(Query) # Final sanity check to ensure schema compiles def test_interface_resolve_type(mocker: MockerFixture): """Check that the default implemenetation of `resolve_type` functions as expected. In this test-case the default implementation of `resolve_type` defined in `GraphQLCoreConverter.from_interface`, should immediately resolve the type of the returned concrete object. A concrete object is defined as one that is an instance of the interface it implements. Before the default implementation of `resolve_type`, the `is_type_of` methods of all specializations of an interface (in this case Anime & Movie) would be called. As this needlessly reduces performance, this test checks if only `Anime.is_type_of` is called when `Query.node` returns an `Anime` object. """ class IsTypeOfTester: @classmethod def is_type_of(cls, obj: Any, _) -> bool: return isinstance(obj, cls) spy_is_type_of = mocker.spy(IsTypeOfTester, "is_type_of") @strawberry.interface class Node: id: int @strawberry.type class Anime(Node, IsTypeOfTester): name: str @strawberry.type class Movie(Node): title: str @classmethod def is_type_of(cls, *args: Any, **kwargs: Any) -> bool: del args, kwargs raise RuntimeError("Movie.is_type_of shouldn't have been called") @strawberry.type class Query: @strawberry.field def node(self) -> Node: return Anime(id=1, name="One Pierce") schema = strawberry.Schema(query=Query, types=[Anime, Movie]) query = "{ node { __typename, id } }" result = schema.execute_sync(query) assert not result.errors assert result.data == {"node": {"__typename": "Anime", "id": 1}} spy_is_type_of.assert_called_once() def test_interface_specialized_resolve_type(mocker: MockerFixture): """Test that a specialized ``resolve_type`` is called.""" class InterfaceTester: @classmethod def resolve_type(cls, obj: Any, *args: Any, **kwargs: Any) -> str: del args, kwargs return obj.__strawberry_definition__.name spy_resolve_type = mocker.spy(InterfaceTester, "resolve_type") @strawberry.interface class Food(InterfaceTester): id: int @strawberry.type class Fruit(Food): name: str @strawberry.type class Query: @strawberry.field def food(self) -> Food: return Fruit(id=1, name="strawberry") schema = strawberry.Schema(query=Query, types=[Fruit]) result = schema.execute_sync("query { food { ... on Fruit { name } } }") assert not result.errors assert result.data == {"food": {"name": "strawberry"}} spy_resolve_type.assert_called_once() @pytest.mark.asyncio async def test_derived_interface(mocker: MockerFixture): """Test if correct resolve_type is called on a derived interface.""" class NodeInterfaceTester: @classmethod def resolve_type(cls, obj: Any, *args: Any, **kwargs: Any) -> str: del args, kwargs return obj.__strawberry_definition__.name class NamedNodeInterfaceTester: @classmethod def resolve_type(cls, obj: Any, *args: Any, **kwargs: Any) -> str: del args, kwargs return obj.__strawberry_definition__.name spy_node_resolve_type = mocker.spy(NodeInterfaceTester, "resolve_type") spy_named_node_resolve_type = mocker.spy(NamedNodeInterfaceTester, "resolve_type") @strawberry.interface class Node(NodeInterfaceTester): id: int @strawberry.interface class NamedNode(NamedNodeInterfaceTester, Node): name: str @strawberry.type class Person(NamedNode): pass @strawberry.type class Query: @strawberry.field def friends(self) -> list[NamedNode]: return [Person(id=1, name="foo"), Person(id=2, name="bar")] schema = strawberry.Schema(Query, types=[Person]) result = await schema.execute("query { friends { name } }") assert not result.errors assert result.data == {"friends": [{"name": "foo"}, {"name": "bar"}]} assert result.data is not None assert spy_named_node_resolve_type.call_count == len(result.data["friends"]) spy_node_resolve_type.assert_not_called() def test_resolve_type_on_interface_returning_interface(): @strawberry.interface class Node: id: strawberry.ID @classmethod def resolve_type(cls, obj: Any, *args: Any, **kwargs: Any) -> str: return "Video" if obj.id == "1" else "Image" @strawberry.type class Video(Node): ... @strawberry.type class Image(Node): ... @strawberry.type class Query: @strawberry.field def node(self, id: strawberry.ID) -> Node: return Node(id=id) schema = strawberry.Schema(query=Query, types=[Video, Image]) query = """ query { one: node(id: "1") { __typename id } two: node(id: "2") { __typename id } } """ result = schema.execute_sync(query) assert not result.errors assert result.data assert result.data["one"] == {"id": "1", "__typename": "Video"} assert result.data["two"] == {"id": "2", "__typename": "Image"} strawberry-graphql-0.287.0/tests/schema/test_lazy/000077500000000000000000000000001511033167500221525ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/schema/test_lazy/__init__.py000066400000000000000000000000001511033167500242510ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/schema/test_lazy/schema.py000066400000000000000000000001531511033167500237630ustar00rootroot00000000000000import strawberry from tests.schema.test_lazy.type_c import Query schema = strawberry.Schema(query=Query) strawberry-graphql-0.287.0/tests/schema/test_lazy/test_lazy.py000066400000000000000000000012021511033167500245350ustar00rootroot00000000000000import textwrap import strawberry from strawberry.printer import print_schema def test_cyclic_import(): from .type_a import TypeA from .type_b import TypeB @strawberry.type class Query: a: TypeA b: TypeB expected = """ type Query { a: TypeA! b: TypeB! } type TypeA { listOfB: [TypeB!] typeB: TypeB! } type TypeB { typeA: TypeA! typeAList: [TypeA!]! typeCList: [TypeC!]! } type TypeC { name: String! } """ schema = strawberry.Schema(Query) assert print_schema(schema) == textwrap.dedent(expected).strip() strawberry-graphql-0.287.0/tests/schema/test_lazy/test_lazy_generic.py000066400000000000000000000103301511033167500262330ustar00rootroot00000000000000import os import subprocess import sys import sysconfig import textwrap from collections.abc import Sequence from pathlib import Path from typing import TYPE_CHECKING, Annotated, Generic, TypeVar import pytest import strawberry if TYPE_CHECKING: from tests.schema.test_lazy.type_a import TypeA from tests.schema.test_lazy.type_c import TypeC STRAWBERRY_EXECUTABLE = next( Path(sysconfig.get_path("scripts")).glob("strawberry*"), None ) T = TypeVar("T") TypeAType = Annotated["TypeA", strawberry.lazy("tests.schema.test_lazy.type_a")] def test_lazy_types_with_generic(): @strawberry.type class Edge(Generic[T]): node: T @strawberry.type class Query: users: Edge[TypeAType] strawberry.Schema(query=Query) def test_no_generic_type_duplication_with_lazy(): from tests.schema.test_lazy.type_b import TypeB @strawberry.type class Edge(Generic[T]): node: T @strawberry.type class Query: users: Edge[TypeB] relatively_lazy_users: Edge[Annotated["TypeB", strawberry.lazy(".type_b")]] absolutely_lazy_users: Edge[ Annotated["TypeB", strawberry.lazy("tests.schema.test_lazy.type_b")] ] schema = strawberry.Schema(query=Query) expected_schema = textwrap.dedent( """ type Query { users: TypeBEdge! relativelyLazyUsers: TypeBEdge! absolutelyLazyUsers: TypeBEdge! } type TypeA { listOfB: [TypeB!] typeB: TypeB! } type TypeB { typeA: TypeA! typeAList: [TypeA!]! typeCList: [TypeC!]! } type TypeBEdge { node: TypeB! } type TypeC { name: String! } """ ).strip() assert str(schema) == expected_schema @pytest.mark.parametrize( "commands", [ pytest.param( [sys.executable, "tests/schema/test_lazy/type_c.py"], id="script", ), pytest.param( [sys.executable, "-m", "tests.schema.test_lazy.type_c"], id="module", ), pytest.param( [STRAWBERRY_EXECUTABLE, "export-schema", "tests.schema.test_lazy.schema"], id="cli", marks=pytest.mark.skipif( sys.platform == "win32", reason="Test is broken on windows" ), ), ], ) def test_lazy_types_loaded_from_same_module(commands: Sequence[str]): """Test if lazy types resolved from the same module produce duplication error. Note: `subprocess` is used since the test must be run as the main module / script. """ result = subprocess.run( args=[*commands], env=os.environ, capture_output=True, check=True, ) expected = """\ type Query { typeA: TypeCEdge! typeB: TypeCEdge! } type TypeC { name: String! } type TypeCEdge { node: TypeC! } """ schema_sdl = result.stdout.decode().replace(os.linesep, "\n") assert textwrap.dedent(schema_sdl) == textwrap.dedent(expected) def test_lazy_types_declared_within_optional(): from tests.schema.test_lazy.type_c import Edge, TypeC @strawberry.type class Query: normal_edges: list[Edge[TypeC | None]] lazy_edges: list[ Edge[ Annotated["TypeC", strawberry.lazy("tests.schema.test_lazy.type_c")] | None ] ] schema = strawberry.Schema(query=Query) expected_schema = textwrap.dedent( """ type Query { normalEdges: [TypeCOptionalEdge!]! lazyEdges: [TypeCOptionalEdge!]! } type TypeC { name: String! } type TypeCOptionalEdge { node: TypeC } """ ).strip() assert str(schema) == expected_schema def test_lazy_with_already_specialized_generic(): from tests.schema.test_lazy.type_d import Query schema = strawberry.Schema(query=Query) expected_schema = textwrap.dedent( """ type Query { typeD1: TypeD! typeD: TypeD! } type TypeD { name: String! } """ ).strip() assert str(schema) == expected_schema strawberry-graphql-0.287.0/tests/schema/test_lazy/type_a.py000066400000000000000000000006511511033167500240070ustar00rootroot00000000000000from typing import TYPE_CHECKING, Annotated import strawberry if TYPE_CHECKING: from .type_b import TypeB @strawberry.type class TypeA: list_of_b: ( list[Annotated["TypeB", strawberry.lazy("tests.schema.test_lazy.type_b")]] | None ) = None @strawberry.field def type_b(self) -> Annotated["TypeB", strawberry.lazy(".type_b")]: from .type_b import TypeB return TypeB() strawberry-graphql-0.287.0/tests/schema/test_lazy/type_b.py000066400000000000000000000017201511033167500240060ustar00rootroot00000000000000from typing import TYPE_CHECKING, Annotated import strawberry if TYPE_CHECKING: from .type_a import TypeA from .type_c import TypeC ListTypeA = list[TypeA] ListTypeC = list[TypeC] else: TypeA = Annotated["TypeA", strawberry.lazy("tests.schema.test_lazy.type_a")] ListTypeA = list[ Annotated["TypeA", strawberry.lazy("tests.schema.test_lazy.type_a")] ] ListTypeC = list[ Annotated["TypeC", strawberry.lazy("tests.schema.test_lazy.type_c")] ] @strawberry.type class TypeB: @strawberry.field() def type_a( self, ) -> TypeA: from .type_a import TypeA return TypeA() @strawberry.field() def type_a_list( self, ) -> ListTypeA: # pragma: no cover from .type_a import TypeA return [TypeA()] @strawberry.field() def type_c_list( self, ) -> ListTypeC: # pragma: no cover from .type_c import TypeC return [TypeC()] strawberry-graphql-0.287.0/tests/schema/test_lazy/type_c.py000066400000000000000000000010101511033167500237770ustar00rootroot00000000000000import sys from typing import Annotated, Generic, TypeVar import strawberry T = TypeVar("T") @strawberry.type class TypeC: name: str @strawberry.type class Edge(Generic[T]): @strawberry.field def node(self) -> T: # type: ignore ... @strawberry.type class Query: type_a: Edge[TypeC] type_b: Edge[Annotated["TypeC", strawberry.lazy("tests.schema.test_lazy.type_c")]] if __name__ == "__main__": schema = strawberry.Schema(query=Query) sys.stdout.write(f"{schema.as_str()}\n") strawberry-graphql-0.287.0/tests/schema/test_lazy/type_d.py000066400000000000000000000006731511033167500240160ustar00rootroot00000000000000import sys from typing import Annotated, Generic, TypeVar import strawberry T = TypeVar("T") class Mixin(Generic[T]): node: T @strawberry.type class TypeD(Mixin[int]): name: str @strawberry.type class Query: type_d_1: TypeD type_d: Annotated["TypeD", strawberry.lazy("tests.schema.test_lazy.type_d")] if __name__ == "__main__": schema = strawberry.Schema(query=Query) sys.stdout.write(f"{schema.as_str()}\n") strawberry-graphql-0.287.0/tests/schema/test_lazy/type_e.py000066400000000000000000000005351511033167500240140ustar00rootroot00000000000000from enum import Enum from typing import Generic, TypeVar import strawberry T = TypeVar("T") @strawberry.enum class MyEnum(Enum): ONE = "ONE" @strawberry.type class ValueContainer(Generic[T]): value: T UnionValue = strawberry.union( "UnionValue", types=( ValueContainer[int], ValueContainer[MyEnum], ), ) strawberry-graphql-0.287.0/tests/schema/test_lazy_types/000077500000000000000000000000001511033167500233765ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/schema/test_lazy_types/__init__.py000066400000000000000000000000001511033167500254750ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/schema/test_lazy_types/test_cyclic.py000066400000000000000000000010401511033167500262500ustar00rootroot00000000000000import textwrap import strawberry from strawberry.printer import print_schema def test_cyclic_import(): from .type_a import TypeA from .type_b import TypeB @strawberry.type class Query: a: TypeA b: TypeB expected = """ type Query { a: TypeA! b: TypeB! } type TypeA { listOfB: [TypeB!] typeB: TypeB! } type TypeB { typeA: TypeA! } """ schema = strawberry.Schema(Query) assert print_schema(schema) == textwrap.dedent(expected).strip() strawberry-graphql-0.287.0/tests/schema/test_lazy_types/test_lazy_enums.py000066400000000000000000000012621511033167500271760ustar00rootroot00000000000000import enum import textwrap from typing import TYPE_CHECKING import pytest import strawberry from strawberry.printer import print_schema if TYPE_CHECKING: import tests @strawberry.enum class LazyEnum(enum.Enum): BREAD = "BREAD" def test_lazy_enum(): with pytest.deprecated_call(): @strawberry.type class Query: a: strawberry.LazyType[ "LazyEnum", "tests.schema.test_lazy_types.test_lazy_enums" ] expected = """ enum LazyEnum { BREAD } type Query { a: LazyEnum! } """ schema = strawberry.Schema(Query) assert print_schema(schema) == textwrap.dedent(expected).strip() strawberry-graphql-0.287.0/tests/schema/test_lazy_types/test_lazy_unions.py000066400000000000000000000032621511033167500273640ustar00rootroot00000000000000import textwrap from typing import Annotated import strawberry from strawberry.printer import print_schema @strawberry.type class TypeA: a: int @strawberry.type class TypeB: b: int ABUnion = Annotated[TypeA | TypeB, strawberry.union("ABUnion", types=[TypeA, TypeB])] TypeALazy = Annotated[ "TypeA", strawberry.lazy("tests.schema.test_lazy_types.test_lazy_unions") ] TypeBLazy = Annotated[ "TypeB", strawberry.lazy("tests.schema.test_lazy_types.test_lazy_unions") ] LazyABUnion = Annotated[ TypeALazy | TypeBLazy, strawberry.union("LazyABUnion", types=[TypeALazy, TypeBLazy]), ] def test_lazy_union_with_non_lazy_members(): @strawberry.type class Query: ab: Annotated[ "ABUnion", strawberry.lazy("tests.schema.test_lazy_types.test_lazy_unions") ] expected = """ union ABUnion = TypeA | TypeB type Query { ab: ABUnion! } type TypeA { a: Int! } type TypeB { b: Int! } """ schema = strawberry.Schema(query=Query) assert print_schema(schema) == textwrap.dedent(expected).strip() def test_lazy_union_with_lazy_members(): @strawberry.type class Query: ab: Annotated[ "LazyABUnion", strawberry.lazy("tests.schema.test_lazy_types.test_lazy_unions"), ] expected = """ union LazyABUnion = TypeA | TypeB type Query { ab: LazyABUnion! } type TypeA { a: Int! } type TypeB { b: Int! } """ schema = strawberry.Schema(query=Query) assert print_schema(schema) == textwrap.dedent(expected).strip() strawberry-graphql-0.287.0/tests/schema/test_lazy_types/type_a.py000066400000000000000000000007051511033167500252330ustar00rootroot00000000000000from typing import TYPE_CHECKING import strawberry if TYPE_CHECKING: import tests.schema.test_lazy_types from .type_b import TypeB @strawberry.type class TypeA: list_of_b: ( list[strawberry.LazyType["TypeB", "tests.schema.test_lazy_types.type_b"]] | None ) = None @strawberry.field def type_b(self) -> strawberry.LazyType["TypeB", ".type_b"]: # noqa: F722 from .type_b import TypeB return TypeB() strawberry-graphql-0.287.0/tests/schema/test_lazy_types/type_b.py000066400000000000000000000005251511033167500252340ustar00rootroot00000000000000from typing import TYPE_CHECKING import strawberry if TYPE_CHECKING: import tests from .type_a import TypeA @strawberry.type class TypeB: @strawberry.field() def type_a( self, ) -> strawberry.LazyType["TypeA", "tests.schema.test_lazy_types.type_a"]: from .type_a import TypeA return TypeA() strawberry-graphql-0.287.0/tests/schema/test_list.py000066400000000000000000000023001511033167500225130ustar00rootroot00000000000000import strawberry def test_basic_list(): @strawberry.type class Query: @strawberry.field def example(self) -> list[str]: return ["Example"] schema = strawberry.Schema(query=Query) query = "{ example }" result = schema.execute_sync(query, root_value=Query()) assert not result.errors assert result.data["example"] == ["Example"] def test_of_optional(): @strawberry.type class Query: @strawberry.field def example(self) -> list[str | None]: return ["Example", None] schema = strawberry.Schema(query=Query) query = "{ example }" result = schema.execute_sync(query, root_value=Query()) assert not result.errors assert result.data["example"] == ["Example", None] def test_lists_of_lists(): def get_polygons() -> list[list[float]]: return [[2.0, 6.0]] @strawberry.type class Query: polygons: list[list[float]] = strawberry.field(resolver=get_polygons) schema = strawberry.Schema(query=Query) query = "{ polygons }" result = schema.execute_sync(query, root_value=Query()) assert not result.errors assert result.data["polygons"] == [[2.0, 6.0]] strawberry-graphql-0.287.0/tests/schema/test_maybe.py000066400000000000000000000667111511033167500226550ustar00rootroot00000000000000from textwrap import dedent import pytest import strawberry @pytest.fixture def maybe_schema() -> strawberry.Schema: @strawberry.type class User: name: str phone: str | None user = User(name="Patrick", phone=None) @strawberry.type class Query: @strawberry.field def user(self) -> User: return user @strawberry.input class UpdateUserInput: phone: strawberry.Maybe[str | None] @strawberry.type class Mutation: @strawberry.mutation def update_user(self, input: UpdateUserInput) -> User: if input.phone: user.phone = input.phone.value return user return strawberry.Schema(query=Query, mutation=Mutation) user_query = """ { user { phone } } """ def set_phone(schema: strawberry.Schema, phone: str | None) -> dict: query = """ mutation ($phone: String) { updateUser(input: { phone: $phone }) { phone } } """ result = schema.execute_sync(query, variable_values={"phone": phone}) assert not result.errors assert result.data return result.data["updateUser"] def get_user(schema: strawberry.Schema) -> dict: result = schema.execute_sync(user_query) assert not result.errors assert result.data return result.data["user"] def test_maybe(maybe_schema: strawberry.Schema) -> None: assert get_user(maybe_schema)["phone"] is None res = set_phone(maybe_schema, "123") assert res["phone"] == "123" def test_maybe_some_to_none(maybe_schema: strawberry.Schema) -> None: assert get_user(maybe_schema)["phone"] is None set_phone(maybe_schema, "123") res = set_phone(maybe_schema, None) assert res["phone"] is None def test_maybe_absent_value(maybe_schema: strawberry.Schema) -> None: set_phone(maybe_schema, "123") query = """ mutation { updateUser(input: {}) { phone } } """ result = maybe_schema.execute_sync(query) assert not result.errors assert result.data assert result.data["updateUser"]["phone"] == "123" # now check the reverse case. set_phone(maybe_schema, None) result = maybe_schema.execute_sync(query) assert not result.errors assert result.data assert result.data["updateUser"]["phone"] is None def test_optional_argument_maybe() -> None: @strawberry.type class Query: @strawberry.field def hello(self, name: strawberry.Maybe[str | None] = None) -> str: if name: return "None" if name.value is None else name.value return "UNSET" schema = strawberry.Schema(query=Query) assert str(schema) == dedent( """\ type Query { hello(name: String): String! }""" ) result = schema.execute_sync( """ query { hello } """ ) assert not result.errors assert result.data == {"hello": "UNSET"} result = schema.execute_sync( """ query { hello(name: "bar") } """ ) assert not result.errors assert result.data == {"hello": "bar"} result = schema.execute_sync( """ query { hello(name: null) } """ ) assert not result.errors assert result.data == {"hello": "None"} def test_maybe_list(): @strawberry.input class InputData: items: strawberry.Maybe[list[str] | None] @strawberry.type class Query: @strawberry.field def test(self, data: InputData) -> str: return "I am a test, and I received: " + str(data.items) schema = strawberry.Schema(Query) assert str(schema) == dedent( """\ input InputData { items: [String!] } type Query { test(data: InputData!): String! }""" ) def test_maybe_str_rejects_explicit_nulls(): """Test that Maybe[str] correctly rejects null values at Python validation level. BEHAVIOR: - Maybe[str] generates 'String' (optional) in GraphQL schema - Rejects null values at Python validation level, not GraphQL level - This allows the GraphQL parser to accept null, but Python validation rejects it """ @strawberry.input class UpdateInput: name: strawberry.Maybe[str] # Should be optional in schema but reject null @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "world" @strawberry.type class Mutation: @strawberry.mutation def update(self, input: UpdateInput) -> str: if input.name is not None: return f"Updated to: {input.name.value}" return "No change" schema = strawberry.Schema(query=Query, mutation=Mutation) # Generates optional field schema_str = str(schema) assert "name: String" in schema_str assert "name: String!" not in schema_str # Test with explicit null fails at Python validation level query = """ mutation { update(input: { name: null }) } """ result = schema.execute_sync(query) assert result.errors assert len(result.errors) == 1 # The error should mention Python-level validation, not GraphQL schema validation error_message = str(result.errors[0]) assert "Expected value of type" in error_message assert "found null" in error_message def test_maybe_str_accepts_valid_values(): """Test that Maybe[str] accepts valid string values.""" @strawberry.input class UpdateInput: name: strawberry.Maybe[str] @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "world" @strawberry.type class Mutation: @strawberry.mutation def update(self, input: UpdateInput) -> str: if input.name is not None: return f"Updated to: {input.name.value}" return "No change" schema = strawberry.Schema(query=Query, mutation=Mutation) # Test with valid string value should work query = """ mutation { update(input: { name: "John" }) } """ result = schema.execute_sync(query) assert not result.errors assert result.data == {"update": "Updated to: John"} def test_maybe_str_handles_absent_fields(): """Test that Maybe[str] properly handles absent fields. BEHAVIOR: - Absent fields are allowed and result in None value """ @strawberry.input class UpdateInput: name: strawberry.Maybe[str] @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "world" @strawberry.type class Mutation: @strawberry.mutation def update(self, input: UpdateInput) -> str: if input.name is not None: return f"Updated to: {input.name.value}" return "No change" schema = strawberry.Schema(query=Query, mutation=Mutation) # Test with absent field should work and return "No change" query = """ mutation { update(input: {}) } """ result = schema.execute_sync(query) assert not result.errors assert result.data == {"update": "No change"} def test_maybe_str_error_messages(): """Test that Maybe[str] provides error messages when null is rejected.""" @strawberry.input class UpdateInput: name: strawberry.Maybe[str] # Rejects null at Python validation level phone: strawberry.Maybe[str | None] # Can accept null @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "world" @strawberry.type class Mutation: @strawberry.mutation def update(self, input: UpdateInput) -> str: return "Updated" schema = strawberry.Schema(query=Query, mutation=Mutation) # Test that null to name field produces a validation error query = """ mutation { update(input: { name: null, phone: null }) } """ result = schema.execute_sync(query) assert result.errors assert len(result.errors) == 1 # The error should be related to the name field, not phone field error_message = str(result.errors[0]) # Error message from Python validation assert "Expected value of type" in error_message assert "found null" in error_message def test_mixed_maybe_field_behavior(): """Test schema with both Maybe[T] and Maybe[T | None] behave differently.""" @strawberry.input class UpdateUserInput: # Should accept value or absent, reject null username: strawberry.Maybe[str] # Can accept null, value, or absent bio: strawberry.Maybe[str | None] # Can accept null, value, or absent website: strawberry.Maybe[str | None] @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "world" @strawberry.type class Mutation: @strawberry.mutation def update_user(self, input: UpdateUserInput) -> str: result = [] if input.username is not None: result.append(f"username={input.username.value}") else: result.append("username=unchanged") if input.bio is not None: bio_value = ( input.bio.value if input.bio.value is not None else "cleared" ) result.append(f"bio={bio_value}") else: result.append("bio=unchanged") if input.website is not None: website_value = ( input.website.value if input.website.value is not None else "cleared" ) result.append(f"website={website_value}") else: result.append("website=unchanged") return ", ".join(result) schema = strawberry.Schema(query=Query, mutation=Mutation) # Test 1: Valid values for all fields query1 = """ mutation { updateUser(input: { username: "john", bio: "Developer", website: "example.com" }) } """ result1 = schema.execute_sync(query1) assert not result1.errors assert result1.data == { "updateUser": "username=john, bio=Developer, website=example.com" } # Test 2: Null for bio and website should work, but not for username query2 = """ mutation { updateUser(input: { username: null, bio: null, website: null }) } """ result2 = schema.execute_sync(query2) assert result2.errors # Should fail due to username: null # Test 3: Valid bio/website nulls without username query3 = """ mutation { updateUser(input: { bio: null, website: null }) } """ result3 = schema.execute_sync(query3) assert not result3.errors assert result3.data == { "updateUser": "username=unchanged, bio=cleared, website=cleared" } # Test 4: Absent fields should work query4 = """ mutation { updateUser(input: {}) } """ result4 = schema.execute_sync(query4) assert not result4.errors assert result4.data == { "updateUser": "username=unchanged, bio=unchanged, website=unchanged" } def test_maybe_nested_types(): """Test Maybe with nested types like lists.""" @strawberry.input class UpdateItemsInput: # Cannot accept null list - only valid list or absent tags: strawberry.Maybe[list[str]] # Can accept null, valid list, or absent categories: strawberry.Maybe[list[str] | None] @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "world" @strawberry.type class Mutation: @strawberry.mutation def update_items(self, input: UpdateItemsInput) -> str: result = [] if input.tags is not None: result.append(f"tags={input.tags.value}") else: result.append("tags=unchanged") if input.categories is not None: cat_value = ( input.categories.value if input.categories.value is not None else "cleared" ) result.append(f"categories={cat_value}") else: result.append("categories=unchanged") return ", ".join(result) schema = strawberry.Schema(query=Query, mutation=Mutation) # Test 1: Valid lists for both fields query1 = """ mutation { updateItems(input: { tags: ["python", "graphql"], categories: ["tech", "web"] }) } """ result1 = schema.execute_sync(query1) assert not result1.errors assert result1.data == { "updateItems": "tags=['python', 'graphql'], categories=['tech', 'web']" } # Test 2: Null categories should work, but null tags should fail query2 = """ mutation { updateItems(input: { tags: null, categories: null }) } """ result2 = schema.execute_sync(query2) assert result2.errors # Should fail due to tags: null # Test 3: Valid categories null without tags query3 = """ mutation { updateItems(input: { categories: null }) } """ result3 = schema.execute_sync(query3) assert not result3.errors assert result3.data == {"updateItems": "tags=unchanged, categories=cleared"} # Test 4: Empty lists should work for both query4 = """ mutation { updateItems(input: { tags: [], categories: [] }) } """ result4 = schema.execute_sync(query4) assert not result4.errors assert result4.data == {"updateItems": "tags=[], categories=[]"} def test_maybe_resolver_arguments(): """Test Maybe fields as resolver arguments.""" @strawberry.type class Query: @strawberry.field def search( self, # Cannot accept null - only value or absent query: strawberry.Maybe[str] = None, # Can accept null, value, or absent filter_by: strawberry.Maybe[str | None] = None, ) -> str: result = [] if query is not None: result.append(f"query={query.value}") else: result.append("query=unset") if filter_by is not None: filter_value = ( filter_by.value if filter_by.value is not None else "cleared" ) result.append(f"filter={filter_value}") else: result.append("filter=unset") return ", ".join(result) schema = strawberry.Schema(query=Query) # Test 1: Valid values for both arguments query1 = """ query { search(query: "python", filterBy: "category") } """ result1 = schema.execute_sync(query1) assert not result1.errors assert result1.data == {"search": "query=python, filter=category"} # Test 2: Null filter should work, but null query should fail query2 = """ query { search(query: null, filterBy: null) } """ result2 = schema.execute_sync(query2) assert result2.errors # Should fail due to query: null # Test 3: Valid filter null without query query3 = """ query { search(filterBy: null) } """ result3 = schema.execute_sync(query3) assert not result3.errors assert result3.data == {"search": "query=unset, filter=cleared"} # Test 4: No arguments query4 = """ query { search } """ result4 = schema.execute_sync(query4) assert not result4.errors assert result4.data == {"search": "query=unset, filter=unset"} def test_maybe_graphql_schema_consistency(): """Test GraphQL schema generation for Maybe types. BEHAVIOR: - Maybe[str] generates String (optional) - allows absent but rejects null - Maybe[str | None] generates String (optional) - allows absent and null - Both generate the same GraphQL schema but have different validation """ # Schema with Maybe[str] @strawberry.input class Input1: field: strawberry.Maybe[str] @strawberry.type class Query1: @strawberry.field def test(self, input: Input1) -> str: return "test" schema1 = strawberry.Schema(query=Query1) # Schema with Maybe[str | None] @strawberry.input class Input2: field: strawberry.Maybe[str | None] @strawberry.type class Query2: @strawberry.field def test(self, input: Input2) -> str: return "test" schema2 = strawberry.Schema(query=Query2) # Document new behavior schema1_str = str(schema1) schema2_str = str(schema2) # Both Maybe[str] and Maybe[str | None] generate String (optional) assert "field: String" in schema1_str assert "field: String!" not in schema1_str assert "field: String" in schema2_str assert "field: String!" not in schema2_str def test_maybe_complex_types(): """Test Maybe with complex custom types.""" @strawberry.input class AddressInput: street: str city: str zip_code: str | None = None @strawberry.input class UpdateProfileInput: # Cannot accept null address - only valid address or absent address: strawberry.Maybe[AddressInput] # Can accept null, valid address, or absent billing_address: strawberry.Maybe[AddressInput | None] @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "world" @strawberry.type class Mutation: @strawberry.mutation def update_profile(self, input: UpdateProfileInput) -> str: result = [] if input.address is not None: addr = input.address.value result.append(f"address={addr.street}, {addr.city}") else: result.append("address=unchanged") if input.billing_address is not None: if input.billing_address.value is not None: addr = input.billing_address.value result.append(f"billing={addr.street}, {addr.city}") else: result.append("billing=cleared") else: result.append("billing=unchanged") return ", ".join(result) schema = strawberry.Schema(query=Query, mutation=Mutation) # Test 1: Valid addresses for both fields query1 = """ mutation { updateProfile(input: { address: { street: "123 Main", city: "NYC" }, billingAddress: { street: "456 Oak", city: "LA" } }) } """ result1 = schema.execute_sync(query1) assert not result1.errors assert result1.data == { "updateProfile": "address=123 Main, NYC, billing=456 Oak, LA" } # Test 2: Null billing should work, but null address should fail query2 = """ mutation { updateProfile(input: { address: null, billingAddress: null }) } """ result2 = schema.execute_sync(query2) assert result2.errors # Should fail due to address: null # Test 3: Valid billing null without address query3 = """ mutation { updateProfile(input: { billingAddress: null }) } """ result3 = schema.execute_sync(query3) assert not result3.errors assert result3.data == {"updateProfile": "address=unchanged, billing=cleared"} # Test 4: Absent fields query4 = """ mutation { updateProfile(input: {}) } """ result4 = schema.execute_sync(query4) assert not result4.errors assert result4.data == {"updateProfile": "address=unchanged, billing=unchanged"} def test_maybe_union_with_none_works(): """Test that Maybe[T | None] works correctly (this should pass).""" @strawberry.input class TestInput: # This should work correctly - can accept value, null, or absent field: strawberry.Maybe[str | None] @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "world" @strawberry.type class Mutation: @strawberry.mutation def test(self, input: TestInput) -> str: if input.field is not None: if input.field.value is not None: return f"value={input.field.value}" return "null" return "absent" schema = strawberry.Schema(query=Query, mutation=Mutation) # Schema should show optional field schema_str = str(schema) assert "field: String" in schema_str assert "field: String!" not in schema_str # Test valid value result1 = schema.execute_sync('mutation { test(input: { field: "hello" }) }') assert not result1.errors assert result1.data == {"test": "value=hello"} # Test null value result2 = schema.execute_sync("mutation { test(input: { field: null }) }") assert not result2.errors assert result2.data == {"test": "null"} # Test absent field result3 = schema.execute_sync("mutation { test(input: {}) }") assert not result3.errors assert result3.data == {"test": "absent"} def test_maybe_behavior_documented(): """Document the behavior of Maybe[str] vs Maybe[str | None].""" @strawberry.input class CompareInput: # Generates String (optional) but rejects null at Python level required_field: strawberry.Maybe[str] # Generates String (optional) and accepts null optional_field: strawberry.Maybe[str | None] @strawberry.type class Query: @strawberry.field def compare(self, input: CompareInput) -> str: return "test" schema = strawberry.Schema(query=Query) schema_str = str(schema) # Document behavior - both generate optional fields assert "requiredField: String" in schema_str assert "requiredField: String!" not in schema_str assert "optionalField: String" in schema_str assert "optionalField: String!" not in schema_str def test_maybe_schema_generation(): """Test schema behavior - both Maybe[T] and Maybe[T | None] generate optional.""" @strawberry.input class Input1: field: strawberry.Maybe[str] @strawberry.input class Input2: field: strawberry.Maybe[str | None] @strawberry.type class Query: @strawberry.field def test1(self, input: Input1) -> str: return "test" @strawberry.field def test2(self, input: Input2) -> str: return "test" schema = strawberry.Schema(query=Query) schema_str = str(schema) # Both Maybe[str] and Maybe[str | None] generate String (optional) assert "field: String" in schema_str # Both Input1 and Input2 have optional fields assert "field: String!" not in schema_str # No required fields def test_maybe_validation(): """Test that Maybe[T] rejects null at Python validation time, not GraphQL schema time.""" @strawberry.input class TestInput: field: strawberry.Maybe[ str ] # Should be optional in schema but reject null in validation @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "world" @strawberry.type class Mutation: @strawberry.mutation def test(self, input: TestInput) -> str: return "test" schema = strawberry.Schema(query=Query, mutation=Mutation) # Schema should show optional field schema_str = str(schema) assert "field: String" in schema_str assert "field: String!" not in schema_str # Null should be rejected during Python validation, not GraphQL parsing result = schema.execute_sync("mutation { test(input: { field: null }) }") assert result.errors # Error should be a validation error, not a GraphQL parsing error error_message = str(result.errors[0]) assert "Expected value of type" in error_message assert "found null" in error_message def test_maybe_comprehensive_behavior_comparison(): """Comprehensive test comparing Maybe[T] vs Maybe[T | None] behavior.""" @strawberry.input class ComprehensiveInput: # String (optional) - can be value or absent, but rejects null at Python level strict_field: strawberry.Maybe[str] # String (optional) - can be null, value, or absent flexible_field: strawberry.Maybe[str | None] @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "world" @strawberry.type class Mutation: @strawberry.mutation def test_comprehensive(self, input: ComprehensiveInput) -> str: result = [] # This logic works for both current and intended behavior if input.strict_field is not None: result.append(f"strict={input.strict_field.value}") else: result.append("strict=absent") if input.flexible_field is not None: if input.flexible_field.value is not None: result.append(f"flexible={input.flexible_field.value}") else: result.append("flexible=null") else: result.append("flexible=absent") return ", ".join(result) schema = strawberry.Schema(query=Query, mutation=Mutation) schema_str = str(schema) # Document schema generation - both are optional assert "strictField: String" in schema_str assert "strictField: String!" not in schema_str assert "flexibleField: String" in schema_str assert "flexibleField: String!" not in schema_str # Test 1: Valid values work for both result1 = schema.execute_sync(""" mutation { testComprehensive(input: { strictField: "hello", flexibleField: "world" }) } """) assert not result1.errors assert result1.data == {"testComprehensive": "strict=hello, flexible=world"} # Test 2: Only flexible field can be null result2 = schema.execute_sync(""" mutation { testComprehensive(input: { strictField: "hello", flexibleField: null }) } """) assert not result2.errors assert result2.data == {"testComprehensive": "strict=hello, flexible=null"} # Test 3: Strict field null causes Python validation error result3 = schema.execute_sync(""" mutation { testComprehensive(input: { strictField: null, flexibleField: "world" }) } """) assert result3.errors # Python validation error - cannot pass null to Maybe[str] # Test 4: Both fields can be omitted now result4 = schema.execute_sync(""" mutation { testComprehensive(input: { strictField: "hello" }) } """) assert not result4.errors assert result4.data == {"testComprehensive": "strict=hello, flexible=absent"} # Test 5: Strict field can now be omitted (both fields optional in schema) result5 = schema.execute_sync(""" mutation { testComprehensive(input: { flexibleField: "world" }) } """) assert not result5.errors # No error - both fields are optional in schema assert result5.data == {"testComprehensive": "strict=absent, flexible=world"} strawberry-graphql-0.287.0/tests/schema/test_maybe_future_annotations.py000066400000000000000000000026441511033167500266570ustar00rootroot00000000000000from __future__ import annotations from textwrap import dedent import strawberry from strawberry import Maybe def test_maybe_annotation_from_strawberry(): global MyInput try: @strawberry.input class MyInput: my_value: strawberry.Maybe[str] @strawberry.type class Query: @strawberry.field def test(self, my_input: MyInput) -> str: return "OK" schema = strawberry.Schema(query=Query) expected_schema = dedent(""" input MyInput { myValue: String } type Query { test(myInput: MyInput!): String! } """).strip() assert str(schema) == expected_schema assert MyInput() finally: del MyInput def test_maybe_annotation_directly(): global MyInput try: @strawberry.input class MyInput: my_value: Maybe[str] @strawberry.type class Query: @strawberry.field def test(self, my_input: MyInput) -> str: return "OK" schema = strawberry.Schema(query=Query) expected_schema = dedent(""" input MyInput { myValue: String } type Query { test(myInput: MyInput!): String! } """).strip() assert str(schema) == expected_schema assert MyInput() finally: del MyInput strawberry-graphql-0.287.0/tests/schema/test_mutation.py000066400000000000000000000145131511033167500234110ustar00rootroot00000000000000import dataclasses from textwrap import dedent import strawberry from strawberry.types.unset import UNSET def test_mutation(): @strawberry.type class Query: hello: str = "Hello" @strawberry.type class Mutation: @strawberry.mutation def say(self) -> str: return "Hello!" schema = strawberry.Schema(query=Query, mutation=Mutation) query = "mutation { say }" result = schema.execute_sync(query) assert not result.errors assert result.data["say"] == "Hello!" def test_mutation_with_input_type(): @strawberry.input class SayInput: name: str age: int @strawberry.type class Query: hello: str = "Hello" @strawberry.type class Mutation: @strawberry.mutation def say(self, input: SayInput) -> str: return f"Hello {input.name} of {input.age} years old!" schema = strawberry.Schema(query=Query, mutation=Mutation) query = 'mutation { say(input: { name: "Patrick", age: 10 }) }' result = schema.execute_sync(query) assert not result.errors assert result.data["say"] == "Hello Patrick of 10 years old!" def test_mutation_reusing_input_types(): @strawberry.input class SayInput: name: str age: int @strawberry.type class Query: hello: str = "Hello" @strawberry.type class Mutation: @strawberry.mutation def say(self, input: SayInput) -> str: return f"Hello {input.name} of {input.age} years old!" @strawberry.mutation def say2(self, input: SayInput) -> str: return f"Hello {input.name} of {input.age}!" schema = strawberry.Schema(query=Query, mutation=Mutation) query = 'mutation { say2(input: { name: "Patrick", age: 10 }) }' result = schema.execute_sync(query) assert not result.errors assert result.data["say2"] == "Hello Patrick of 10!" def test_unset_types(): @strawberry.type class Query: hello: str = "Hello" @strawberry.input class InputExample: name: str age: int | None = UNSET @strawberry.type class Mutation: @strawberry.mutation def say(self, name: str | None = UNSET) -> str: # type: ignore if name is UNSET: return "Name is unset" return f"Hello {name}!" @strawberry.mutation def say_age(self, input: InputExample) -> str: age = "unset" if input.age is UNSET else input.age return f"Hello {input.name} of age {age}!" schema = strawberry.Schema(query=Query, mutation=Mutation) query = 'mutation { say sayAge(input: { name: "P"}) }' result = schema.execute_sync(query) assert not result.errors assert result.data["say"] == "Name is unset" assert result.data["sayAge"] == "Hello P of age unset!" def test_unset_types_name_with_underscore(): @strawberry.type class Query: hello: str = "Hello" @strawberry.input class InputExample: first_name: str age: str | None = UNSET @strawberry.type class Mutation: @strawberry.mutation def say(self, first_name: str | None = UNSET) -> str: # type: ignore if first_name is UNSET: return "Name is unset" if first_name == "": return "Hello Empty!" return f"Hello {first_name}!" @strawberry.mutation def say_age(self, input: InputExample) -> str: age = "unset" if input.age is UNSET else input.age age = "empty" if age == "" else age return f"Hello {input.first_name} of age {age}!" schema = strawberry.Schema(query=Query, mutation=Mutation) query = """mutation { one: say two: say(firstName: "Patrick") three: say(firstName: "") empty: sayAge(input: { firstName: "Patrick", age: "" }) null: sayAge(input: { firstName: "Patrick", age: null }) sayAge(input: { firstName: "Patrick" }) }""" result = schema.execute_sync(query) assert not result.errors assert result.data["one"] == "Name is unset" assert result.data["two"] == "Hello Patrick!" assert result.data["three"] == "Hello Empty!" assert result.data["empty"] == "Hello Patrick of age empty!" assert result.data["null"] == "Hello Patrick of age None!" assert result.data["sayAge"] == "Hello Patrick of age unset!" def test_unset_types_stringify_empty(): @strawberry.type class Query: hello: str = "Hello" @strawberry.type class Mutation: @strawberry.mutation def say(self, first_name: str | None = UNSET) -> str: # type: ignore return f"Hello {first_name}!" schema = strawberry.Schema(query=Query, mutation=Mutation) query = """mutation { say }""" result = schema.execute_sync(query) assert not result.errors assert result.data["say"] == "Hello !" query = """mutation { say(firstName: null) }""" result = schema.execute_sync(query) assert not result.errors assert result.data["say"] == "Hello None!" def test_converting_to_dict_with_unset(): @strawberry.type class Query: hello: str = "Hello" @strawberry.input class Input: name: str | None = UNSET @strawberry.type class Mutation: @strawberry.mutation def say(self, input: Input) -> str: data = dataclasses.asdict(input) if data["name"] is UNSET: return "Hello 🤨" return f"Hello {data['name']}!" schema = strawberry.Schema(query=Query, mutation=Mutation) query = """mutation { say(input: {}) }""" result = schema.execute_sync(query) assert not result.errors assert result.data["say"] == "Hello 🤨" def test_mutation_deprecation_reason(): @strawberry.type class Query: hello: str = "world" @strawberry.type class Mutation: @strawberry.mutation(deprecation_reason="Your reason") def say(self, name: str) -> str: return f"Hello {name}!" schema = strawberry.Schema(query=Query, mutation=Mutation) assert str(schema) == dedent( """\ type Mutation { say(name: String!): String! @deprecated(reason: "Your reason") } type Query { hello: String! }""" ) strawberry-graphql-0.287.0/tests/schema/test_name_converter.py000066400000000000000000000124761511033167500245660ustar00rootroot00000000000000import textwrap from enum import Enum from typing import Annotated, Generic, TypeVar import strawberry from strawberry.directive import StrawberryDirective from strawberry.schema.config import StrawberryConfig from strawberry.schema.name_converter import NameConverter from strawberry.schema_directive import Location, StrawberrySchemaDirective from strawberry.types.arguments import StrawberryArgument from strawberry.types.base import StrawberryObjectDefinition, StrawberryType from strawberry.types.enum import EnumValue, StrawberryEnumDefinition from strawberry.types.field import StrawberryField from strawberry.types.scalar import ScalarDefinition from strawberry.types.union import StrawberryUnion class AppendsNameConverter(NameConverter): def __init__(self, suffix: str): self.suffix = suffix super().__init__(auto_camel_case=True) def from_argument(self, argument: StrawberryArgument) -> str: return super().from_argument(argument) + self.suffix def from_scalar(self, scalar: ScalarDefinition) -> str: return super().from_scalar(scalar) + self.suffix def from_field(self, field: StrawberryField) -> str: return super().from_field(field) + self.suffix def from_union(self, union: StrawberryUnion) -> str: return super().from_union(union) + self.suffix def from_generic( self, generic_type: StrawberryObjectDefinition, types: list[StrawberryType | type], ) -> str: return super().from_generic(generic_type, types) + self.suffix def from_interface(self, interface: StrawberryObjectDefinition) -> str: return super().from_interface(interface) + self.suffix def from_directive( self, directive: StrawberryDirective | StrawberrySchemaDirective ) -> str: return super().from_directive(directive) + self.suffix def from_input_object(self, input_type: StrawberryObjectDefinition) -> str: return super().from_object(input_type) + self.suffix def from_object(self, object_type: StrawberryObjectDefinition) -> str: return super().from_object(object_type) + self.suffix def from_enum(self, enum: StrawberryEnumDefinition) -> str: return super().from_enum(enum) + self.suffix def from_enum_value( self, enum: StrawberryEnumDefinition, enum_value: EnumValue ) -> str: return super().from_enum_value(enum, enum_value) + self.suffix # TODO: maybe we should have a kitchen sink schema that we can reuse for tests T = TypeVar("T") MyScalar = strawberry.scalar(str, name="SensitiveConfiguration") @strawberry.enum class MyEnum(Enum): A = "a" B = "b" @strawberry.type class User: name: str @strawberry.type(name="MyType") class TypeWithDifferentNameThanClass: name: str @strawberry.type class Error: message: str @strawberry.input class UserInput: name: str @strawberry.schema_directive( locations=[ Location.FIELD_DEFINITION, ] ) class MyDirective: name: str @strawberry.interface class Node: id: strawberry.ID @strawberry.type class MyGeneric(Generic[T]): value: T @strawberry.type class Query: @strawberry.field(directives=[MyDirective(name="my-directive")]) def user(self, input: UserInput) -> User | Error: return User(name="Patrick") enum: MyEnum = MyEnum.A field: MyGeneric[str] | None = None field_with_lazy: MyGeneric[ Annotated[ "TypeWithDifferentNameThanClass", strawberry.lazy("tests.schema.test_name_converter"), ] ] = None @strawberry.field def print(self, enum: MyEnum) -> str: return enum.value schema = strawberry.Schema( query=Query, types=[MyScalar, Node], config=StrawberryConfig(name_converter=AppendsNameConverter("X")), ) def test_name_converter(): expected_schema = """ directive @myDirectiveX(name: String!) on FIELD_DEFINITION type ErrorX { messageX: String! } enum MyEnumX { AX BX } type MyTypeMyGenericXX { valueX: MyTypeX! } type MyTypeX { nameX: String! } interface NodeXX { idX: ID! } type QueryX { enumX: MyEnumX! fieldX: StrMyGenericXX fieldWithLazyX: MyTypeMyGenericXX! userX(inputX: UserInputX!): UserXErrorXX! @myDirectiveX(name: "my-directive") printX(enumX: MyEnumX!): String! } scalar SensitiveConfiguration type StrMyGenericXX { valueX: String! } input UserInputX { nameX: String! } type UserX { nameX: String! } union UserXErrorXX = UserX | ErrorX """ assert textwrap.dedent(expected_schema).strip() == str(schema) def test_returns_enum_with_correct_value(): query = " { enumX } " result = schema.execute_sync(query, root_value=Query()) assert not result.errors assert result.data == {"enumX": "AX"} def test_can_use_enum_value(): query = " { printX(enumX: AX) } " result = schema.execute_sync(query, root_value=Query()) assert not result.errors assert result.data == {"printX": "a"} def test_can_use_enum_value_with_variable(): query = " query ($enum: MyEnumX!) { printX(enumX: $enum) } " result = schema.execute_sync( query, root_value=Query(), variable_values={"enum": "AX"} ) assert not result.errors assert result.data == {"printX": "a"} strawberry-graphql-0.287.0/tests/schema/test_one_of.py000066400000000000000000000153431511033167500230200ustar00rootroot00000000000000from typing import Any import pytest import strawberry from strawberry.schema_directives import OneOf @strawberry.input(one_of=True) class ExampleInputTagged: a: strawberry.Maybe[str | None] b: strawberry.Maybe[int | None] @strawberry.type class ExampleResult: a: str | None b: int | None @strawberry.type class Query: @strawberry.field def test(self, input: ExampleInputTagged) -> ExampleResult: if input.a: return ExampleResult(a=input.a.value, b=None) if input.b: return ExampleResult(a=None, b=input.b.value) return ExampleResult(a=None, b=None) schema = strawberry.Schema(query=Query) @pytest.mark.parametrize( ("default_value", "variables"), [ ("{a: null, b: null}", {}), ('{ a: "abc", b: 123 }', {}), ("{a: null, b: 123}", {}), ("{}", {}), ], ) def test_must_specify_at_least_one_key_default( default_value: str, variables: dict[str, Any] ): query = f""" query ($input: ExampleInputTagged! = {default_value}) {{ test(input: $input) {{ a b }} }} """ result = schema.execute_sync(query, variable_values=variables) assert result.errors assert len(result.errors) == 1 assert ( result.errors[0].message == "OneOf Input Object 'ExampleInputTagged' must specify exactly one key." ) @pytest.mark.parametrize( ("value", "variables"), [ ("{a: null, b: null}", {}), ('{ a: "abc", b: 123 }', {}), ("{a: null, b: 123}", {}), ("{}", {}), ("{ a: $a, b: 123 }", {"a": "abc"}), ("{ a: $a, b: 123 }", {}), ("{ a: $a, b: $b }", {"a": "abc"}), ("$input", {"input": {"a": "abc", "b": 123}}), ("$input", {"input": {"a": "abc", "b": None}}), ("$input", {"input": {}}), ('{ a: "abc", b: null }', {}), ], ) def test_must_specify_at_least_one_key_literal(value: str, variables: dict[str, Any]): variables_definitions = [] if "$a" in value: variables_definitions.append("$a: String") if "$b" in value: variables_definitions.append("$b: Int") if "$input" in value: variables_definitions.append("$input: ExampleInputTagged!") variables_definition_str = ( f"({', '.join(variables_definitions)})" if variables_definitions else "" ) query = f""" query {variables_definition_str} {{ test(input: {value}) {{ a b }} }} """ result = schema.execute_sync(query, variable_values=variables) assert result.errors assert len(result.errors) == 1 assert ( result.errors[0].message == "OneOf Input Object 'ExampleInputTagged' must specify exactly one key." ) def test_value_must_be_non_null_input(): query = """ query ($input: ExampleInputTagged!) { test(input: $input) { a b } } """ result = schema.execute_sync(query, variable_values={"input": {"a": None}}) assert result.errors assert len(result.errors) == 1 assert result.errors[0].message == "Value for member field 'a' must be non-null" def test_value_must_be_non_null_literal(): query = """ query { test(input: { a: null }) { a b } } """ result = schema.execute_sync(query, variable_values={"input": {"a": None}}) assert result.errors assert len(result.errors) == 1 assert result.errors[0].message == "Field 'ExampleInputTagged.a' must be non-null." def test_value_must_be_non_null_variable(): query = """ query ($b: Int) { test(input: { b: $b }) { b } } """ result = schema.execute_sync(query, variable_values={}) assert result.errors assert len(result.errors) == 1 assert ( result.errors[0].message == "Variable 'b' must be non-nullable to be used for OneOf Input Object 'ExampleInputTagged'." ) @pytest.mark.parametrize( ("value", "variables", "expected"), [ ("{ b: $b }", {"b": 123}, {"b": 123}), ("$input", {"input": {"b": 123}}, {"b": 123}), ('{ a: "abc" }', {}, {"a": "abc"}), ("$input", {"input": {"a": "abc"}}, {"a": "abc"}), ], ) def test_works(value: str, variables: dict[str, Any], expected: dict[str, Any]): variables_definitions = [] if "$b" in value: variables_definitions.append("$b: Int!") if "$input" in value: variables_definitions.append("$input: ExampleInputTagged!") variables_definition_str = ( f"({', '.join(variables_definitions)})" if variables_definitions else "" ) field = next(iter(expected.keys())) query = f""" query {variables_definition_str} {{ test(input: {value}) {{ {field} }} }} """ result = schema.execute_sync(query, variable_values=variables) assert not result.errors assert result.data["test"] == expected def test_works_with_camelcasing(): global ExampleWithLongerNames, Result @strawberry.input(directives=[OneOf()]) class ExampleWithLongerNames: a_field: strawberry.Maybe[str | None] b_field: strawberry.Maybe[int | None] @strawberry.type class Result: a_field: str | None b_field: int | None @strawberry.type class Query: @strawberry.field def test(self, input: ExampleWithLongerNames) -> Result: return Result( # noqa: F821 a_field=input.a_field.value if input.a_field else None, b_field=input.b_field.value if input.b_field else None, ) schema = strawberry.Schema(query=Query) query = """ query ($input: ExampleWithLongerNames!) { test(input: $input) { aField bField } } """ result = schema.execute_sync(query, variable_values={"input": {"aField": "abc"}}) assert not result.errors assert result.data["test"] == {"aField": "abc", "bField": None} del ExampleWithLongerNames, Result def test_introspection(): query = """ query { __type(name: "ExampleInputTagged") { name isOneOf } } """ result = schema.execute_sync(query) assert not result.errors assert result.data == {"__type": {"name": "ExampleInputTagged", "isOneOf": True}} def test_introspection_builtin(): query = """ query { __type(name: "String") { name isOneOf } } """ result = schema.execute_sync(query) assert not result.errors assert result.data == {"__type": {"name": "String", "isOneOf": False}} strawberry-graphql-0.287.0/tests/schema/test_permission.py000066400000000000000000000432201511033167500237360ustar00rootroot00000000000000import re import textwrap import typing import pytest import strawberry from strawberry.exceptions import StrawberryGraphQLError from strawberry.exceptions.permission_fail_silently_requires_optional import ( PermissionFailSilentlyRequiresOptionalError, ) from strawberry.permission import BasePermission, PermissionExtension from strawberry.printer import print_schema from strawberry.utils.aio import aclosing def test_raises_graphql_error_when_permission_method_is_missing(): class IsAuthenticated(BasePermission): pass error_msg = ( re.escape("Can't instantiate abstract class IsAuthenticated ") + r"(.*)*" ) with pytest.raises(TypeError, match=error_msg): @strawberry.type class Query: @strawberry.field(permission_classes=[IsAuthenticated]) def user(self) -> str: # pragma: no cover return "patrick" def test_raises_graphql_error_when_permission_is_denied(): class IsAuthenticated(BasePermission): message = "User is not authenticated" def has_permission( self, source: typing.Any, info: strawberry.Info, **kwargs: typing.Any ) -> bool: return False @strawberry.type class Query: @strawberry.field(permission_classes=[IsAuthenticated]) def user(self) -> str: # pragma: no cover return "patrick" schema = strawberry.Schema(query=Query) query = "{ user }" result = schema.execute_sync(query) assert result.errors[0].message == "User is not authenticated" @pytest.mark.asyncio async def test_raises_permission_error_for_subscription(): class IsAdmin(BasePermission): message = "You are not authorized" def has_permission( self, source: typing.Any, info: strawberry.Info, **kwargs: typing.Any ) -> bool: return False @strawberry.type class Query: name: str = "Andrew" @strawberry.type class Subscription: @strawberry.subscription(permission_classes=[IsAdmin]) async def user(self) -> typing.AsyncGenerator[str, None]: # pragma: no cover yield "Hello" schema = strawberry.Schema(query=Query, subscription=Subscription) query = "subscription { user }" async with aclosing(await schema.subscribe(query)) as sub_result: result = await sub_result.__anext__() assert result.errors[0].message == "You are not authorized" @pytest.mark.asyncio async def test_sync_permissions_work_with_async_resolvers(): class IsAuthorized(BasePermission): message = "User is not authorized" def has_permission(self, source, info, **kwargs: typing.Any) -> bool: return info.context["user"] == "Patrick" @strawberry.type class User: name: str email: str @strawberry.type class Query: @strawberry.field(permission_classes=[IsAuthorized]) async def user(self, name: str) -> User: return User(name=name, email="patrick.arminio@gmail.com") schema = strawberry.Schema(query=Query) query = '{ user(name: "patrick") { email } }' result = await schema.execute(query, context_value={"user": "Patrick"}) assert result.data["user"]["email"] == "patrick.arminio@gmail.com" query = '{ user(name: "marco") { email } }' result = await schema.execute(query, context_value={"user": "Marco"}) assert result.errors[0].message == "User is not authorized" def test_can_use_source_when_testing_permission(): class CanSeeEmail(BasePermission): message = "Cannot see email for this user" def has_permission( self, source: typing.Any, info: strawberry.Info, **kwargs: typing.Any ) -> bool: return source.name.lower() == "patrick" @strawberry.type class User: name: str @strawberry.field(permission_classes=[CanSeeEmail]) def email(self) -> str: return "patrick.arminio@gmail.com" @strawberry.type class Query: @strawberry.field def user(self, name: str) -> User: return User(name=name) schema = strawberry.Schema(query=Query) query = '{ user(name: "patrick") { email } }' result = schema.execute_sync(query) assert result.data["user"]["email"] == "patrick.arminio@gmail.com" query = '{ user(name: "marco") { email } }' result = schema.execute_sync(query) assert result.errors[0].message == "Cannot see email for this user" def test_can_use_args_when_testing_permission(): class CanSeeEmail(BasePermission): message = "Cannot see email for this user" def has_permission( self, source: typing.Any, info: strawberry.Info, **kwargs: typing.Any ) -> bool: return kwargs.get("secure", False) @strawberry.type class User: name: str @strawberry.field(permission_classes=[CanSeeEmail]) def email(self, secure: bool) -> str: return "patrick.arminio@gmail.com" @strawberry.type class Query: @strawberry.field def user(self, name: str) -> User: return User(name=name) schema = strawberry.Schema(query=Query) query = '{ user(name: "patrick") { email(secure: true) } }' result = schema.execute_sync(query) assert result.data["user"]["email"] == "patrick.arminio@gmail.com" query = '{ user(name: "patrick") { email(secure: false) } }' result = schema.execute_sync(query) assert result.errors[0].message == "Cannot see email for this user" def test_can_use_on_simple_fields(): class CanSeeEmail(BasePermission): message = "Cannot see email for this user" def has_permission( self, source: typing.Any, info: strawberry.Info, **kwargs: typing.Any ) -> bool: return source.name.lower() == "patrick" @strawberry.type class User: name: str email: str = strawberry.field(permission_classes=[CanSeeEmail]) @strawberry.type class Query: @strawberry.field def user(self, name: str) -> User: return User(name=name, email="patrick.arminio@gmail.com") schema = strawberry.Schema(query=Query) query = '{ user(name: "patrick") { email } }' result = schema.execute_sync(query) assert result.data["user"]["email"] == "patrick.arminio@gmail.com" query = '{ user(name: "marco") { email } }' result = schema.execute_sync(query) assert result.errors[0].message == "Cannot see email for this user" @pytest.mark.asyncio async def test_dataclass_field_with_async_permission_class(): class CanSeeEmail(BasePermission): message = "Cannot see email for this user" async def has_permission(self, source, info, **kwargs: typing.Any) -> bool: return source.name.lower() == "patrick" @strawberry.type class User: name: str email: str = strawberry.field(permission_classes=[CanSeeEmail]) @strawberry.type class Query: @strawberry.field() async def user(self, name: str) -> User: return User(name=name, email="patrick.arminio@gmail.com") schema = strawberry.Schema(query=Query) query = '{ user(name: "patrick") { email } }' result = await schema.execute(query) assert result.data["user"]["email"] == "patrick.arminio@gmail.com" query = '{ user(name: "marco") { email } }' result = await schema.execute(query) assert result.errors[0].message == "Cannot see email for this user" @pytest.mark.asyncio async def test_async_resolver_with_async_permission_class(): class IsAuthorized(BasePermission): message = "User is not authorized" async def has_permission(self, source, info, **kwargs: typing.Any) -> bool: return info.context["user"] == "Patrick" @strawberry.type class User: name: str email: str @strawberry.type class Query: @strawberry.field(permission_classes=[IsAuthorized]) async def user(self, name: str) -> User: return User(name=name, email="patrick.arminio@gmail.com") schema = strawberry.Schema(query=Query) query = '{ user(name: "patrick") { email } }' result = await schema.execute(query, context_value={"user": "Patrick"}) assert result.data["user"]["email"] == "patrick.arminio@gmail.com" query = '{ user(name: "marco") { email } }' result = await schema.execute(query, context_value={"user": "Marco"}) assert result.errors[0].message == "User is not authorized" @pytest.mark.asyncio async def test_sync_resolver_with_async_permission_class(): class IsAuthorized(BasePermission): message = "User is not authorized" async def has_permission(self, source, info, **kwargs: typing.Any) -> bool: return info.context["user"] == "Patrick" @strawberry.type class User: name: str email: str @strawberry.type class Query: @strawberry.field(permission_classes=[IsAuthorized]) def user(self, name: str) -> User: return User(name=name, email="patrick.arminio@gmail.com") schema = strawberry.Schema(query=Query) query = '{ user(name: "patrick") { email } }' result = await schema.execute(query, context_value={"user": "Patrick"}) assert result.data["user"]["email"] == "patrick.arminio@gmail.com" query = '{ user(name: "marco") { email } }' result = await schema.execute(query, context_value={"user": "Marco"}) assert result.errors[0].message == "User is not authorized" @pytest.mark.asyncio async def test_mixed_sync_and_async_permission_classes(): class IsAuthorizedAsync(BasePermission): message = "User is not authorized (async)" async def has_permission(self, source, info, **kwargs: typing.Any) -> bool: return info.context.get("passAsync", False) class IsAuthorizedSync(BasePermission): message = "User is not authorized (sync)" def has_permission(self, source, info, **kwargs: typing.Any) -> bool: return info.context.get("passSync", False) @strawberry.type class User: name: str email: str @strawberry.type class Query: @strawberry.field(permission_classes=[IsAuthorizedAsync, IsAuthorizedSync]) def user(self, name: str) -> User: return User(name=name, email="patrick.arminio@gmail.com") schema = strawberry.Schema(query=Query) query = '{ user(name: "patrick") { email } }' context = {"passAsync": False, "passSync": False} result = await schema.execute(query, context_value=context) assert result.errors[0].message == "User is not authorized (async)" context = {"passAsync": True, "passSync": False} result = await schema.execute(query, context_value=context) assert result.errors[0].message == "User is not authorized (sync)" context = {"passAsync": False, "passSync": True} result = await schema.execute(query, context_value=context) assert result.errors[0].message == "User is not authorized (async)" context = {"passAsync": True, "passSync": True} result = await schema.execute(query, context_value=context) assert result.data["user"]["email"] == "patrick.arminio@gmail.com" def test_permissions_with_custom_extensions(): class IsAuthorized(BasePermission): message = "User is not authorized" error_extensions = {"code": "UNAUTHORIZED"} def has_permission(self, source, info, **kwargs: typing.Any) -> bool: return False @strawberry.type class Query: @strawberry.field(permission_classes=[IsAuthorized]) def name(self) -> str: # pragma: no cover return "ABC" schema = strawberry.Schema(query=Query) query = "{ name }" result = schema.execute_sync(query) assert result.errors[0].message == "User is not authorized" assert result.errors[0].extensions assert result.errors[0].extensions["code"] == "UNAUTHORIZED" def test_permissions_with_custom_extensions_on_custom_error(): class CustomError(StrawberryGraphQLError): def __init__(self, message: str): super().__init__(message, extensions={"general_info": "CUSTOM_ERROR"}) class IsAuthorized(BasePermission): message = "User is not authorized" error_class = CustomError error_extensions = {"code": "UNAUTHORIZED"} def has_permission(self, source, info, **kwargs: typing.Any) -> bool: return False @strawberry.type class Query: @strawberry.field(permission_classes=[IsAuthorized]) def name(self) -> str: # pragma: no cover return "ABC" schema = strawberry.Schema(query=Query) query = "{ name }" result = schema.execute_sync(query) assert result.errors[0].message == "User is not authorized" assert result.errors[0].extensions assert result.errors[0].extensions["code"] == "UNAUTHORIZED" assert result.errors[0].extensions["general_info"] == "CUSTOM_ERROR" def test_silent_permissions_optional(): class IsAuthorized(BasePermission): message = "User is not authorized" def has_permission(self, source, info, **kwargs: typing.Any) -> bool: return False @strawberry.type class Query: @strawberry.field( extensions=[PermissionExtension([IsAuthorized()], fail_silently=True)] ) def name(self) -> str | None: # pragma: no cover return "ABC" schema = strawberry.Schema(query=Query) query = "{ name }" result = schema.execute_sync(query) assert result.data["name"] is None assert result.errors is None def test_silent_permissions_optional_list(): class IsAuthorized(BasePermission): message = "User is not authorized" def has_permission(self, source, info, **kwargs: typing.Any) -> bool: return False @strawberry.type class Query: @strawberry.field( extensions=[PermissionExtension([IsAuthorized()], fail_silently=True)] ) def names(self) -> list[str] | None: # pragma: no cover return ["ABC"] schema = strawberry.Schema(query=Query) query = "{ names }" result = schema.execute_sync(query) assert result.data["names"] == [] assert result.errors is None def test_silent_permissions_list(): class IsAuthorized(BasePermission): message = "User is not authorized" def has_permission(self, source, info, **kwargs: typing.Any) -> bool: return False @strawberry.type class Query: @strawberry.field( extensions=[PermissionExtension([IsAuthorized()], fail_silently=True)] ) def names(self) -> list[str]: # pragma: no cover return ["ABC"] schema = strawberry.Schema(query=Query) query = "{ names }" result = schema.execute_sync(query) assert result.data["names"] == [] assert result.errors is None @pytest.mark.raises_strawberry_exception( PermissionFailSilentlyRequiresOptionalError, match="Cannot use fail_silently=True with a non-optional or non-list field", ) def test_silent_permissions_incompatible_types(): class IsAuthorized(BasePermission): message = "User is not authorized" def has_permission( self, source, info, **kwargs: typing.Any ) -> bool: # pragma: no cover return False @strawberry.type class User: name: str @strawberry.type class Query: @strawberry.field( extensions=[PermissionExtension([IsAuthorized()], fail_silently=True)] ) def name(self) -> User: # pragma: no cover return User(name="ABC") strawberry.Schema(query=Query) def test_permission_directives_added(): class IsAuthorized(BasePermission): message = "User is not authorized" def has_permission( self, source, info, **kwargs: typing.Any ) -> bool: # pragma: no cover return False @strawberry.type class Query: @strawberry.field(extensions=[PermissionExtension([IsAuthorized()])]) def name(self) -> str: # pragma: no cover return "ABC" schema = strawberry.Schema(query=Query) expected_output = """ directive @isAuthorized on FIELD_DEFINITION type Query { name: String! @isAuthorized } """ assert print_schema(schema) == textwrap.dedent(expected_output).strip() def test_permission_directives_not_added_on_field(): class IsAuthorized(BasePermission): message = "User is not authorized" def has_permission( self, source, info, **kwargs: typing.Any ) -> bool: # pragma: no cover return False @strawberry.type class Query: @strawberry.field(permission_classes=[IsAuthorized]) def name(self) -> str: # pragma: no cover return "ABC" schema = strawberry.Schema(query=Query) expected_output = """ type Query { name: String! } """ assert print_schema(schema) == textwrap.dedent(expected_output).strip() def test_basic_permission_access_inputs(): class IsAuthorized(BasePermission): message = "User is not authorized" def has_permission( self, source, info, **kwargs: typing.Any ) -> bool: # pragma: no cover return kwargs["a_key"] == "secret" @strawberry.type class Query: @strawberry.field(permission_classes=[IsAuthorized]) def name(self, a_key: str) -> str: # pragma: no cover return "Erik" schema = strawberry.Schema(query=Query) query = '{ name(aKey: "example") }' result = schema.execute_sync(query) assert result.errors[0].message == "User is not authorized" query = '{ name(aKey: "secret") }' result = schema.execute_sync(query) assert result.data["name"] == "Erik" strawberry-graphql-0.287.0/tests/schema/test_private_field.py000066400000000000000000000072151511033167500243670ustar00rootroot00000000000000from dataclasses import dataclass from typing import Generic, TypeVar import pytest import strawberry from strawberry.annotation import StrawberryAnnotation from strawberry.exceptions import PrivateStrawberryFieldError from strawberry.types.field import StrawberryField def test_private_field(): @strawberry.type class Query: name: str age: strawberry.Private[int] definition = Query.__strawberry_definition__ assert definition.name == "Query" assert len(definition.fields) == 1 assert definition.fields[0].python_name == "name" assert definition.fields[0].graphql_name is None assert definition.fields[0].type is str instance = Query(name="Luke", age=22) assert instance.name == "Luke" assert instance.age == 22 @pytest.mark.raises_strawberry_exception( PrivateStrawberryFieldError, match=("Field age on type Query cannot be both private and a strawberry.field"), ) def test_private_field_with_strawberry_field_error(): @strawberry.type class Query: name: str age: strawberry.Private[int] = strawberry.field(description="🤫") def test_private_field_access_in_resolver(): @strawberry.type class Query: name: str age: strawberry.Private[int] @strawberry.field def age_in_months(self) -> int: return self.age * 12 schema = strawberry.Schema(query=Query) result = schema.execute_sync( "query { ageInMonths }", root_value=Query(name="Dave", age=7) ) assert not result.errors assert result.data == { "ageInMonths": 84, } @strawberry.type class Query: not_seen: "strawberry.Private[SensitiveData]" @strawberry.field def accessible_info(self) -> str: return self.not_seen.info @dataclass class SensitiveData: value: int info: str def test_private_field_with_str_annotations(): """Check compatibility of strawberry.Private with annotations as string.""" schema = strawberry.Schema(query=Query) result = schema.execute_sync( "query { accessibleInfo }", root_value=Query(not_seen=SensitiveData(1, "foo")), ) assert result.data == {"accessibleInfo": "foo"} # Check if querying `notSeen` raises error and no data is returned assert "notSeen" not in str(schema) failed_result = schema.execute_sync( "query { notSeen }", root_value=Query(not_seen=SensitiveData(1, "foo")) ) assert failed_result.data is None def test_private_field_defined_outside_module_scope(): """Check compatibility of strawberry.Private when defined outside module scope.""" global LocallyScopedSensitiveData @strawberry.type class LocallyScopedQuery: not_seen: "strawberry.Private[LocallyScopedSensitiveData]" @strawberry.field def accessible_info(self) -> str: return self.not_seen.info @dataclass class LocallyScopedSensitiveData: value: int info: str schema = strawberry.Schema(query=LocallyScopedQuery) assert "notSeen" not in str(schema) del LocallyScopedSensitiveData def test_private_field_type_resolution_with_generic_type(): """Check strawberry.Private when its argument is a implicit `Any` generic type. Refer to: https://github.com/strawberry-graphql/strawberry/issues/1938 """ T = TypeVar("T") class GenericPrivateType(Generic[T]): pass private_field = StrawberryField( type_annotation=StrawberryAnnotation( annotation="strawberry.Private[GenericPrivateType]", namespace={**globals(), **locals()}, ), ) assert private_field.type == strawberry.Private[GenericPrivateType] strawberry-graphql-0.287.0/tests/schema/test_pydantic.py000066400000000000000000000030561511033167500233640ustar00rootroot00000000000000import pytest import strawberry pytestmark = pytest.mark.pydantic def test_use_alias_as_gql_name(): from pydantic import BaseModel, Field class UserModel(BaseModel): age_: int = Field(..., alias="age_alias") @strawberry.experimental.pydantic.type( UserModel, all_fields=True, use_pydantic_alias=True ) class User: ... @strawberry.type class Query: user: User = strawberry.field(default_factory=lambda: User(age_=5)) schema = strawberry.Schema(query=Query) query = """{ user { __typename, ... on User { age_alias } } }""" result = schema.execute_sync(query, root_value=Query()) assert not result.errors assert result.data["user"] == {"__typename": "User", "age_alias": 5} def test_do_not_use_alias_as_gql_name(): from pydantic import BaseModel, Field class UserModel(BaseModel): age_: int = Field(..., alias="age_alias") @strawberry.experimental.pydantic.type( UserModel, all_fields=True, use_pydantic_alias=False ) class User: ... @strawberry.type class Query: user: User = strawberry.field(default_factory=lambda: User(age_=5)) schema = strawberry.Schema(query=Query) query = """{ user { __typename, ... on User { age_ } } }""" result = schema.execute_sync(query, root_value=Query()) assert not result.errors assert result.data["user"] == {"__typename": "User", "age_": 5} strawberry-graphql-0.287.0/tests/schema/test_resolvers.py000066400000000000000000000370321511033167500235760ustar00rootroot00000000000000# type: ignore from contextlib import nullcontext from typing import Any, Generic, NamedTuple, TypeVar import pytest import strawberry from strawberry.exceptions import ConflictingArgumentsError from strawberry.parent import Parent from strawberry.types.info import Info def test_resolver(): @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "I'm a resolver" schema = strawberry.Schema(query=Query) query = "{ hello }" result = schema.execute_sync(query) assert not result.errors assert result.data["hello"] == "I'm a resolver" @pytest.mark.asyncio async def test_resolver_function(): def function_resolver(root) -> str: return "I'm a function resolver" async def async_resolver(root) -> str: return "I'm an async resolver" def resolve_name(root) -> str: return root.name def resolve_say_hello(root, name: str) -> str: return f"Hello {name}" @strawberry.type class Query: hello: str = strawberry.field(resolver=function_resolver) hello_async: str = strawberry.field(resolver=async_resolver) get_name: str = strawberry.field(resolver=resolve_name) say_hello: str = strawberry.field(resolver=resolve_say_hello) name = "Patrick" schema = strawberry.Schema(query=Query) query = """{ hello helloAsync getName sayHello(name: "Marco") }""" result = await schema.execute(query, root_value=Query()) assert not result.errors assert result.data["hello"] == "I'm a function resolver" assert result.data["helloAsync"] == "I'm an async resolver" assert result.data["getName"] == "Patrick" assert result.data["sayHello"] == "Hello Marco" def test_resolvers_on_types(): def function_resolver(root) -> str: return "I'm a function resolver" def function_resolver_with_params(root, x: str) -> str: return f"I'm {x}" @strawberry.type class Example: hello: str = strawberry.field(resolver=function_resolver) hello_with_params: str = strawberry.field( resolver=function_resolver_with_params ) @strawberry.type class Query: @strawberry.field def example(self) -> Example: return Example() schema = strawberry.Schema(query=Query) query = """{ example { hello helloWithParams(x: "abc") } }""" result = schema.execute_sync(query, root_value=Query()) assert not result.errors assert result.data["example"]["hello"] == "I'm a function resolver" assert result.data["example"]["helloWithParams"] == "I'm abc" def test_optional_info_and_root_params_function_resolver(): def function_resolver() -> str: return "I'm a function resolver" def function_resolver_with_root(root) -> str: return root._example def function_resolver_with_params(x: str) -> str: return f"I'm {x}" @strawberry.type class Query: hello: str = strawberry.field(resolver=function_resolver) hello_with_root: str = strawberry.field(resolver=function_resolver_with_root) hello_with_params: str = strawberry.field( resolver=function_resolver_with_params ) def __post_init__(self): self._example = "Example" schema = strawberry.Schema(query=Query) query = """{ hello helloWithRoot helloWithParams(x: "abc") }""" result = schema.execute_sync(query, root_value=Query()) assert not result.errors assert result.data["hello"] == "I'm a function resolver" assert result.data["helloWithParams"] == "I'm abc" assert result.data["helloWithRoot"] == "Example" def test_optional_info_and_root_params(): @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "I'm a function resolver" @strawberry.field def hello_with_params(self, x: str) -> str: return f"I'm {x}" @strawberry.field def uses_self(self) -> str: return f"I'm {self._example}" def __post_init__(self): self._example = "self" schema = strawberry.Schema(query=Query) query = """{ hello helloWithParams(x: "abc") usesSelf }""" result = schema.execute_sync(query, root_value=Query()) assert not result.errors assert result.data["hello"] == "I'm a function resolver" assert result.data["helloWithParams"] == "I'm abc" assert result.data["usesSelf"] == "I'm self" def test_only_info_function_resolvers(): def function_resolver(info: strawberry.Info) -> str: return f"I'm a function resolver for {info.field_name}" def function_resolver_with_params(info: strawberry.Info, x: str) -> str: return f"I'm {x} for {info.field_name}" @strawberry.type class Query: hello: str = strawberry.field(resolver=function_resolver) hello_with_params: str = strawberry.field( resolver=function_resolver_with_params ) schema = strawberry.Schema(query=Query) query = """{ hello helloWithParams(x: "abc") }""" result = schema.execute_sync(query) assert not result.errors assert result.data["hello"] == "I'm a function resolver for hello" # TODO: in future, should we map names of info.field_name to the matching # dataclass field name? assert result.data["helloWithParams"] == "I'm abc for helloWithParams" def test_classmethod_resolvers(): global User @strawberry.type class User: name: str age: int @classmethod def get_users(cls) -> "list[User]": return [cls(name="Bob", age=10), cls(name="Nancy", age=30)] @strawberry.type class Query: users: list[User] = strawberry.field(resolver=User.get_users) schema = strawberry.Schema(query=Query) query = "{ users { name } }" result = schema.execute_sync(query) assert not result.errors assert result.data == {"users": [{"name": "Bob"}, {"name": "Nancy"}]} del User def test_staticmethod_resolvers(): class Alphabet: @staticmethod def get_letters() -> list[str]: return ["a", "b", "c"] @strawberry.type class Query: letters: list[str] = strawberry.field(resolver=Alphabet.get_letters) schema = strawberry.Schema(query=Query) query = "{ letters }" result = schema.execute_sync(query) assert not result.errors assert result.data == {"letters": ["a", "b", "c"]} def test_lambda_resolvers(): @strawberry.type class Query: letter: str = strawberry.field(resolver=lambda: "λ") schema = strawberry.Schema(query=Query) query = "{ letter }" result = schema.execute_sync(query) assert not result.errors assert result.data == {"letter": "λ"} def test_bounded_instance_method_resolvers(): class CoolClass: def method(self): _ = self return "something" instance = CoolClass() @strawberry.type class Query: blah: str = strawberry.field(resolver=instance.method) schema = strawberry.Schema(query=Query) query = "{ blah }" result = schema.execute_sync(query) assert not result.errors assert result.data == {"blah": "something"} def test_extending_type(): def name_resolver(id: strawberry.ID) -> str: return "Name" def name_2_resolver(id: strawberry.ID) -> str: return "Name 2" @strawberry.type class NameQuery: name: str = strawberry.field(permission_classes=[], resolver=name_resolver) @strawberry.type class ExampleQuery: name_2: str = strawberry.field(permission_classes=[], resolver=name_2_resolver) @strawberry.type class RootQuery(NameQuery, ExampleQuery): pass schema = strawberry.Schema(query=RootQuery) query = '{ name(id: "abc"), name2(id: "abc") }' result = schema.execute_sync(query) assert not result.errors assert result.data == {"name": "Name", "name2": "Name 2"} @pytest.mark.asyncio async def test_async_list_resolver(): @strawberry.type class Query: @strawberry.field async def best_flavours(self) -> list[str]: return ["strawberry", "pistachio"] schema = strawberry.Schema(query=Query) query = "{ bestFlavours }" result = await schema.execute(query, root_value=Query()) assert not result.errors assert result.data["bestFlavours"] == ["strawberry", "pistachio"] def test_can_use_source_as_argument_name(): @strawberry.type class Query: @strawberry.field def hello(self, source: str) -> str: return f"I'm a resolver for {source}" schema = strawberry.Schema(query=Query) query = '{ hello(source: "🍓") }' result = schema.execute_sync(query) assert not result.errors assert result.data["hello"] == "I'm a resolver for 🍓" def test_generic_resolver_factory(): @strawberry.type class AType: some: int T = TypeVar("T") def resolver_factory(strawberry_type: type[T]): def resolver() -> T: return strawberry_type(some=1) return resolver @strawberry.type class Query: a_type: AType = strawberry.field(resolver_factory(AType)) strawberry.Schema(query=Query) schema = strawberry.Schema(query=Query) query = "{ aType { some } }" result = schema.execute_sync(query) assert not result.errors assert result.data == {"aType": {"some": 1}} def test_generic_resolver_optional(): @strawberry.type class AType: some: int T = TypeVar("T") def resolver() -> T | None: return AType(some=1) @strawberry.type class Query: a_type: AType | None = strawberry.field(resolver) strawberry.Schema(query=Query) schema = strawberry.Schema(query=Query) query = "{ aType { some } }" result = schema.execute_sync(query) assert not result.errors assert result.data == {"aType": {"some": 1}} def test_generic_resolver_container(): T = TypeVar("T") @strawberry.type class Container(Generic[T]): item: T @strawberry.type class AType: some: int def resolver() -> Container[T]: return Container(item=AType(some=1)) @strawberry.type class Query: a_type_in_container: Container[AType] = strawberry.field(resolver) strawberry.Schema(query=Query) schema = strawberry.Schema(query=Query) query = "{ aTypeInContainer { item { some } } }" result = schema.execute_sync(query) assert not result.errors assert result.data == {"aTypeInContainer": {"item": {"some": 1}}} def test_generic_resolver_union(): T = TypeVar("T") @strawberry.type class AType: some: int @strawberry.type class OtherType: other: int def resolver() -> T | OtherType: return AType(some=1) @strawberry.type class Query: union_type: AType | OtherType = strawberry.field(resolver) strawberry.Schema(query=Query) schema = strawberry.Schema(query=Query) query = "{ unionType { ... on AType { some } } }" result = schema.execute_sync(query) assert not result.errors assert result.data == {"unionType": {"some": 1}} def test_generic_resolver_list(): T = TypeVar("T") @strawberry.type class AType: some: int def resolver() -> list[T]: return [AType(some=1)] @strawberry.type class Query: list_type: list[AType] = strawberry.field(resolver) strawberry.Schema(query=Query) schema = strawberry.Schema(query=Query) query = "{ listType { some } }" result = schema.execute_sync(query) assert not result.errors assert result.data == {"listType": [{"some": 1}]} def name_based_info(info, icon: str) -> str: return f"I'm a resolver for {icon} {info.field_name}" def type_based_info(info: strawberry.Info, icon: str) -> str: return f"I'm a resolver for {icon} {info.field_name}" def generic_type_based_info(icon: str, info: strawberry.Info) -> str: return f"I'm a resolver for {icon} {info.field_name}" def arbitrarily_named_info(icon: str, info_argument: Info) -> str: return f"I'm a resolver for {icon} {info_argument.field_name}" @pytest.mark.parametrize( ("resolver", "deprecation"), [ pytest.param( name_based_info, pytest.deprecated_call(match="Argument name-based matching of"), ), pytest.param(type_based_info, nullcontext()), pytest.param(generic_type_based_info, nullcontext()), pytest.param(arbitrarily_named_info, nullcontext()), ], ) def test_info_argument(resolver, deprecation): with deprecation: @strawberry.type class ResolverGreeting: hello: str = strawberry.field(resolver=resolver) schema = strawberry.Schema(query=ResolverGreeting) result = schema.execute_sync('{ hello(icon: "🍓") }') assert not result.errors assert result.data["hello"] == "I'm a resolver for 🍓 hello" def test_name_based_info_is_deprecated(): with pytest.deprecated_call(match=r"Argument name-based matching of 'info'"): @strawberry.type class Query: @strawberry.field def foo(info: Any) -> str: ... strawberry.Schema(query=Query) class UserLiteral(NamedTuple): id: str def parent_no_self(parent: Parent[UserLiteral]) -> str: return f"User {parent.id}" class Foo: @staticmethod def static_method_parent(asdf: Parent[UserLiteral]) -> str: return f"User {asdf.id}" @pytest.mark.parametrize( "resolver", [ pytest.param(parent_no_self), pytest.param(Foo.static_method_parent), ], ) def test_parent_argument(resolver): @strawberry.type class User: id: str name: str = strawberry.field(resolver=resolver) @strawberry.type class Query: @strawberry.field def user(self, user_id: str) -> User: return UserLiteral(user_id) schema = strawberry.Schema(query=Query) result = schema.execute_sync('{ user(userId: "🍓") { name } }') assert not result.errors assert result.data["user"]["name"] == "User 🍓" def parent_and_self(self, parent: Parent[UserLiteral]) -> str: raise AssertionError("Unreachable code.") def parent_self_and_root(self, root, parent: Parent[UserLiteral]) -> str: raise AssertionError("Unreachable code.") def self_and_root(self, root) -> str: raise AssertionError("Unreachable code.") def multiple_parents(user: Parent[Any], user2: Parent[Any]) -> str: raise AssertionError("Unreachable code.") def multiple_infos(root, info1: Info, info2: Info) -> str: raise AssertionError("Unreachable code.") @pytest.mark.parametrize( "resolver", [ pytest.param(parent_self_and_root), pytest.param(multiple_parents), pytest.param(multiple_infos), ], ) @pytest.mark.raises_strawberry_exception( ConflictingArgumentsError, match=( "Arguments .* define conflicting resources. " "Only one of these arguments may be defined per resolver." ), ) def test_multiple_conflicting_reserved_arguments(resolver): @strawberry.type class Query: name: str = strawberry.field(resolver=resolver) strawberry.Schema(query=Query) @pytest.mark.parametrize("resolver", [parent_and_self, self_and_root]) def test_self_should_not_raise_conflicting_arguments_error(resolver): @strawberry.type class Query: name: str = strawberry.field(resolver=resolver) strawberry.Schema(query=Query) strawberry-graphql-0.287.0/tests/schema/test_scalars.py000066400000000000000000000264231511033167500232040ustar00rootroot00000000000000from datetime import date, datetime, timedelta, timezone from decimal import Decimal from textwrap import dedent from uuid import UUID import pytest import strawberry from strawberry import scalar from strawberry.exceptions import ScalarAlreadyRegisteredError from strawberry.scalars import JSON, Base16, Base32, Base64 from strawberry.schema.types.base_scalars import Date def test_void_function(): NoneType = type(None) @strawberry.type class Query: @strawberry.field def void_ret(self) -> None: return @strawberry.field def void_ret_crash(self) -> NoneType: return 1 @strawberry.field def void_arg(self, x: None) -> None: return schema = strawberry.Schema(query=Query) assert ( str(schema) == dedent( ''' type Query { voidRet: Void voidRetCrash: Void voidArg(x: Void): Void } """Represents NULL values""" scalar Void ''' ).strip() ) result = schema.execute_sync("query { voidRet }") assert not result.errors assert result.data == { "voidRet": None, } result = schema.execute_sync("query { voidArg (x: null) }") assert not result.errors assert result.data == { "voidArg": None, } result = schema.execute_sync("query { voidArg (x: 1) }") assert result.errors result = schema.execute_sync("query { voidRetCrash }") assert result.errors def test_uuid_field_string_value(): @strawberry.type class Query: unique_id: UUID schema = strawberry.Schema(query=Query) assert ( str(schema) == dedent( """ type Query { uniqueId: UUID! } scalar UUID """ ).strip() ) result = schema.execute_sync( "query { uniqueId }", root_value=Query( unique_id="e350746c-33b6-4469-86b0-5f16e1e12232", ), ) assert not result.errors assert result.data == { "uniqueId": "e350746c-33b6-4469-86b0-5f16e1e12232", } def test_uuid_field_uuid_value(): @strawberry.type class Query: unique_id: UUID schema = strawberry.Schema(query=Query) assert ( str(schema) == dedent( """ type Query { uniqueId: UUID! } scalar UUID """ ).strip() ) result = schema.execute_sync( "query { uniqueId }", root_value=Query( unique_id=UUID("e350746c-33b6-4469-86b0-5f16e1e12232"), ), ) assert not result.errors assert result.data == { "uniqueId": "e350746c-33b6-4469-86b0-5f16e1e12232", } def test_uuid_input(): @strawberry.type class Query: ok: bool @strawberry.type class Mutation: @strawberry.mutation def uuid_input(self, input_id: UUID) -> str: assert isinstance(input_id, UUID) return str(input_id) schema = strawberry.Schema(query=Query, mutation=Mutation) result = schema.execute_sync( """ mutation { uuidInput(inputId: "e350746c-33b6-4469-86b0-5f16e1e12232") } """ ) assert not result.errors assert result.data == { "uuidInput": "e350746c-33b6-4469-86b0-5f16e1e12232", } def test_json(): @strawberry.type class Query: @strawberry.field def echo_json(data: JSON) -> JSON: return data @strawberry.field def echo_json_nullable(data: JSON | None) -> JSON | None: return data schema = strawberry.Schema(query=Query) expected_schema = dedent( ''' """ The `JSON` scalar type represents JSON values as specified by [ECMA-404](https://ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf). """ scalar JSON @specifiedBy(url: "https://ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf") type Query { echoJson(data: JSON!): JSON! echoJsonNullable(data: JSON): JSON } ''' ).strip() assert str(schema) == expected_schema result = schema.execute_sync( """ query { echoJson(data: {hello: {a: 1}, someNumbers: [1, 2, 3], null: null}) echoJsonNullable(data: {hello: {a: 1}, someNumbers: [1, 2, 3], null: null}) } """ ) assert not result.errors assert result.data == { "echoJson": {"hello": {"a": 1}, "someNumbers": [1, 2, 3], "null": None}, "echoJsonNullable": {"hello": {"a": 1}, "someNumbers": [1, 2, 3], "null": None}, } result = schema.execute_sync( """ query { echoJson(data: null) } """ ) assert result.errors # echoJson is not-null null result = schema.execute_sync( """ query { echoJsonNullable(data: null) } """ ) assert not result.errors assert result.data == { "echoJsonNullable": None, } def test_base16(): @strawberry.type class Query: @strawberry.field def base16_encode(data: str) -> Base16: return bytes(data, "utf-8") @strawberry.field def base16_decode(data: Base16) -> str: return data.decode("utf-8") @strawberry.field def base32_encode(data: str) -> Base32: return bytes(data, "utf-8") @strawberry.field def base32_decode(data: Base32) -> str: return data.decode("utf-8") @strawberry.field def base64_encode(data: str) -> Base64: return bytes(data, "utf-8") @strawberry.field def base64_decode(data: Base64) -> str: return data.decode("utf-8") schema = strawberry.Schema(query=Query) assert ( str(schema) == dedent( ''' """Represents binary data as Base16-encoded (hexadecimal) strings.""" scalar Base16 @specifiedBy(url: "https://datatracker.ietf.org/doc/html/rfc4648.html#section-8") """ Represents binary data as Base32-encoded strings, using the standard alphabet. """ scalar Base32 @specifiedBy(url: "https://datatracker.ietf.org/doc/html/rfc4648.html#section-6") """ Represents binary data as Base64-encoded strings, using the standard alphabet. """ scalar Base64 @specifiedBy(url: "https://datatracker.ietf.org/doc/html/rfc4648.html#section-4") type Query { base16Encode(data: String!): Base16! base16Decode(data: Base16!): String! base32Encode(data: String!): Base32! base32Decode(data: Base32!): String! base64Encode(data: String!): Base64! base64Decode(data: Base64!): String! } ''' ).strip() ) result = schema.execute_sync( """ query { base16Encode(data: "Hello") base16Decode(data: "48656c6C6f") # < Mix lowercase and uppercase base32Encode(data: "Hello") base32Decode(data: "JBSWY3dp") # < Mix lowercase and uppercase base64Encode(data: "Hello") base64Decode(data: "SGVsbG8=") } """ ) assert not result.errors assert result.data == { "base16Encode": "48656C6C6F", "base16Decode": "Hello", "base32Encode": "JBSWY3DP", "base32Decode": "Hello", "base64Encode": "SGVsbG8=", "base64Decode": "Hello", } def test_override_built_in_scalars(): EpochDateTime = strawberry.scalar( datetime, serialize=lambda value: int(value.timestamp()), parse_value=lambda value: datetime.fromtimestamp(int(value), timezone.utc), ) @strawberry.type class Query: @strawberry.field def current_time(self) -> datetime: return datetime(2021, 8, 11, 12, 0, tzinfo=timezone.utc) @strawberry.field def isoformat(self, input_datetime: datetime) -> str: return input_datetime.isoformat() schema = strawberry.Schema( Query, scalar_overrides={ datetime: EpochDateTime, }, ) result = schema.execute_sync( """ { currentTime isoformat(inputDatetime: 1628683200) } """ ) assert not result.errors assert result.data["currentTime"] == 1628683200 assert result.data["isoformat"] == "2021-08-11T12:00:00+00:00" def test_override_unknown_scalars(): Duration = strawberry.scalar( timedelta, name="Duration", serialize=timedelta.total_seconds, parse_value=lambda s: timedelta(seconds=s), ) @strawberry.type class Query: @strawberry.field def duration(self, value: timedelta) -> timedelta: return value schema = strawberry.Schema(Query, scalar_overrides={timedelta: Duration}) result = schema.execute_sync("{ duration(value: 10) }") assert not result.errors assert result.data == {"duration": 10} def test_decimal(): @strawberry.type class Query: @strawberry.field def decimal(value: Decimal) -> Decimal: return value schema = strawberry.Schema(query=Query) result = schema.execute_sync( """ query { floatDecimal: decimal(value: 3.14) floatDecimal2: decimal(value: 3.14509999) floatDecimal3: decimal(value: 0.000001) stringDecimal: decimal(value: "3.14") stringDecimal2: decimal(value: "3.1499999991") } """ ) assert not result.errors assert result.data == { "floatDecimal": "3.14", "floatDecimal2": "3.14509999", "floatDecimal3": "0.000001", "stringDecimal": "3.14", "stringDecimal2": "3.1499999991", } @pytest.mark.raises_strawberry_exception( ScalarAlreadyRegisteredError, match="Scalar `MyCustomScalar` has already been registered", ) def test_duplicate_scalars_raises_exception(): MyCustomScalar = strawberry.scalar( str, name="MyCustomScalar", ) MyCustomScalar2 = strawberry.scalar( int, name="MyCustomScalar", ) @strawberry.type class Query: scalar_1: MyCustomScalar scalar_2: MyCustomScalar2 strawberry.Schema(Query) @pytest.mark.raises_strawberry_exception( ScalarAlreadyRegisteredError, match="Scalar `MyCustomScalar` has already been registered", ) def test_duplicate_scalars_raises_exception_using_alias(): MyCustomScalar = scalar( str, name="MyCustomScalar", ) MyCustomScalar2 = scalar( int, name="MyCustomScalar", ) @strawberry.type class Query: scalar_1: MyCustomScalar scalar_2: MyCustomScalar2 strawberry.Schema(Query) def test_optional_scalar_with_or_operator(): """Check `|` operator support with an optional scalar.""" @strawberry.type class Query: date: Date | None schema = strawberry.Schema(query=Query) query = "{ date }" result = schema.execute_sync(query, root_value=Query(date=None)) assert not result.errors assert result.data["date"] is None result = schema.execute_sync(query, root_value=Query(date=date(2020, 1, 1))) assert not result.errors assert result.data["date"] == "2020-01-01" strawberry-graphql-0.287.0/tests/schema/test_schema_generation.py000066400000000000000000000025701511033167500252240ustar00rootroot00000000000000import pytest from graphql import ( GraphQLField, GraphQLNonNull, GraphQLObjectType, GraphQLSchema, GraphQLString, ) from graphql import print_schema as graphql_core_print_schema import strawberry def test_generates_schema(): @strawberry.type class Query: example: str schema = strawberry.Schema(query=Query) target_schema = GraphQLSchema( query=GraphQLObjectType( name="Query", fields={ "example": GraphQLField( GraphQLNonNull(GraphQLString), resolve=lambda obj, info: "world" ) }, ) ) assert schema.as_str().strip() == graphql_core_print_schema(target_schema).strip() def test_schema_introspect_returns_the_introspection_query_result(): @strawberry.type class Query: example: str schema = strawberry.Schema(query=Query) introspection = schema.introspect() assert {"__schema"} == introspection.keys() assert { "queryType", "mutationType", "subscriptionType", "types", "directives", } == introspection["__schema"].keys() def test_schema_fails_on_an_invalid_schema(): @strawberry.type class Query: ... # Type must have at least one field with pytest.raises(ValueError, match=r"Invalid Schema. Errors.*"): strawberry.Schema(query=Query) strawberry-graphql-0.287.0/tests/schema/test_schema_hooks.py000066400000000000000000000020501511033167500242050ustar00rootroot00000000000000import textwrap import strawberry from strawberry.types.base import StrawberryObjectDefinition from strawberry.types.field import StrawberryField def test_can_change_which_fields_are_exposed(): @strawberry.type class User: name: str email: str = strawberry.field(metadata={"tags": ["internal"]}) @strawberry.type class Query: user: User def public_field_filter(field: StrawberryField) -> bool: return "internal" not in field.metadata.get("tags", []) class PublicSchema(strawberry.Schema): def get_fields( self, type_definition: StrawberryObjectDefinition ) -> list[StrawberryField]: fields = super().get_fields(type_definition) return list(filter(public_field_filter, fields)) schema = PublicSchema(query=Query) expected_schema = textwrap.dedent( """ type Query { user: User! } type User { name: String! } """ ).strip() assert schema.as_str() == expected_schema strawberry-graphql-0.287.0/tests/schema/test_subscription.py000066400000000000000000000157471511033167500243070ustar00rootroot00000000000000# ruff: noqa: F821 from __future__ import annotations from collections import abc # noqa: F401 from collections.abc import AsyncGenerator, AsyncIterable, AsyncIterator # noqa: F401 from typing import ( Annotated, Any, ) import pytest import strawberry from strawberry.types.execution import PreExecutionError from strawberry.utils.aio import aclosing @pytest.mark.asyncio async def test_subscription(): @strawberry.type class Query: x: str = "Hello" @strawberry.type class Subscription: @strawberry.subscription async def example(self) -> AsyncGenerator[str, None]: yield "Hi" schema = strawberry.Schema(query=Query, subscription=Subscription) query = "subscription { example }" async with aclosing(await schema.subscribe(query)) as sub_result: result = await sub_result.__anext__() assert not result.errors assert result.data["example"] == "Hi" @pytest.mark.asyncio async def test_subscription_with_permission(): from strawberry import BasePermission class IsAuthenticated(BasePermission): message = "Unauthorized" async def has_permission( self, source: Any, info: strawberry.Info, **kwargs: Any ) -> bool: return True @strawberry.type class Query: x: str = "Hello" @strawberry.type class Subscription: @strawberry.subscription(permission_classes=[IsAuthenticated]) async def example(self) -> AsyncGenerator[str, None]: yield "Hi" schema = strawberry.Schema(query=Query, subscription=Subscription) query = "subscription { example }" async with aclosing(await schema.subscribe(query)) as sub_result: result = await sub_result.__anext__() assert not result.errors assert result.data["example"] == "Hi" @pytest.mark.asyncio async def test_subscription_with_arguments(): @strawberry.type class Query: x: str = "Hello" @strawberry.type class Subscription: @strawberry.subscription async def example(self, name: str) -> AsyncGenerator[str, None]: yield f"Hi {name}" schema = strawberry.Schema(query=Query, subscription=Subscription) query = 'subscription { example(name: "Nina") }' async with aclosing(await schema.subscribe(query)) as sub_result: result = await sub_result.__anext__() assert not result.errors assert result.data["example"] == "Hi Nina" @pytest.mark.parametrize( "return_annotation", [ "AsyncGenerator[str, None]", "AsyncIterable[str]", "AsyncIterator[str]", "abc.AsyncIterator[str]", "abc.AsyncGenerator[str, None]", "abc.AsyncIterable[str]", ], ) @pytest.mark.asyncio async def test_subscription_return_annotations(return_annotation: str): async def async_resolver(): yield "Hi" async_resolver.__annotations__["return"] = return_annotation @strawberry.type class Query: x: str = "Hello" @strawberry.type class Subscription: example = strawberry.subscription(resolver=async_resolver) schema = strawberry.Schema(query=Query, subscription=Subscription) query = "subscription { example }" async with aclosing(await schema.subscribe(query)) as sub_result: result = await sub_result.__anext__() assert not result.errors assert result.data["example"] == "Hi" @pytest.mark.asyncio async def test_subscription_with_unions(): global A, B @strawberry.type class A: a: str @strawberry.type class B: b: str @strawberry.type class Query: x: str = "Hello" @strawberry.type class Subscription: @strawberry.subscription async def example_with_union(self) -> AsyncGenerator[A | B, None]: yield A(a="Hi") schema = strawberry.Schema(query=Query, subscription=Subscription) query = "subscription { exampleWithUnion { ... on A { a } } }" async with aclosing(await schema.subscribe(query)) as sub_result: result = await sub_result.__anext__() assert not result.errors assert result.data["exampleWithUnion"]["a"] == "Hi" del A, B @pytest.mark.asyncio async def test_subscription_with_unions_and_annotated(): global C, D @strawberry.type class C: c: str @strawberry.type class D: d: str @strawberry.type class Query: x: str = "Hello" @strawberry.type class Subscription: @strawberry.subscription async def example_with_annotated_union( self, ) -> AsyncGenerator[Annotated[C | D, strawberry.union("UnionName")], None]: yield C(c="Hi") schema = strawberry.Schema(query=Query, subscription=Subscription) query = "subscription { exampleWithAnnotatedUnion { ... on C { c } } }" async with aclosing(await schema.subscribe(query)) as sub_result: result = await sub_result.__anext__() assert not result.errors assert result.data["exampleWithAnnotatedUnion"]["c"] == "Hi" del C, D @pytest.mark.asyncio async def test_subscription_with_annotated(): @strawberry.type class Query: x: str = "Hello" @strawberry.type class Subscription: @strawberry.subscription async def example( self, ) -> Annotated[AsyncGenerator[str, None], "this doesn't matter"]: yield "Hi" schema = strawberry.Schema(query=Query, subscription=Subscription) query = "subscription { example }" async with aclosing(await schema.subscribe(query)) as sub_result: result = await sub_result.__anext__() assert not result.errors assert result.data["example"] == "Hi" async def test_subscription_immediate_error(): @strawberry.type class Query: x: str = "Hello" @strawberry.type class Subscription: @strawberry.subscription() async def example(self) -> AsyncGenerator[str, None]: return "fds" schema = strawberry.Schema(query=Query, subscription=Subscription) query = """#graphql subscription { example } """ async with aclosing(await schema.subscribe(query)) as sub_result: result = await sub_result.__anext__() assert isinstance(result, PreExecutionError) assert result.errors async def test_wrong_operation_variables(): @strawberry.type class Query: x: str = "Hello" @strawberry.type class Subscription: @strawberry.subscription async def example(self, name: str) -> AsyncGenerator[str, None]: yield f"Hi {name}" # pragma: no cover schema = strawberry.Schema(query=Query, subscription=Subscription) query = """#graphql subscription subOp($opVar: String!){ example(name: $opVar) } """ async with aclosing(await schema.subscribe(query)) as sub_result: result = await sub_result.__anext__() assert result.errors assert ( result.errors[0].message == "Variable '$opVar' of required type 'String!' was not provided." ) strawberry-graphql-0.287.0/tests/schema/test_union.py000066400000000000000000000600711511033167500227010ustar00rootroot00000000000000import textwrap from dataclasses import dataclass from textwrap import dedent from typing import Annotated, Generic, TypeVar, Union import pytest import strawberry from strawberry.exceptions import InvalidUnionTypeError from strawberry.types.lazy_type import lazy def test_union_as_field(): @strawberry.type class A: a: int @strawberry.type class B: b: int @strawberry.type class Query: ab: A | B = strawberry.field(default_factory=lambda: A(a=5)) schema = strawberry.Schema(query=Query) query = """{ ab { __typename, ... on A { a } } }""" result = schema.execute_sync(query, root_value=Query()) assert not result.errors assert result.data["ab"] == {"__typename": "A", "a": 5} def test_union_as_field_inverse(): @strawberry.type class A: a: int @strawberry.type class B: b: int @strawberry.type class Query: ab: A | B = strawberry.field(default_factory=lambda: B(b=5)) schema = strawberry.Schema(query=Query) query = """{ ab { __typename, ... on B { b } } }""" result = schema.execute_sync(query, root_value=Query()) assert not result.errors assert result.data["ab"] == {"__typename": "B", "b": 5} def test_cannot_use_non_strawberry_fields_for_the_union(): @strawberry.type class A: a: int @strawberry.type class B: b: int @strawberry.type class Query: ab: A | B = "ciao" schema = strawberry.Schema(query=Query) query = """{ ab { __typename, ... on A { a } } }""" result = schema.execute_sync(query, root_value=Query()) assert ( result.errors[0].message == 'The type "" cannot be resolved for the field "ab" ' ", are you using a strawberry.field?" ) def test_union_as_mutation_return(): @strawberry.type class A: x: int @strawberry.type class B: y: int @strawberry.type class Mutation: @strawberry.mutation def hello(self) -> A | B: return B(y=5) schema = strawberry.Schema(query=A, mutation=Mutation) query = """ mutation { hello { __typename ... on A { x } ... on B { y } } } """ result = schema.execute_sync(query) assert not result.errors assert result.data["hello"] == {"__typename": "B", "y": 5} def test_types_not_included_in_the_union_are_rejected(): @strawberry.type class Outside: c: int @strawberry.type class A: a: int @strawberry.type class B: b: int @strawberry.type class Mutation: @strawberry.mutation def hello(self) -> A | B: return Outside(c=5) # type:ignore schema = strawberry.Schema(query=A, mutation=Mutation, types=[Outside]) query = """ mutation { hello { __typename ... on A { a } ... on B { b } } } """ result = schema.execute_sync(query) assert ( result.errors[0].message == "The type " "\".Outside'>\"" ' of the field "hello" ' "is not in the list of the types of the union: \"['A', 'B']\"" ) def test_unknown_types_are_rejected(): @strawberry.type class Outside: c: int @strawberry.type class A: a: int @strawberry.type class B: b: int @strawberry.type class Query: @strawberry.field def hello(self) -> A | B: return Outside(c=5) # type:ignore schema = strawberry.Schema(query=Query) query = """ { hello { ... on A { a } } } """ result = schema.execute_sync(query) assert "Outside" in result.errors[0].message def test_named_union(): @strawberry.type class A: a: int @strawberry.type class B: b: int Result = Annotated[A | B, strawberry.union(name="Result")] @strawberry.type class Query: ab: Result = strawberry.field(default_factory=lambda: A(a=5)) schema = strawberry.Schema(query=Query) query = """{ __type(name: "Result") { kind description } ab { __typename, ... on A { a } } }""" result = schema.execute_sync(query, root_value=Query()) assert not result.errors assert result.data["ab"] == {"__typename": "A", "a": 5} assert result.data["__type"] == {"kind": "UNION", "description": None} def test_named_union_description(): @strawberry.type class A: a: int @strawberry.type class B: b: int Result = Annotated[ A | B, strawberry.union(name="Result", description="Example Result") ] @strawberry.type class Query: ab: Result = strawberry.field(default_factory=lambda: A(a=5)) schema = strawberry.Schema(query=Query) query = """{ __type(name: "Result") { kind description } ab { __typename, ... on A { a } } }""" result = schema.execute_sync(query, root_value=Query()) assert not result.errors assert result.data["ab"] == {"__typename": "A", "a": 5} assert result.data["__type"] == {"kind": "UNION", "description": "Example Result"} def test_can_use_union_in_optional(): @strawberry.type class A: a: int @strawberry.type class B: b: int Result = Annotated[A | B, strawberry.union(name="Result")] @strawberry.type class Query: ab: Result | None = None schema = strawberry.Schema(query=Query) query = """{ __type(name: "Result") { kind description } ab { __typename, ... on A { a } } }""" result = schema.execute_sync(query, root_value=Query()) assert not result.errors assert result.data["ab"] is None def test_multiple_unions(): @strawberry.type class CoolType: @strawberry.type class UnionA1: value: int @strawberry.type class UnionA2: value: int @strawberry.type class UnionB1: value: int @strawberry.type class UnionB2: value: int field1: UnionA1 | UnionA2 field2: UnionB1 | UnionB2 schema = strawberry.Schema(query=CoolType) query = """ { __type(name:"CoolType") { name description fields { name } } } """ result = schema.execute_sync(query) assert not result.errors assert result.data["__type"] == { "description": None, "fields": [{"name": "field1"}, {"name": "field2"}], "name": "CoolType", } def test_union_used_multiple_times(): @strawberry.type class A: a: int @strawberry.type class B: b: int MyUnion = Annotated[A | B, strawberry.union("MyUnion")] @strawberry.type class Query: field1: MyUnion field2: MyUnion schema = strawberry.Schema(query=Query) assert schema.as_str() == dedent( """\ type A { a: Int! } type B { b: Int! } union MyUnion = A | B type Query { field1: MyUnion! field2: MyUnion! }""" ) def test_union_explicit_type_resolution(): @dataclass class ADataclass: a: int @strawberry.type class A: a: int @classmethod def is_type_of(cls, obj, _info) -> bool: return isinstance(obj, ADataclass) @strawberry.type class B: b: int MyUnion = Annotated[A | B, strawberry.union("MyUnion")] @strawberry.type class Query: @strawberry.field def my_field(self) -> MyUnion: return ADataclass(a=1) # type: ignore schema = strawberry.Schema(query=Query) query = "{ myField { __typename, ... on A { a }, ... on B { b } } }" result = schema.execute_sync(query) assert not result.errors assert result.data == {"myField": {"__typename": "A", "a": 1}} def test_union_optional_with_or_operator(): """Verify that the `|` operator is supported when annotating unions as optional in schemas. """ @strawberry.type class Cat: name: str @strawberry.type class Dog: name: str animal_union = Annotated[Cat | Dog, strawberry.union("Animal")] @strawberry.type class Query: @strawberry.field def animal(self) -> animal_union | None: return None schema = strawberry.Schema(query=Query) query = """{ animal { __typename } }""" result = schema.execute_sync(query, root_value=Query()) assert not result.errors assert result.data["animal"] is None def test_union_with_input_types(): """Verify that union of input types raises an error.""" @strawberry.type class User: name: str age: int @strawberry.input class A: a: str @strawberry.input class B: b: str @strawberry.input class Input: name: str something: A | B @strawberry.type class Query: @strawberry.field def user(self, data: Input) -> User: return User(name=data.name, age=100) with pytest.raises( TypeError, match="Union for A is not supported because it is an Input type" ): strawberry.Schema(query=Query) def test_union_with_similar_nested_generic_types(): """Previously this failed due to an edge case where Strawberry would choose AContainer as the resolved type for container_b due to the inability to exactly match the nested generic `Container.items`. """ T = TypeVar("T") @strawberry.type class Container(Generic[T]): items: list[T] @strawberry.type class A: a: str @strawberry.type class B: b: int @strawberry.type class Query: @strawberry.field def container_a(self) -> Container[A] | A: return Container(items=[A(a="hello")]) @strawberry.field def container_b(self) -> Container[B] | B: return Container(items=[B(b=3)]) schema = strawberry.Schema(query=Query) query = """ { containerA { __typename ... on AContainer { items { a } } ... on A { a } } } """ result = schema.execute_sync(query) assert result.data["containerA"]["items"][0]["a"] == "hello" query = """ { containerB { __typename ... on BContainer { items { b } } ... on B { b } } } """ result = schema.execute_sync(query) assert result.data["containerB"]["items"][0]["b"] == 3 def test_lazy_union(): """Previously this failed to evaluate generic parameters on lazy types""" TypeA = Annotated["TypeA", lazy("tests.schema.test_lazy_types.type_a")] TypeB = Annotated["TypeB", lazy("tests.schema.test_lazy_types.type_b")] @strawberry.type class Query: @strawberry.field def a(self) -> TypeA | TypeB: from tests.schema.test_lazy_types.type_a import TypeA return TypeA(list_of_b=[]) @strawberry.field def b(self) -> TypeA | TypeB: from tests.schema.test_lazy_types.type_b import TypeB return TypeB() schema = strawberry.Schema(query=Query) query = """ { a { __typename } b { __typename } } """ result = schema.execute_sync(query) assert result.data["a"]["__typename"] == "TypeA" assert result.data["b"]["__typename"] == "TypeB" def test_lazy_union_with_generic(): UnionValue = Annotated["UnionValue", lazy("tests.schema.test_lazy.type_e")] @strawberry.type class Query: @strawberry.field def a(self) -> UnionValue: from tests.schema.test_lazy.type_e import MyEnum, ValueContainer return ValueContainer(value=MyEnum.ONE) schema = strawberry.Schema(query=Query) query = """ { a { __typename } } """ result = schema.execute_sync(query) assert result.data["a"]["__typename"] == "MyEnumValueContainer" @pytest.mark.raises_strawberry_exception( InvalidUnionTypeError, match="Type `int` cannot be used in a GraphQL Union" ) def test_error_with_invalid_annotated_type(): @strawberry.type class Something: h: str AnnotatedInt = Annotated[int, "something_else"] @strawberry.type class Query: union: Something | AnnotatedInt strawberry.Schema(query=Query) @pytest.mark.raises_strawberry_exception( InvalidUnionTypeError, match="Type `int` cannot be used in a GraphQL Union" ) def test_raises_on_union_with_int(): global ICanBeInUnion @strawberry.type class ICanBeInUnion: foo: str @strawberry.type class Query: union: ICanBeInUnion | int strawberry.Schema(query=Query) del ICanBeInUnion @pytest.mark.raises_strawberry_exception( InvalidUnionTypeError, match=r"Type `list\[...\]` cannot be used in a GraphQL Union", ) def test_raises_on_union_with_list_str(): global ICanBeInUnion @strawberry.type class ICanBeInUnion: foo: str @strawberry.type class Query: union: ICanBeInUnion | list[str] strawberry.Schema(query=Query) del ICanBeInUnion @pytest.mark.raises_strawberry_exception( InvalidUnionTypeError, match=r"Type `list\[...\]` cannot be used in a GraphQL Union", ) def test_raises_on_union_with_list_str_38(): global ICanBeInUnion @strawberry.type class ICanBeInUnion: foo: str @strawberry.type class Query: union: ICanBeInUnion | list[str] strawberry.Schema(query=Query) del ICanBeInUnion @pytest.mark.raises_strawberry_exception( InvalidUnionTypeError, match="Type `Always42` cannot be used in a GraphQL Union" ) def test_raises_on_union_of_custom_scalar(): @strawberry.type class ICanBeInUnion: foo: str @strawberry.scalar(serialize=lambda x: 42, parse_value=lambda x: Always42()) class Always42: pass @strawberry.type class Query: union: Annotated[ Always42 | ICanBeInUnion, strawberry.union(name="ExampleUnion") ] strawberry.Schema(query=Query) def test_union_of_unions(): @strawberry.type class User: name: str @strawberry.type class Error: name: str @strawberry.type class SpecificError: name: str @strawberry.type class EvenMoreSpecificError: name: str ErrorUnion = Union[SpecificError, EvenMoreSpecificError] @strawberry.type class Query: user: User | Error error: User | ErrorUnion schema = strawberry.Schema(query=Query) expected_schema = textwrap.dedent( """ type Error { name: String! } type EvenMoreSpecificError { name: String! } type Query { user: UserError! error: UserSpecificErrorEvenMoreSpecificError! } type SpecificError { name: String! } type User { name: String! } union UserError = User | Error union UserSpecificErrorEvenMoreSpecificError = User | SpecificError | EvenMoreSpecificError """ ).strip() assert str(schema) == expected_schema def test_single_union(): @strawberry.type class A: a: int = 5 @strawberry.type class Query: something: Annotated[A, strawberry.union(name="Something")] = strawberry.field( default_factory=A ) schema = strawberry.Schema(query=Query) query = """{ something { __typename, ... on A { a } } }""" assert ( str(schema) == textwrap.dedent( """ type A { a: Int! } type Query { something: Something! } union Something = A """ ).strip() ) result = schema.execute_sync(query, root_value=Query()) assert not result.errors assert result.data["something"] == {"__typename": "A", "a": 5} def test_generic_union_with_annotated(): @strawberry.type class SomeType: id: strawberry.ID name: str @strawberry.type class NotFoundError: id: strawberry.ID message: str T = TypeVar("T") @strawberry.type class ObjectQueries(Generic[T]): @strawberry.field def by_id( self, id: strawberry.ID ) -> Annotated[T | NotFoundError, strawberry.union("ByIdResult")]: ... @strawberry.type class Query: @strawberry.field def some_type_queries(self, id: strawberry.ID) -> ObjectQueries[SomeType]: raise NotImplementedError schema = strawberry.Schema(Query) assert ( str(schema) == textwrap.dedent( """ type NotFoundError { id: ID! message: String! } type Query { someTypeQueries(id: ID!): SomeTypeObjectQueries! } type SomeType { id: ID! name: String! } union SomeTypeByIdResult = SomeType | NotFoundError type SomeTypeObjectQueries { byId(id: ID!): SomeTypeByIdResult! } """ ).strip() ) def test_generic_union_with_annotated_inside(): @strawberry.type class SomeType: id: strawberry.ID name: str @strawberry.type class NotFoundError: id: strawberry.ID message: str T = TypeVar("T") @strawberry.type class ObjectQueries(Generic[T]): @strawberry.field def by_id( self, id: strawberry.ID ) -> T | Annotated[NotFoundError, strawberry.union("ByIdResult")]: ... @strawberry.type class Query: @strawberry.field def some_type_queries(self, id: strawberry.ID) -> ObjectQueries[SomeType]: ... schema = strawberry.Schema(Query) assert ( str(schema) == textwrap.dedent( """ type NotFoundError { id: ID! message: String! } type Query { someTypeQueries(id: ID!): SomeTypeObjectQueries! } type SomeType { id: ID! name: String! } union SomeTypeByIdResult = SomeType | NotFoundError type SomeTypeObjectQueries { byId(id: ID!): SomeTypeByIdResult! } """ ).strip() ) def test_annoted_union_with_two_generics(): @strawberry.type class SomeType: a: str @strawberry.type class OtherType: b: str @strawberry.type class NotFoundError: message: str T = TypeVar("T") U = TypeVar("U") @strawberry.type class UnionObjectQueries(Generic[T, U]): @strawberry.field def by_id( self, id: strawberry.ID ) -> T | Annotated[U | NotFoundError, strawberry.union("ByIdResult")]: ... @strawberry.type class Query: @strawberry.field def some_type_queries( self, id: strawberry.ID ) -> UnionObjectQueries[SomeType, OtherType]: ... schema = strawberry.Schema(Query) assert ( str(schema) == textwrap.dedent( """ type NotFoundError { message: String! } type OtherType { b: String! } type Query { someTypeQueries(id: ID!): SomeTypeOtherTypeUnionObjectQueries! } type SomeType { a: String! } union SomeTypeOtherTypeByIdResult = SomeType | OtherType | NotFoundError type SomeTypeOtherTypeUnionObjectQueries { byId(id: ID!): SomeTypeOtherTypeByIdResult! } """ ).strip() ) def test_union_merging_without_annotated(): @strawberry.type class A: a: int @strawberry.type class B: b: int @strawberry.type class C: c: int a = Union[A, B] b = Union[B, C] c = Union[a, b] @strawberry.type class Query: union_field: c schema = strawberry.Schema(query=Query) assert ( str(schema) == textwrap.dedent( """ type A { a: Int! } union ABC = A | B | C type B { b: Int! } type C { c: Int! } type Query { unionField: ABC! } """ ).strip() ) def test_union_merging_with_annotated(): @strawberry.type class A: a: int @strawberry.type class B: b: int @strawberry.type class C: c: int a = Annotated[A | B, strawberry.union("AorB")] b = Annotated[B | C, strawberry.union("BorC")] c = Union[a, b] @strawberry.type class Query: union_field: c schema = strawberry.Schema(query=Query) assert ( str(schema) == textwrap.dedent( """ type A { a: Int! } union AorBBorC = A | B | C type B { b: Int! } type C { c: Int! } type Query { unionField: AorBBorC! } """ ).strip() ) def test_union_merging_with_annotated_annotated_merge(): @strawberry.type class A: a: int @strawberry.type class B: b: int @strawberry.type class C: c: int a = Annotated[A | B, strawberry.union("AorB")] b = Annotated[B | C, strawberry.union("BorC")] c = Annotated[a | b, strawberry.union("ABC")] @strawberry.type class Query: union_field: c schema = strawberry.Schema(query=Query) assert ( str(schema) == textwrap.dedent( """ type A { a: Int! } union ABC = A | B | C type B { b: Int! } type C { c: Int! } type Query { unionField: ABC! } """ ).strip() ) def test_union_used_inside_generic(): T = TypeVar("T") @strawberry.type class User: name: str age: int @strawberry.type class ProUser: name: str age: float @strawberry.type class GenType(Generic[T]): data: T GeneralUser = Annotated[User | ProUser, strawberry.union("GeneralUser")] @strawberry.type class Response(GenType[GeneralUser]): ... @strawberry.type class Query: @strawberry.field def user(self) -> Response: ... schema = strawberry.Schema(query=Query) assert ( str(schema) == textwrap.dedent( """ union GeneralUser = User | ProUser type ProUser { name: String! age: Float! } type Query { user: Response! } type Response { data: GeneralUser! } type User { name: String! age: Int! } """ ).strip() ) strawberry-graphql-0.287.0/tests/schema/test_union_deprecated.py000066400000000000000000000117421511033167500250620ustar00rootroot00000000000000from dataclasses import dataclass from textwrap import dedent import pytest import strawberry def test_named_union(): @strawberry.type class A: a: int @strawberry.type class B: b: int with pytest.deprecated_call( match="Passing types to `strawberry.union` is deprecated" ): Result = strawberry.union("Result", (A, B)) @strawberry.type class Query: ab: Result = strawberry.field(default_factory=lambda: A(a=5)) schema = strawberry.Schema(query=Query) query = """{ __type(name: "Result") { kind description } ab { __typename, ... on A { a } } }""" result = schema.execute_sync(query, root_value=Query()) assert not result.errors assert result.data["ab"] == {"__typename": "A", "a": 5} assert result.data["__type"] == {"kind": "UNION", "description": None} def test_named_union_description(): @strawberry.type class A: a: int @strawberry.type class B: b: int with pytest.deprecated_call( match="Passing types to `strawberry.union` is deprecated" ): Result = strawberry.union("Result", (A, B), description="Example Result") @strawberry.type class Query: ab: Result = strawberry.field(default_factory=lambda: A(a=5)) schema = strawberry.Schema(query=Query) query = """{ __type(name: "Result") { kind description } ab { __typename, ... on A { a } } }""" result = schema.execute_sync(query, root_value=Query()) assert not result.errors assert result.data["ab"] == {"__typename": "A", "a": 5} assert result.data["__type"] == {"kind": "UNION", "description": "Example Result"} def test_can_use_union_in_optional(): @strawberry.type class A: a: int @strawberry.type class B: b: int with pytest.deprecated_call( match="Passing types to `strawberry.union` is deprecated" ): Result = strawberry.union("Result", (A, B)) @strawberry.type class Query: ab: Result | None = None schema = strawberry.Schema(query=Query) query = """{ __type(name: "Result") { kind description } ab { __typename, ... on A { a } } }""" result = schema.execute_sync(query, root_value=Query()) assert not result.errors assert result.data["ab"] is None def test_union_used_multiple_times(): @strawberry.type class A: a: int @strawberry.type class B: b: int with pytest.deprecated_call( match="Passing types to `strawberry.union` is deprecated" ): MyUnion = strawberry.union("MyUnion", types=(A, B)) @strawberry.type class Query: field1: MyUnion field2: MyUnion schema = strawberry.Schema(query=Query) assert schema.as_str() == dedent( """\ type A { a: Int! } type B { b: Int! } union MyUnion = A | B type Query { field1: MyUnion! field2: MyUnion! }""" ) def test_union_explicit_type_resolution(): @dataclass class ADataclass: a: int @strawberry.type class A: a: int @classmethod def is_type_of(cls, obj, _info) -> bool: return isinstance(obj, ADataclass) @strawberry.type class B: b: int with pytest.deprecated_call( match="Passing types to `strawberry.union` is deprecated" ): MyUnion = strawberry.union("MyUnion", types=(A, B)) @strawberry.type class Query: @strawberry.field def my_field(self) -> MyUnion: return ADataclass(a=1) # type: ignore schema = strawberry.Schema(query=Query) query = "{ myField { __typename, ... on A { a }, ... on B { b } } }" result = schema.execute_sync(query) assert not result.errors assert result.data == {"myField": {"__typename": "A", "a": 1}} def test_union_optional_with_or_operator(): """Verify that the `|` operator is supported when annotating unions as optional in schemas. """ @strawberry.type class Cat: name: str @strawberry.type class Dog: name: str with pytest.deprecated_call( match="Passing types to `strawberry.union` is deprecated" ): animal_union = strawberry.union("Animal", (Cat, Dog)) @strawberry.type class Query: @strawberry.field def animal(self) -> animal_union | None: return None schema = strawberry.Schema(query=Query) query = """{ animal { __typename } }""" result = schema.execute_sync(query, root_value=Query()) assert not result.errors assert result.data["animal"] is None strawberry-graphql-0.287.0/tests/schema/test_unresolved_fields.py000066400000000000000000000016551511033167500252700ustar00rootroot00000000000000import pytest import strawberry from strawberry.exceptions.unresolved_field_type import UnresolvedFieldTypeError @pytest.mark.raises_strawberry_exception( UnresolvedFieldTypeError, match=( "Could not resolve the type of 'user'. Check that " "the class is accessible from the global module scope." ), ) def test_unresolved_field_fails(): @strawberry.type class Query: user: "User" # type: ignore # noqa: F821 strawberry.Schema(query=Query) @pytest.mark.raises_strawberry_exception( UnresolvedFieldTypeError, match=( "Could not resolve the type of 'user'. Check that " "the class is accessible from the global module scope." ), ) def test_unresolved_field_with_resolver_fails(): @strawberry.type class Query: @strawberry.field def user(self) -> "User": # type: ignore # noqa: F821 ... strawberry.Schema(query=Query) strawberry-graphql-0.287.0/tests/schema/types/000077500000000000000000000000001511033167500213005ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/schema/types/__init__.py000066400000000000000000000000001511033167500233770ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/schema/types/test_date.py000066400000000000000000000060261511033167500236320ustar00rootroot00000000000000import datetime import pytest from graphql import GraphQLError import strawberry from strawberry.types.execution import ExecutionResult def test_serialization(): @strawberry.type class Query: @strawberry.field def serialize(self) -> datetime.date: return datetime.date(2019, 10, 25) schema = strawberry.Schema(Query) result = schema.execute_sync("{ serialize }") assert not result.errors assert result.data["serialize"] == "2019-10-25" def test_deserialization(): @strawberry.type class Query: deserialized = None @strawberry.field def deserialize(self, arg: datetime.date) -> bool: Query.deserialized = arg return True schema = strawberry.Schema(Query) query = """query Deserialize($value: Date!) { deserialize(arg: $value) }""" result = schema.execute_sync(query, variable_values={"value": "2019-10-25"}) assert not result.errors assert Query.deserialized == datetime.date(2019, 10, 25) def test_deserialization_with_parse_literal(): @strawberry.type class Query: deserialized = None @strawberry.field def deserialize(self, arg: datetime.date) -> bool: Query.deserialized = arg return True schema = strawberry.Schema(Query) query = """query Deserialize { deserialize(arg: "2019-10-25") }""" result = schema.execute_sync(query) assert not result.errors assert Query.deserialized == datetime.date(2019, 10, 25) def execute_mutation(value) -> ExecutionResult: @strawberry.type class Query: ok: bool @strawberry.type class Mutation: @strawberry.mutation def date_input(self, date_input: datetime.date) -> datetime.date: assert isinstance(date_input, datetime.date) return date_input schema = strawberry.Schema(query=Query, mutation=Mutation) return schema.execute_sync( """ mutation dateInput($value: Date!) { dateInput(dateInput: $value) } """, variable_values={"value": value}, ) @pytest.mark.parametrize( "value", [ "2012-12-01T09:00", "2012-13-01", "2012-04-9", # this might have been fixed in 3.11 # "20120411", ], ) def test_serialization_of_incorrect_date_string(value): """Test GraphQLError is raised for incorrect date. The error should exclude "original_error". """ result = execute_mutation(value) assert result.errors assert isinstance(result.errors[0], GraphQLError) def test_serialization_error_message_for_incorrect_date_string(): """Test if error message is using original error message from date lib, and is properly formatted. """ result = execute_mutation("2021-13-01") assert result.errors assert result.errors[0].message.startswith( "Variable '$value' got invalid value '2021-13-01'; Value cannot represent a " 'Date: "2021-13-01". month must be in 1..12' ) strawberry-graphql-0.287.0/tests/schema/types/test_datetime.py000066400000000000000000000111211511033167500245010ustar00rootroot00000000000000import datetime import dateutil.tz import pytest from graphql import GraphQLError import strawberry from strawberry.types.execution import ExecutionResult @pytest.mark.parametrize( ("typing", "instance", "serialized"), [ (datetime.date, datetime.date(2019, 10, 25), "2019-10-25"), ( datetime.datetime, datetime.datetime(2019, 10, 25, 13, 37), "2019-10-25T13:37:00", ), (datetime.time, datetime.time(13, 37), "13:37:00"), ], ) def test_serialization(typing, instance, serialized): @strawberry.type class Query: @strawberry.field def serialize(self) -> typing: return instance schema = strawberry.Schema(Query) result = schema.execute_sync("{ serialize }") assert not result.errors assert result.data["serialize"] == serialized @pytest.mark.parametrize( ("typing", "name", "instance", "serialized"), [ (datetime.date, "Date", datetime.date(2019, 10, 25), "2019-10-25"), ( datetime.datetime, "DateTime", datetime.datetime(2019, 10, 25, 13, 37), "2019-10-25T13:37:00", ), ( datetime.datetime, "DateTime", datetime.datetime(2019, 10, 25, 13, 37, tzinfo=dateutil.tz.tzutc()), "2019-10-25T13:37:00Z", ), (datetime.time, "Time", datetime.time(13, 37), "13:37:00"), ], ) def test_deserialization(typing, name, instance, serialized): @strawberry.type class Query: deserialized = None @strawberry.field def deserialize(self, arg: typing) -> bool: Query.deserialized = arg return True schema = strawberry.Schema(Query) query = f"""query Deserialize($value: {name}!) {{ deserialize(arg: $value) }}""" result = schema.execute_sync(query, variable_values={"value": serialized}) assert not result.errors assert Query.deserialized == instance @pytest.mark.parametrize( ("typing", "instance", "serialized"), [ (datetime.date, datetime.date(2019, 10, 25), "2019-10-25"), ( datetime.datetime, datetime.datetime(2019, 10, 25, 13, 37), "2019-10-25T13:37:00", ), (datetime.time, datetime.time(13, 37), "13:37:00"), ], ) def test_deserialization_with_parse_literal(typing, instance, serialized): @strawberry.type class Query: deserialized = None @strawberry.field def deserialize(self, arg: typing) -> bool: Query.deserialized = arg return True schema = strawberry.Schema(Query) query = f"""query Deserialize {{ deserialize(arg: "{serialized}") }}""" result = schema.execute_sync(query) assert not result.errors assert Query.deserialized == instance def execute_mutation(value) -> ExecutionResult: @strawberry.type class Query: ok: bool @strawberry.type class Mutation: @strawberry.mutation def datetime_input( self, datetime_input: datetime.datetime ) -> datetime.datetime: assert isinstance(datetime_input, datetime.datetime) return datetime_input schema = strawberry.Schema(query=Query, mutation=Mutation) return schema.execute_sync( """ mutation datetimeInput($value: DateTime!) { datetimeInput(datetimeInput: $value) } """, variable_values={"value": value}, ) @pytest.mark.parametrize( "value", [ "2012-13-01", "2012-04-9", "20120411T03:30+", "20120411T03:30+1234567", "20120411T03:30-25:40", "20120411T03:30+00:60", "20120411T03:30+00:61", "20120411T033030.123456012:002014-03-12T12:30:14", "2014-04-21T24:00:01", ], ) def test_serialization_of_incorrect_datetime_string(value): """Test GraphQLError is raised for incorrect datetime. The error should exclude "original_error". """ result = execute_mutation(value) assert result.errors assert isinstance(result.errors[0], GraphQLError) def test_serialization_error_message_for_incorrect_datetime_string(): """Test if error message is using original error message from datetime lib, and is properly formatted. """ result = execute_mutation("2021-13-01T09:00:00") assert result.errors assert result.errors[0].message.startswith( "Variable '$value' got invalid value '2021-13-01T09:00:00'; Value cannot " 'represent a DateTime: "2021-13-01T09:00:00". month must be in 1..12' ) strawberry-graphql-0.287.0/tests/schema/types/test_decimal.py000066400000000000000000000033051511033167500243100ustar00rootroot00000000000000from decimal import Decimal from graphql import GraphQLError import strawberry def test_decimal(): @strawberry.type class Query: @strawberry.field def example_decimal(self) -> Decimal: return Decimal("3.14159") schema = strawberry.Schema(Query) result = schema.execute_sync("{ exampleDecimal }") assert not result.errors assert result.data["exampleDecimal"] == "3.14159" def test_decimal_as_input(): @strawberry.type class Query: @strawberry.field def example_decimal(self, decimal: Decimal) -> Decimal: return decimal schema = strawberry.Schema(Query) result = schema.execute_sync('{ exampleDecimal(decimal: "3.14") }') assert not result.errors assert result.data["exampleDecimal"] == "3.14" def test_serialization_of_incorrect_decimal_string(): """Test GraphQLError is raised for an invalid Decimal. The error should exclude "original_error". """ @strawberry.type class Query: ok: bool @strawberry.type class Mutation: @strawberry.mutation def decimal_input(self, decimal_input: Decimal) -> Decimal: return decimal_input schema = strawberry.Schema(query=Query, mutation=Mutation) result = schema.execute_sync( """ mutation decimalInput($value: Decimal!) { decimalInput(decimalInput: $value) } """, variable_values={"value": "fail"}, ) assert result.errors assert isinstance(result.errors[0], GraphQLError) assert result.errors[0].message == ( "Variable '$value' got invalid value 'fail'; Value cannot represent a " 'Decimal: "fail".' ) strawberry-graphql-0.287.0/tests/schema/types/test_time.py000066400000000000000000000057011511033167500236520ustar00rootroot00000000000000import datetime import pytest from graphql import GraphQLError import strawberry from strawberry.types.execution import ExecutionResult def test_serialization(): @strawberry.type class Query: @strawberry.field def serialize(self) -> datetime.time: return datetime.time(13, 37) schema = strawberry.Schema(Query) result = schema.execute_sync("{ serialize }") assert not result.errors assert result.data["serialize"] == "13:37:00" def test_deserialization(): @strawberry.type class Query: deserialized = None @strawberry.field def deserialize(self, arg: datetime.time) -> bool: Query.deserialized = arg return True schema = strawberry.Schema(Query) query = """query Deserialize($value: Time!) { deserialize(arg: $value) }""" result = schema.execute_sync(query, variable_values={"value": "13:37:00"}) assert not result.errors assert Query.deserialized == datetime.time(13, 37) def test_deserialization_with_parse_literal(): @strawberry.type class Query: deserialized = None @strawberry.field def deserialize(self, arg: datetime.time) -> bool: Query.deserialized = arg return True schema = strawberry.Schema(Query) query = """query Deserialize { deserialize(arg: "13:37:00") }""" result = schema.execute_sync(query) assert not result.errors assert Query.deserialized == datetime.time(13, 37) def execute_mutation(value) -> ExecutionResult: @strawberry.type class Query: ok: bool @strawberry.type class Mutation: @strawberry.mutation def time_input(self, time_input: datetime.time) -> datetime.time: assert isinstance(time_input, datetime.time) return time_input schema = strawberry.Schema(query=Query, mutation=Mutation) return schema.execute_sync( """ mutation timeInput($value: Time!) { timeInput(timeInput: $value) } """, variable_values={"value": value}, ) @pytest.mark.parametrize( "value", [ "2012-12-01T09:00", "03:30+", "03:30+1234567", "03:30-25:40", ], ) def test_serialization_of_incorrect_time_string(value): """Test GraphQLError is raised for incorrect time. The error should exclude "original_error". """ result = execute_mutation(value) assert result.errors assert isinstance(result.errors[0], GraphQLError) def test_serialization_error_message_for_incorrect_time_string(): """Test if error message is using original error message from time lib, and is properly formatted. """ result = execute_mutation("25:00") assert result.errors assert result.errors[0].message.startswith( "Variable '$value' got invalid value '25:00'; Value cannot represent a " 'Time: "25:00". hour must be in 0..23' ) strawberry-graphql-0.287.0/tests/schema/types/test_uuid.py000066400000000000000000000033471511033167500236660ustar00rootroot00000000000000import uuid from graphql import GraphQLError import strawberry def test_uuid(): @strawberry.type class Query: @strawberry.field def example_uuid_out(self) -> uuid.UUID: return uuid.NAMESPACE_DNS schema = strawberry.Schema(Query) result = schema.execute_sync("{ exampleUuidOut }") assert not result.errors assert result.data["exampleUuidOut"] == str(uuid.NAMESPACE_DNS) def test_uuid_as_input(): @strawberry.type class Query: @strawberry.field def example_uuid_in(self, uid: uuid.UUID) -> uuid.UUID: return uid schema = strawberry.Schema(Query) result = schema.execute_sync(f'{{ exampleUuidIn(uid: "{uuid.NAMESPACE_DNS!s}") }}') assert not result.errors assert result.data["exampleUuidIn"] == str(uuid.NAMESPACE_DNS) def test_serialization_of_incorrect_uuid_string(): """Test GraphQLError is raised for an invalid UUID. The error should exclude "original_error". """ @strawberry.type class Query: ok: bool @strawberry.type class Mutation: @strawberry.mutation def uuid_input(self, uuid_input: uuid.UUID) -> uuid.UUID: return uuid_input schema = strawberry.Schema(query=Query, mutation=Mutation) result = schema.execute_sync( """ mutation uuidInput($value: UUID!) { uuidInput(uuidInput: $value) } """, variable_values={"value": "fail"}, ) assert result.errors assert isinstance(result.errors[0], GraphQLError) assert result.errors[0].message == ( "Variable '$value' got invalid value 'fail'; Value cannot represent a " 'UUID: "fail". badly formed hexadecimal UUID string' ) strawberry-graphql-0.287.0/tests/schema/validation_rules/000077500000000000000000000000001511033167500235005ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/schema/validation_rules/__init__.py000066400000000000000000000000001511033167500255770ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/schema/validation_rules/test_maybe_null.py000066400000000000000000000167351511033167500272540ustar00rootroot00000000000000import strawberry def test_maybe_null_validation_rule_input_fields(): """Test MaybeNullValidationRule validates input object fields correctly.""" @strawberry.input class TestInput: strict_field: strawberry.Maybe[str] # Should reject null flexible_field: strawberry.Maybe[str | None] # Should allow null @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "world" @strawberry.type class Mutation: @strawberry.mutation def test(self, input: TestInput) -> str: return "success" schema = strawberry.Schema(query=Query, mutation=Mutation) # Test 1: Valid values should work result = schema.execute_sync(""" mutation { test(input: { strictField: "hello", flexibleField: "world" }) } """) assert not result.errors assert result.data == {"test": "success"} # Test 2: Flexible field can be null result = schema.execute_sync(""" mutation { test(input: { strictField: "hello", flexibleField: null }) } """) assert not result.errors assert result.data == {"test": "success"} # Test 3: Strict field cannot be null result = schema.execute_sync(""" mutation { test(input: { strictField: null, flexibleField: "world" }) } """) assert result.errors assert len(result.errors) == 1 error = result.errors[0] assert "Expected value of type 'str', found null" in str(error) assert "strictField" in str(error) assert "cannot be explicitly set to null" in str(error) assert "Use 'Maybe[str | None]'" in str(error) def test_maybe_null_validation_rule_resolver_arguments(): """Test MaybeNullValidationRule validates resolver arguments correctly.""" @strawberry.type class Query: @strawberry.field def search( self, query: strawberry.Maybe[str] = None, # Should reject null filter_by: strawberry.Maybe[str | None] = None, # Should allow null ) -> str: return "success" schema = strawberry.Schema(query=Query) # Test 1: Valid values should work result = schema.execute_sync(""" query { search(query: "hello", filterBy: "world") } """) assert not result.errors assert result.data == {"search": "success"} # Test 2: Flexible argument can be null result = schema.execute_sync(""" query { search(query: "hello", filterBy: null) } """) assert not result.errors assert result.data == {"search": "success"} # Test 3: Strict argument cannot be null result = schema.execute_sync(""" query { search(query: null, filterBy: "world") } """) assert result.errors assert len(result.errors) == 1 error = result.errors[0] assert "Expected value of type 'str', found null" in str(error) assert "query" in str(error) assert "cannot be explicitly set to null" in str(error) assert "Use 'Maybe[str | None]'" in str(error) def test_maybe_null_validation_rule_multiple_errors(): """Test that multiple null violations are all reported.""" @strawberry.input class TestInput: field1: strawberry.Maybe[str] field2: strawberry.Maybe[int] field3: strawberry.Maybe[str | None] # This one allows null @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "world" @strawberry.type class Mutation: @strawberry.mutation def test(self, input: TestInput) -> str: return "success" schema = strawberry.Schema(query=Query, mutation=Mutation) # Test with multiple nulls - should get multiple errors result = schema.execute_sync(""" mutation { test(input: { field1: null, field2: null, field3: null }) } """) assert result.errors assert len(result.errors) == 2 # field1 and field2 should fail, field3 should pass error_messages = [str(error) for error in result.errors] assert any("field1" in msg for msg in error_messages) assert any("field2" in msg for msg in error_messages) # field3 should NOT generate an error because it allows null def test_maybe_null_validation_rule_nested_input(): """Test validation works with nested input objects.""" @strawberry.input class NestedInput: value: strawberry.Maybe[str] @strawberry.input class TestInput: nested: NestedInput @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "world" @strawberry.type class Mutation: @strawberry.mutation def test(self, input: TestInput) -> str: return "success" schema = strawberry.Schema(query=Query, mutation=Mutation) # Test with null in nested input result = schema.execute_sync(""" mutation { test(input: { nested: { value: null } }) } """) assert result.errors assert len(result.errors) == 1 error = result.errors[0] assert "Expected value of type 'str', found null" in str(error) assert "value" in str(error) def test_maybe_null_validation_rule_different_types(): """Test validation works with different field types.""" @strawberry.input class TestInput: string_field: strawberry.Maybe[str] int_field: strawberry.Maybe[int] bool_field: strawberry.Maybe[bool] list_field: strawberry.Maybe[list[str]] @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "world" @strawberry.type class Mutation: @strawberry.mutation def test(self, input: TestInput) -> str: return "success" schema = strawberry.Schema(query=Query, mutation=Mutation) # Test each field type with null test_cases = [ ("stringField", "str"), ("intField", "int"), ("boolField", "bool"), ("listField", "list[str]"), ] for field_name, type_name in test_cases: result = schema.execute_sync(f""" mutation {{ test(input: {{ {field_name}: null }}) }} """) assert result.errors assert len(result.errors) == 1 error = result.errors[0] assert f"Expected value of type '{type_name}', found null" in str(error) def test_maybe_null_validation_rule_custom_graphql_names(): """Test validation works with custom GraphQL field names.""" @strawberry.input class TestInput: internal_name: strawberry.Maybe[str] = strawberry.field(name="customName") @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "world" @strawberry.type class Mutation: @strawberry.mutation def test(self, input: TestInput) -> str: return "success" schema = strawberry.Schema(query=Query, mutation=Mutation) # Test with custom GraphQL name result = schema.execute_sync(""" mutation { test(input: { customName: null }) } """) assert result.errors assert len(result.errors) == 1 error = result.errors[0] assert "customName" in str(error) # TODO: Add test for auto_camel_case=False configuration # This requires accessing the schema configuration from the validation rule context, # which needs further investigation of the GraphQL validation context API. strawberry-graphql-0.287.0/tests/schema_codegen/000077500000000000000000000000001511033167500216205ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/schema_codegen/__init__.py000066400000000000000000000000001511033167500237170ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/schema_codegen/snapshots/000077500000000000000000000000001511033167500236425ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/schema_codegen/snapshots/long_descriptions.py000066400000000000000000000021751511033167500277460ustar00rootroot00000000000000import strawberry @strawberry.type(description="A connection to a list of items.") class FilmCharactersConnection: page_info: PageInfo = strawberry.field(description="Information to aid in pagination.") edges: list[FilmCharactersEdge | None] | None = strawberry.field(description="A list of edges.") total_count: int | None = strawberry.field(description=""" A count of the total number of objects in this connection, ignoring pagination. This allows a client to fetch the first five objects by passing "5" as the argument to "first", then fetch the total count so it could display "5 of 83", for example. """) characters: list[Person | None] | None = strawberry.field(description=""" A list of all of the objects returned in the connection. This is a convenience field provided for quickly exploring the API; rather than querying for "{ edges { node } }" when no edge data is needed, this field can be be used instead. Note that when clients like Relay need to fetch the "cursor" field on the edge to enable efficient pagination, this shortcut cannot be used, and the full "{ edges { node } }" version should be used instead. """) strawberry-graphql-0.287.0/tests/schema_codegen/test_descriptions.py000066400000000000000000000041461511033167500257440ustar00rootroot00000000000000import ast import textwrap from pathlib import Path from pytest_snapshot.plugin import Snapshot from strawberry.schema_codegen import codegen HERE = Path(__file__).parent def test_long_descriptions(snapshot: Snapshot): snapshot.snapshot_dir = HERE / "snapshots" schema = ''' """A connection to a list of items.""" type FilmCharactersConnection { """Information to aid in pagination.""" pageInfo: PageInfo! """A list of edges.""" edges: [FilmCharactersEdge] """ A count of the total number of objects in this connection, ignoring pagination. This allows a client to fetch the first five objects by passing "5" as the argument to "first", then fetch the total count so it could display "5 of 83", for example. """ totalCount: Int """ A list of all of the objects returned in the connection. This is a convenience field provided for quickly exploring the API; rather than querying for "{ edges { node } }" when no edge data is needed, this field can be be used instead. Note that when clients like Relay need to fetch the "cursor" field on the edge to enable efficient pagination, this shortcut cannot be used, and the full "{ edges { node } }" version should be used instead. """ characters: [Person] } ''' output = codegen(schema) ast.parse(output) snapshot.assert_match(output, "long_descriptions.py") def test_can_convert_descriptions_with_quotes(): schema = ''' """A type of person or character within the "Star Wars" Universe.""" type Species { """The classification of this species, such as "mammal" or "reptile".""" classification: String! } ''' output = codegen(schema) expected_output = textwrap.dedent( """ import strawberry @strawberry.type(description='A type of person or character within the "Star Wars" Universe.') class Species: classification: str = strawberry.field(description='The classification of this species, such as "mammal" or "reptile".') """ ).lstrip() assert output == expected_output strawberry-graphql-0.287.0/tests/schema_codegen/test_enum.py000066400000000000000000000025111511033167500241740ustar00rootroot00000000000000import textwrap from strawberry.schema_codegen import codegen def test_enum(): schema = """ enum AuthStateNameEnum { AUTH_BROWSER_LAUNCHED AUTH_COULD_NOT_LAUNCH_BROWSER AUTH_ERROR_DURING_LOGIN } """ expected = textwrap.dedent( """ import strawberry from enum import Enum @strawberry.enum class AuthStateNameEnum(Enum): AUTH_BROWSER_LAUNCHED = "AUTH_BROWSER_LAUNCHED" AUTH_COULD_NOT_LAUNCH_BROWSER = "AUTH_COULD_NOT_LAUNCH_BROWSER" AUTH_ERROR_DURING_LOGIN = "AUTH_ERROR_DURING_LOGIN" """ ).strip() assert codegen(schema).strip() == expected # TODO: descriptions def test_multiple_enums_single_import(): schema = """ enum AuthStateNameEnum { AUTH_BROWSER_LAUNCHED } enum AuthStateNameEnum2 { AUTH_COULD_NOT_LAUNCH_BROWSER } """ expected = textwrap.dedent( """ import strawberry from enum import Enum @strawberry.enum class AuthStateNameEnum(Enum): AUTH_BROWSER_LAUNCHED = "AUTH_BROWSER_LAUNCHED" @strawberry.enum class AuthStateNameEnum2(Enum): AUTH_COULD_NOT_LAUNCH_BROWSER = "AUTH_COULD_NOT_LAUNCH_BROWSER" """ ).strip() assert codegen(schema).strip() == expected strawberry-graphql-0.287.0/tests/schema_codegen/test_federation.py000066400000000000000000000131451511033167500253550ustar00rootroot00000000000000import textwrap from strawberry.schema_codegen import codegen def test_support_for_key_directive(): schema = """ extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"]) type User @key(fields: "id") { id: ID! username: String! } """ expected = textwrap.dedent( """ import strawberry @strawberry.federation.type(keys=["id"]) class User: id: strawberry.ID username: str """ ).strip() assert codegen(schema).strip() == expected def test_support_for_shareable_directive(): schema = """ extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@shareable"]) type User @shareable { id: ID! username: String! } """ expected = textwrap.dedent( """ import strawberry @strawberry.federation.type(shareable=True) class User: id: strawberry.ID username: str """ ).strip() assert codegen(schema).strip() == expected def test_support_for_inaccessible_directive(): schema = """ extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@inaccessible"]) type User @inaccessible { id: ID! username: String! } """ expected = textwrap.dedent( """ import strawberry @strawberry.federation.type(inaccessible=True) class User: id: strawberry.ID username: str """ ).strip() assert codegen(schema).strip() == expected def test_support_for_tags_directive(): schema = """ extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@tags"]) type User @tag(name: "user") @tag(name: "admin") { id: ID! username: String! } """ expected = textwrap.dedent( """ import strawberry @strawberry.federation.type(tags=["user", "admin"]) class User: id: strawberry.ID username: str """ ).strip() assert codegen(schema).strip() == expected def test_uses_federation_schema(): schema = """ extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@inaccessible"]) type Query { me: String! } """ expected = textwrap.dedent( """ import strawberry @strawberry.type class Query: me: str schema = strawberry.federation.Schema(query=Query) """ ).strip() assert codegen(schema).strip() == expected def test_supports_authenticated_directive(): schema = """ extend schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@authenticated"]) type User @authenticated { name: String! @authenticated } """ expected = textwrap.dedent( """ import strawberry @strawberry.federation.type(authenticated=True) class User: name: str = strawberry.federation.field(authenticated=True) """ ).strip() assert codegen(schema).strip() == expected def test_requires_scope(): schema = """ extend schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@requiresScope"]) type User @requiresScopes(scopes: [["client", "poweruser"], ["admin"], ["productowner"]]){ name: String! } """ expected = textwrap.dedent( """ import strawberry @strawberry.federation.type(requires_scopes=[["client", "poweruser"], ["admin"], ["productowner"]]) class User: name: str """ ).strip() assert codegen(schema).strip() == expected def test_policy_directive(): schema = """ extend schema @link(url: "https://specs.apollo.dev/federation/v2.11", import: ["@policy"]) type User @policy(policies: ["userPolicy", [["client", "poweruser"], ["admin"], ["productowner"]]]){ name: String! } """ expected = textwrap.dedent( """ import strawberry @strawberry.federation.type(policy=["userPolicy", [["client", "poweruser"], ["admin"], ["productowner"]]]) class User: name: str """ ).strip() assert codegen(schema).strip() == expected def test_support_for_directives_on_fields(): schema = """ extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@requires", "@provides"]) type User { a: String! @shareable b: String! @inaccessible c: String! @override(from: "mySubGraph") c1: String! @override(from: "mySubGraph", label: "some.label") d: String! @external e: String! @requires(fields: "id") f: String! @provides(fields: "id") g: String! @tag(name: "user") } """ expected = textwrap.dedent( """ import strawberry from strawberry.federation.schema_directives import Override @strawberry.type class User: a: str = strawberry.federation.field(shareable=True) b: str = strawberry.federation.field(inaccessible=True) c: str = strawberry.federation.field(override="mySubGraph") c1: str = strawberry.federation.field(override=Override(override_from="mySubGraph", label="some.label")) d: str = strawberry.federation.field(external=True) e: str = strawberry.federation.field(requires=["id"]) f: str = strawberry.federation.field(provides=["id"]) g: str = strawberry.federation.field(tags=["user"]) """ ).strip() assert codegen(schema).strip() == expected strawberry-graphql-0.287.0/tests/schema_codegen/test_input_types.py000066400000000000000000000020701511033167500256130ustar00rootroot00000000000000import textwrap from strawberry.schema_codegen import codegen def test_codegen_input_type(): schema = """ input Example { a: Int! b: Float! c: Boolean! d: String! e: ID! f: [Int!]! g: [Float!]! h: [Boolean!]! i: [String!]! j: [ID!]! k: [Int] l: [Float] m: [Boolean] n: [String] o: [ID] } """ expected = textwrap.dedent( """ import strawberry @strawberry.input class Example: a: int b: float c: bool d: str e: strawberry.ID f: list[int] g: list[float] h: list[bool] i: list[str] j: list[strawberry.ID] k: list[int | None] | None l: list[float | None] | None m: list[bool | None] | None n: list[str | None] | None o: list[strawberry.ID | None] | None """ ).strip() assert codegen(schema).strip() == expected strawberry-graphql-0.287.0/tests/schema_codegen/test_names.py000066400000000000000000000021001511033167500243250ustar00rootroot00000000000000import keyword import textwrap import pytest from strawberry.schema_codegen import codegen @pytest.mark.parametrize( "name", [keyword for keyword in keyword.kwlist if keyword not in ("False", "True", "None")], ) def test_handles_keywords(name: str): schema = f""" type Example {{ {name}: String! }} """ expected = textwrap.dedent( f""" import strawberry @strawberry.type class Example: {name}_: str = strawberry.field(name="{name}") """ ).strip() assert codegen(schema).strip() == expected def test_converts_names_to_snake_case(): schema = """ type Example { someField: String! allowCustomExportURL: Boolean! allowInsecureTLS: Boolean! } """ expected = textwrap.dedent( """ import strawberry @strawberry.type class Example: some_field: str allow_custom_export_url: bool allow_insecure_tls: bool """ ).strip() assert codegen(schema).strip() == expected strawberry-graphql-0.287.0/tests/schema_codegen/test_order.py000066400000000000000000000016611511033167500243500ustar00rootroot00000000000000import textwrap from strawberry.schema_codegen import codegen def test_generates_used_interface_before(): schema = """ type Human implements Being { id: ID! name: String! friends: [Human] } type Cat implements Being { id: ID! name: String! livesLeft: Int } interface Being { id: ID! name: String! } """ expected = textwrap.dedent( """ import strawberry @strawberry.interface class Being: id: strawberry.ID name: str @strawberry.type class Human(Being): id: strawberry.ID name: str friends: list[Human | None] | None @strawberry.type class Cat(Being): id: strawberry.ID name: str lives_left: int | None """ ).strip() assert codegen(schema).strip() == expected strawberry-graphql-0.287.0/tests/schema_codegen/test_root_types_extend.py000066400000000000000000000023041511033167500270060ustar00rootroot00000000000000import textwrap from strawberry.schema_codegen import codegen def test_extend_query(): schema = """ extend type Query { world: String! } """ expected = textwrap.dedent( """ import strawberry @strawberry.type class Query: world: str schema = strawberry.Schema(query=Query) """ ).strip() assert codegen(schema).strip() == expected def test_extend_mutation(): schema = """ extend type Mutation { world: String! } """ expected = textwrap.dedent( """ import strawberry @strawberry.type class Mutation: world: str schema = strawberry.Schema(mutation=Mutation) """ ).strip() assert codegen(schema).strip() == expected def test_extend_subscription(): schema = """ extend type Subscription { world: String! } """ expected = textwrap.dedent( """ import strawberry @strawberry.type class Subscription: world: str schema = strawberry.Schema(subscription=Subscription) """ ).strip() assert codegen(schema).strip() == expected strawberry-graphql-0.287.0/tests/schema_codegen/test_scalar.py000066400000000000000000000036171511033167500245050ustar00rootroot00000000000000import textwrap from strawberry.schema_codegen import codegen def test_scalar(): schema = """ scalar LocalDate @specifiedBy(url: "https://scalars.graphql.org/andimarek/local-date.html") """ expected = textwrap.dedent( """ import strawberry from typing import NewType LocalDate = strawberry.scalar(NewType("LocalDate", object), specified_by_url="https://scalars.graphql.org/andimarek/local-date.html", serialize=lambda v: v, parse_value=lambda v: v) """ ).strip() assert codegen(schema).strip() == expected def test_scalar_with_description(): schema = """ "A date without a time-zone in the ISO-8601 calendar system, such as 2007-12-03." scalar LocalDate """ expected = textwrap.dedent( """ import strawberry from typing import NewType LocalDate = strawberry.scalar(NewType("LocalDate", object), description="A date without a time-zone in the ISO-8601 calendar system, such as 2007-12-03.", serialize=lambda v: v, parse_value=lambda v: v) """ ).strip() assert codegen(schema).strip() == expected def test_builtin_scalars(): schema = """ scalar JSON scalar Date scalar Time scalar DateTime scalar UUID scalar Decimal type Example { a: JSON! b: Date! c: Time! d: DateTime! e: UUID! f: Decimal! } """ expected = textwrap.dedent( """ import strawberry from datetime import date from datetime import datetime from datetime import time from decimal import Decimal from uuid import UUID @strawberry.type class Example: a: strawberry.JSON b: date c: time d: datetime e: UUID f: Decimal """ ).strip() assert codegen(schema).strip() == expected strawberry-graphql-0.287.0/tests/schema_codegen/test_schema.py000066400000000000000000000040001511033167500244630ustar00rootroot00000000000000import textwrap from strawberry.schema_codegen import codegen def test_adds_schema_if_schema_is_defined(): schema = """ type Root { hello: String! } type RootMutation { hello: String! } type RootSubscription { hello: String! } schema { query: Root mutation: RootMutation subscription: RootSubscription } """ expected = textwrap.dedent( """ import strawberry @strawberry.type class Root: hello: str @strawberry.type class RootMutation: hello: str @strawberry.type class RootSubscription: hello: str schema = strawberry.Schema(query=Root, mutation=RootMutation, subscription=RootSubscription) """ ).strip() assert codegen(schema).strip() == expected def test_adds_schema_if_has_query(): schema = """ type Query { hello: String! } """ expected = textwrap.dedent( """ import strawberry @strawberry.type class Query: hello: str schema = strawberry.Schema(query=Query) """ ).strip() assert codegen(schema).strip() == expected def test_adds_schema_if_has_mutation(): schema = """ type Mutation { hello: String! } """ expected = textwrap.dedent( """ import strawberry @strawberry.type class Mutation: hello: str schema = strawberry.Schema(mutation=Mutation) """ ).strip() assert codegen(schema).strip() == expected def test_adds_schema_if_has_subscription(): schema = """ type Subscription { hello: String! } """ expected = textwrap.dedent( """ import strawberry @strawberry.type class Subscription: hello: str schema = strawberry.Schema(subscription=Subscription) """ ).strip() assert codegen(schema).strip() == expected strawberry-graphql-0.287.0/tests/schema_codegen/test_types.py000066400000000000000000000041431511033167500243770ustar00rootroot00000000000000import textwrap from strawberry.schema_codegen import codegen def test_codegen_object_type(): schema = """ type Example { a: Int! b: Float! c: Boolean! d: String! e: ID! f: [Int!]! g: [Float!]! h: [Boolean!]! i: [String!]! j: [ID!]! k: [Int] l: [Float] m: [Boolean] n: [String] o: [ID] } """ expected = textwrap.dedent( """ import strawberry @strawberry.type class Example: a: int b: float c: bool d: str e: strawberry.ID f: list[int] g: list[float] h: list[bool] i: list[str] j: list[strawberry.ID] k: list[int | None] | None l: list[float | None] | None m: list[bool | None] | None n: list[str | None] | None o: list[strawberry.ID | None] | None """ ).strip() assert codegen(schema).strip() == expected def test_supports_descriptions(): schema = """ "Example description" type Example { "a description" a: Int! "b description" b: Float! } """ expected = textwrap.dedent( """ import strawberry @strawberry.type(description="Example description") class Example: a: int = strawberry.field(description="a description") b: float = strawberry.field(description="b description") """ ).strip() assert codegen(schema).strip() == expected def test_supports_interfaces(): schema = """ interface Node { id: ID! } type User implements Node { id: ID! name: String! } """ expected = textwrap.dedent( """ import strawberry @strawberry.interface class Node: id: strawberry.ID @strawberry.type class User(Node): id: strawberry.ID name: str """ ).strip() assert codegen(schema).strip() == expected strawberry-graphql-0.287.0/tests/schema_codegen/test_union.py000066400000000000000000000011631511033167500243620ustar00rootroot00000000000000import textwrap from strawberry.schema_codegen import codegen def test_union(): schema = """ union User = Admin | Client type Admin { name: String! } type Client { name: String! } """ expected = textwrap.dedent( """ import strawberry from typing import Annotated User = Annotated[Admin | Client, strawberry.union(name="User")] @strawberry.type class Admin: name: str @strawberry.type class Client: name: str """ ).strip() assert codegen(schema).strip() == expected strawberry-graphql-0.287.0/tests/test/000077500000000000000000000000001511033167500176535ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/test/__init__.py000066400000000000000000000000001511033167500217520ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/test/conftest.py000066400000000000000000000037331511033167500220600ustar00rootroot00000000000000from __future__ import annotations from collections.abc import AsyncGenerator from contextlib import asynccontextmanager from typing import TYPE_CHECKING import pytest from tests.views.schema import schema if TYPE_CHECKING: from strawberry.test import BaseGraphQLTestClient @asynccontextmanager async def aiohttp_graphql_client() -> AsyncGenerator[BaseGraphQLTestClient]: try: from aiohttp import web from aiohttp.test_utils import TestClient, TestServer from strawberry.aiohttp.test import GraphQLTestClient from strawberry.aiohttp.views import GraphQLView except ImportError: pytest.skip("Aiohttp not installed") view = GraphQLView(schema=schema) app = web.Application() app.router.add_route("*", "/graphql/", view) async with TestClient(TestServer(app)) as client: yield GraphQLTestClient(client) @asynccontextmanager async def asgi_graphql_client() -> AsyncGenerator[BaseGraphQLTestClient]: try: from starlette.testclient import TestClient from strawberry.asgi import GraphQL from strawberry.asgi.test import GraphQLTestClient except ImportError: pytest.skip("Starlette not installed") yield GraphQLTestClient(TestClient(GraphQL(schema))) @asynccontextmanager async def django_graphql_client() -> AsyncGenerator[BaseGraphQLTestClient]: try: from django.test.client import Client from strawberry.django.test import GraphQLTestClient except ImportError: pytest.skip("Django not installed") yield GraphQLTestClient(Client()) @pytest.fixture( params=[ pytest.param(aiohttp_graphql_client, marks=[pytest.mark.aiohttp]), pytest.param(asgi_graphql_client, marks=[pytest.mark.asgi]), pytest.param(django_graphql_client, marks=[pytest.mark.django]), ] ) async def graphql_client(request) -> AsyncGenerator[BaseGraphQLTestClient]: async with request.param() as graphql_client: yield graphql_client strawberry-graphql-0.287.0/tests/test/test_client.py000066400000000000000000000021331511033167500225410ustar00rootroot00000000000000from contextlib import nullcontext import pytest from strawberry.utils.await_maybe import await_maybe @pytest.mark.parametrize("asserts_errors", [True, False]) async def test_query_asserts_errors_option_is_deprecated( graphql_client, asserts_errors ): with pytest.deprecated_call( match="The `asserts_errors` argument has been renamed to `assert_no_errors`" ): await await_maybe( graphql_client.query("{ hello }", asserts_errors=asserts_errors) ) @pytest.mark.parametrize( ("option_name", "expectation1"), [("asserts_errors", pytest.deprecated_call()), ("assert_no_errors", nullcontext())], ) @pytest.mark.parametrize( ("assert_no_errors", "expectation2"), [(True, pytest.raises(AssertionError)), (False, nullcontext())], ) async def test_query_with_assert_no_errors_option( graphql_client, option_name, assert_no_errors, expectation1, expectation2 ): query = "{ ThisIsNotAValidQuery }" with expectation1, expectation2: await await_maybe( graphql_client.query(query, **{option_name: assert_no_errors}) ) strawberry-graphql-0.287.0/tests/test_aio.py000066400000000000000000000037521511033167500210640ustar00rootroot00000000000000from strawberry.utils.aio import ( aenumerate, aislice, asyncgen_to_list, resolve_awaitable, ) async def test_aenumerate(): async def gen(): yield "a" yield "b" yield "c" yield "d" res = [(i, v) async for i, v in aenumerate(gen())] assert res == [(0, "a"), (1, "b"), (2, "c"), (3, "d")] async def test_aslice(): async def gen(): yield "a" yield "b" raise AssertionError("should never be called") # pragma: no cover yield "c" # pragma: no cover res = [] async for v in aislice(gen(), 0, 2): res.append(v) # noqa: PERF401 assert res == ["a", "b"] async def test_aislice_empty_generator(): async def gen(): if False: # pragma: no cover yield "should not be returned" raise AssertionError("should never be called") res = [] async for v in aislice(gen(), 0, 2): res.append(v) # noqa: PERF401 assert res == [] async def test_aislice_empty_slice(): async def gen(): if False: # pragma: no cover yield "should not be returned" raise AssertionError("should never be called") res = [] async for v in aislice(gen(), 0, 0): res.append(v) # noqa: PERF401 assert res == [] async def test_aislice_with_step(): async def gen(): yield "a" yield "b" yield "c" raise AssertionError("should never be called") # pragma: no cover yield "d" # pragma: no cover yield "e" # pragma: no cover res = [] async for v in aislice(gen(), 0, 4, 2): res.append(v) # noqa: PERF401 assert res == ["a", "c"] async def test_asyncgen_to_list(): async def gen(): yield "a" yield "b" yield "c" assert await asyncgen_to_list(gen()) == ["a", "b", "c"] async def test_resolve_awaitable(): async def awaitable(): return 1 assert await resolve_awaitable(awaitable(), lambda v: v + 1) == 2 strawberry-graphql-0.287.0/tests/test_auto.py000066400000000000000000000030001511033167500212460ustar00rootroot00000000000000from typing import Annotated, Any, cast, get_args import strawberry from strawberry.annotation import StrawberryAnnotation from strawberry.types.auto import StrawberryAuto, auto from strawberry.types.base import StrawberryList @strawberry.type class ExampleType: some_var: str def test_singleton(): assert get_args(auto)[1] is StrawberryAuto() assert StrawberryAuto() is StrawberryAuto() def test_annotated(): assert get_args(auto) == (Any, StrawberryAuto()) some_obj = object() new_annotated = Annotated[strawberry.auto, some_obj] assert get_args(new_annotated) == (Any, StrawberryAuto(), some_obj) def test_str(): assert str(StrawberryAuto()) == "auto" def test_repr(): assert repr(StrawberryAuto()) == "" def test_isinstance(): assert isinstance(auto, StrawberryAuto) assert not isinstance(object, StrawberryAuto) assert not isinstance(cast("Any", object()), StrawberryAuto) def test_isinstance_with_annotation(): annotation = StrawberryAnnotation(auto) assert isinstance(annotation, StrawberryAuto) str_annotation = StrawberryAnnotation("auto", namespace=globals()) assert isinstance(str_annotation, StrawberryAuto) def test_isinstance_with_annotated(): assert isinstance(Annotated[auto, object()], StrawberryAuto) assert not isinstance(Annotated[str, strawberry.auto], StrawberryAuto) def test_isinstance_with_unresolvable_annotation(): type_ = StrawberryList(of_type=ExampleType) assert not isinstance(type_, StrawberryAuto) strawberry-graphql-0.287.0/tests/test_dataloaders.py000066400000000000000000000276411511033167500226020ustar00rootroot00000000000000import asyncio from asyncio.futures import Future from collections.abc import Awaitable, Callable from typing import Any, Optional, cast import pytest from pytest_mock import MockerFixture from strawberry.dataloader import AbstractCache, DataLoader from strawberry.exceptions import WrongNumberOfResultsReturned IDXType = Callable[[list[int]], Awaitable[list[int]]] async def idx(keys: list[int]) -> list[int]: return keys @pytest.mark.asyncio async def test_loading(): loader = DataLoader(load_fn=idx) value_a = await loader.load(1) value_b = await loader.load(2) value_c = await loader.load(3) assert value_a == 1 assert value_b == 2 assert value_c == 3 values = await loader.load_many([1, 2, 3, 4, 5, 6]) assert values == [1, 2, 3, 4, 5, 6] @pytest.mark.asyncio async def test_gathering(mocker: MockerFixture): mock_loader = mocker.Mock(side_effect=idx) loader = DataLoader(load_fn=cast("IDXType", mock_loader)) [value_a, value_b, value_c] = await asyncio.gather( loader.load(1), loader.load(2), loader.load(3), ) mock_loader.assert_called_once_with([1, 2, 3]) assert value_a == 1 assert value_b == 2 assert value_c == 3 @pytest.mark.asyncio async def test_max_batch_size(mocker: MockerFixture): mock_loader = mocker.Mock(side_effect=idx) loader = DataLoader(load_fn=cast("IDXType", mock_loader), max_batch_size=2) [value_a, value_b, value_c] = await asyncio.gather( loader.load(1), loader.load(2), loader.load(3), ) mock_loader.assert_has_calls([mocker.call([1, 2]), mocker.call([3])]) # type: ignore assert value_a == 1 assert value_b == 2 assert value_c == 3 @pytest.mark.asyncio async def test_error(): async def idx(keys: list[int]) -> list[int | ValueError]: return [ValueError()] loader = DataLoader(load_fn=idx) with pytest.raises(ValueError): await loader.load(1) @pytest.mark.asyncio async def test_error_and_values(): async def idx(keys: list[int]) -> list[int | ValueError]: return [2] if keys == [2] else [ValueError()] loader = DataLoader(load_fn=idx) with pytest.raises(ValueError): await loader.load(1) assert await loader.load(2) == 2 @pytest.mark.asyncio async def test_when_raising_error_in_loader(): async def idx(keys: list[int]) -> list[int | ValueError]: raise ValueError loader = DataLoader(load_fn=idx) with pytest.raises(ValueError): await loader.load(1) with pytest.raises(ValueError): await asyncio.gather( loader.load(1), loader.load(2), loader.load(3), ) @pytest.mark.asyncio async def test_returning_wrong_number_of_results(): async def idx(keys: list[int]) -> list[int]: return [1, 2] loader = DataLoader(load_fn=idx) with pytest.raises( WrongNumberOfResultsReturned, match=( "Received wrong number of results in dataloader, expected: 1, received: 2" ), ): await loader.load(1) @pytest.mark.asyncio async def test_caches_by_id(mocker: MockerFixture): mock_loader = mocker.Mock(side_effect=idx) loader = DataLoader(load_fn=cast("IDXType", mock_loader), cache=True) a = loader.load(1) b = loader.load(1) assert a == b assert await a == 1 assert await b == 1 mock_loader.assert_called_once_with([1]) @pytest.mark.asyncio async def test_caches_by_id_when_loading_many(mocker: MockerFixture): mock_loader = mocker.Mock(side_effect=idx) loader = DataLoader(load_fn=cast("IDXType", mock_loader), cache=True) a = loader.load(1) b = loader.load(1) assert a == b assert await asyncio.gather(a, b) == [1, 1] mock_loader.assert_called_once_with([1]) @pytest.mark.asyncio async def test_cache_disabled(mocker: MockerFixture): mock_loader = mocker.Mock(side_effect=idx) loader = DataLoader(load_fn=cast("IDXType", mock_loader), cache=False) a = loader.load(1) b = loader.load(1) assert a != b assert await a == 1 assert await b == 1 mock_loader.assert_has_calls([mocker.call([1, 1])]) # type: ignore @pytest.mark.asyncio async def test_cache_disabled_immediate_await(mocker: MockerFixture): mock_loader = mocker.Mock(side_effect=idx) loader = DataLoader(load_fn=cast("IDXType", mock_loader), cache=False) a = await loader.load(1) b = await loader.load(1) assert a == b mock_loader.assert_has_calls([mocker.call([1]), mocker.call([1])]) # type: ignore @pytest.mark.asyncio async def test_prime(): async def idx(keys: list[int | float]) -> list[int | float]: assert keys, "At least one key must be specified" return keys loader = DataLoader(load_fn=idx) # Basic behavior intact a1 = loader.load(1) assert await a1 == 1 # Prime doesn't overrides value loader.prime(1, 1.1) loader.prime(2, 2.1) b1 = loader.load(1) b2 = loader.load(2) assert await b1 == 1 assert await b2 == 2.1 # Unless you tell it to loader.prime(1, 1.2, force=True) loader.prime(2, 2.2, force=True) b1 = loader.load(1) b2 = loader.load(2) assert await b1 == 1.2 assert await b2 == 2.2 # Preset will override pending values, but not cached values c2 = loader.load(2) # This is in cache c3 = loader.load(3) # This is pending loader.prime_many({2: 2.3, 3: 3.3}, force=True) assert await c2 == 2.2 assert await c3 == 3.3 # If we prime all keys in a batch, the load_fn is never called # (See assertion in idx) c4 = loader.load(4) loader.prime_many({4: 4.4}) assert await c4 == 4.4 # Yield to ensure the last batch has been dispatched, # despite all values being primed await asyncio.sleep(0) @pytest.mark.asyncio async def test_prime_nocache(): async def idx(keys: list[int | float]) -> list[int | float]: assert keys, "At least one key must be specified" return keys loader = DataLoader(load_fn=idx, cache=False) # Primed value is ignored loader.prime(1, 1.1) a1 = loader.load(1) assert await a1 == 1 # Unless it affects pending value in the current batch b1 = loader.load(2) loader.prime(2, 2.2) assert await b1 == 2.2 # Yield to ensure the last batch has been dispatched, # despite all values being primed await asyncio.sleep(0) @pytest.mark.asyncio async def test_clear(): batch_num = 0 async def idx(keys: list[int]) -> list[tuple[int, int]]: """Maps key => (key, batch_num)""" nonlocal batch_num batch_num += 1 return [(key, batch_num) for key in keys] loader = DataLoader(load_fn=idx) assert await loader.load_many([1, 2, 3]) == [(1, 1), (2, 1), (3, 1)] loader.clear(1) assert await loader.load_many([1, 2, 3]) == [(1, 2), (2, 1), (3, 1)] loader.clear_many([1, 2]) assert await loader.load_many([1, 2, 3]) == [(1, 3), (2, 3), (3, 1)] loader.clear_all() assert await loader.load_many([1, 2, 3]) == [(1, 4), (2, 4), (3, 4)] @pytest.mark.asyncio async def test_clear_nocache(): batch_num = 0 async def idx(keys: list[int]) -> list[tuple[int, int]]: """Maps key => (key, batch_num)""" nonlocal batch_num batch_num += 1 return [(key, batch_num) for key in keys] loader = DataLoader(load_fn=idx, cache=False) assert await loader.load_many([1, 2, 3]) == [(1, 1), (2, 1), (3, 1)] loader.clear(1) assert await loader.load_many([1, 2, 3]) == [(1, 2), (2, 2), (3, 2)] loader.clear_many([1, 2]) assert await loader.load_many([1, 2, 3]) == [(1, 3), (2, 3), (3, 3)] loader.clear_all() assert await loader.load_many([1, 2, 3]) == [(1, 4), (2, 4), (3, 4)] @pytest.mark.asyncio async def test_dont_dispatch_cancelled(): async def idx(keys: list[int]) -> list[int]: await asyncio.sleep(0.2) return keys loader = DataLoader(load_fn=idx) value_a = await loader.load(1) # value_b will be cancelled by hand value_b = cast("Future[Any]", loader.load(2)) value_b.cancel() # value_c will be cancelled by the timeout value_c = cast("Future[Any]", loader.load(3)) with pytest.raises(asyncio.TimeoutError): await asyncio.wait_for(value_c, 0.1) value_d = await loader.load(4) assert value_a == 1 assert value_d == 4 # 2 can still be used here because a new future will be created for it values = await loader.load_many([1, 2, 3, 4, 5, 6]) assert values == [1, 2, 3, 4, 5, 6] with pytest.raises(asyncio.CancelledError): value_b.result() with pytest.raises(asyncio.CancelledError): value_c.result() # pyright: ignore # Try single loading results again to make sure the cancelled # futures are not being reused value_a = await loader.load(1) value_b = await loader.load(2) value_c = await loader.load(3) value_d = await loader.load(4) assert value_a == 1 assert value_b == 2 assert value_c == 3 assert value_d == 4 @pytest.mark.asyncio async def test_cache_override(): class TestCache(AbstractCache[int, int]): def __init__(self): self.cache: dict[int, Future[int]] = {} def get(self, key: int) -> Optional["Future[int]"]: return self.cache.get(key) def set(self, key: int, value: "Future[int]") -> None: self.cache[key] = value def delete(self, key: int) -> None: del self.cache[key] def clear(self) -> None: self.cache.clear() custom_cache = TestCache() loader = DataLoader(load_fn=idx, cache_map=custom_cache) await loader.load(1) await loader.load(2) await loader.load(3) assert len(custom_cache.cache) == 3 assert await custom_cache.cache[1] == 1 assert await custom_cache.cache[2] == 2 assert await custom_cache.cache[3] == 3 loader.clear(1) assert len(custom_cache.cache) == 2 assert sorted(custom_cache.cache.keys()) == [2, 3] loader.clear_all() assert len(custom_cache.cache) == 0 assert not list(custom_cache.cache.keys()) await loader.load(1) await loader.load(2) await loader.load(3) loader.clear_many([1, 2]) assert len(custom_cache.cache) == 1 assert list(custom_cache.cache.keys()) == [3] data = await loader.load(3) assert data == 3 loader.prime(3, 4) assert await custom_cache.cache[3] == 3 loader.prime(3, 4, True) assert await custom_cache.cache[3] == 4 with pytest.raises(TypeError, match="unhashable type: 'list'"): await loader.load([1, 2, 3]) # type: ignore data = await loader.load((1, 2, 3)) # type: ignore assert await custom_cache.get((1, 2, 3)) == data # type: ignore @pytest.mark.asyncio async def test_custom_cache_key_fn(): def custom_cache_key(key: list[int]) -> str: return ",".join(str(k) for k in key) loader = DataLoader(load_fn=idx, cache_key_fn=custom_cache_key) data = await loader.load([1, 2, "test"]) assert data == [1, 2, "test"] @pytest.mark.asyncio async def test_user_class_custom_cache_key_fn(): class CustomData: def __init__(self, custom_id: int, name: str): self.id: int = custom_id self.name: str = name def custom_cache_key(key: CustomData) -> int: return key.id loader = DataLoader(load_fn=idx, cache_key_fn=custom_cache_key) data1 = await loader.load(CustomData(1, "Nick")) data2 = await loader.load(CustomData(1, "Nick")) assert data1 == data2 data2 = await loader.load(CustomData(2, "Jane")) assert data1 != data2 def test_works_when_created_in_a_different_loop(mocker: MockerFixture): mock_loader = mocker.Mock(side_effect=idx) loader = DataLoader(load_fn=cast("IDXType", mock_loader), cache=False) loop = asyncio.new_event_loop() async def run(): return await loader.load(1) data = loop.run_until_complete(run()) assert data == 1 mock_loader.assert_called_once_with([1]) strawberry-graphql-0.287.0/tests/test_deprecations.py000066400000000000000000000024231511033167500227660ustar00rootroot00000000000000from enum import Enum import pytest import strawberry from strawberry.utils.deprecations import DEPRECATION_MESSAGES @strawberry.type class A: a: int @strawberry.enum class Color(Enum): RED = "red" GREEN = "green" BLUE = "blue" def test_type_definition_is_aliased(): with pytest.warns(match=DEPRECATION_MESSAGES._TYPE_DEFINITION): assert A.__strawberry_definition__ is A._type_definition def test_get_warns(): with pytest.warns(match=DEPRECATION_MESSAGES._TYPE_DEFINITION): assert A._type_definition.fields[0] def test_can_import_type_definition(): from strawberry.types.base import StrawberryObjectDefinition, TypeDefinition assert TypeDefinition assert TypeDefinition is StrawberryObjectDefinition def test_enum_definition_is_aliased(): with pytest.warns(match=DEPRECATION_MESSAGES._ENUM_DEFINITION): assert Color.__strawberry_definition__ is Color._enum_definition def test_enum_get_warns(): with pytest.warns(match=DEPRECATION_MESSAGES._ENUM_DEFINITION): assert Color._enum_definition.name == "Color" def test_can_import_enum_definition(): from strawberry.types.enum import EnumDefinition, StrawberryEnumDefinition assert EnumDefinition assert EnumDefinition is StrawberryEnumDefinition strawberry-graphql-0.287.0/tests/test_forward_references.py000066400000000000000000000136451511033167500241630ustar00rootroot00000000000000# type: ignore from __future__ import annotations import textwrap from typing import Annotated import strawberry from strawberry.printer import print_schema from strawberry.scalars import JSON from strawberry.types.base import StrawberryList, StrawberryOptional from tests.a import A from tests.d import D def test_forward_reference(): global MyType @strawberry.type class Query: me: MyType = strawberry.field(name="myself") @strawberry.type class MyType: id: strawberry.ID scalar: JSON optional_scalar: JSON | None expected_representation = ''' """ The `JSON` scalar type represents JSON values as specified by [ECMA-404](https://ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf). """ scalar JSON @specifiedBy(url: "https://ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf") type MyType { id: ID! scalar: JSON! optionalScalar: JSON } type Query { myself: MyType! } ''' schema = strawberry.Schema(Query) assert print_schema(schema) == textwrap.dedent(expected_representation).strip() del MyType def test_lazy_forward_reference(): @strawberry.type class Query: @strawberry.field async def a(self) -> A: # pragma: no cover return A(id=strawberry.ID("1")) expected_representation = """ type A { id: ID! b: B! optionalB: B optionalB2: B } type B { id: ID! a: A! aList: [A!]! optionalA: A optionalA2: A } type Query { a: A! } """ schema = strawberry.Schema(query=Query) assert print_schema(schema) == textwrap.dedent(expected_representation).strip() def test_lazy_forward_reference_schema_with_a_list_only(): @strawberry.type class Query: @strawberry.field async def d(self) -> D: # pragma: no cover return D(id=strawberry.ID("1")) expected_representation = """ type C { id: ID! } type D { id: ID! cList: [C!]! } type Query { d: D! } """ schema = strawberry.Schema(query=Query) assert print_schema(schema) == textwrap.dedent(expected_representation).strip() def test_with_resolver(): global User @strawberry.type class User: name: str def get_users() -> list[User]: # pragma: no cover return [] @strawberry.type class Query: users: list[User] = strawberry.field(resolver=get_users) definition = Query.__strawberry_definition__ assert definition.name == "Query" [field] = definition.fields assert field.python_name == "users" assert isinstance(field.type, StrawberryList) assert field.type.of_type is User del User def test_union_or_notation(): global User @strawberry.type class User: name: str def get_users() -> list[User] | None: # pragma: no cover return [] @strawberry.type class Query: users: list[User] | None = strawberry.field(resolver=get_users) definition = Query.__strawberry_definition__ assert definition.name == "Query" [field] = definition.fields assert field.python_name == "users" assert isinstance(field.type, StrawberryOptional) assert isinstance(field.type.of_type, StrawberryList) assert field.type.of_type.of_type is User del User def test_union_or_notation_generic_type_alias(): global User @strawberry.type class User: name: str def get_users() -> list[User] | None: # pragma: no cover return [] @strawberry.type class Query: users: list[User] | None = strawberry.field(resolver=get_users) definition = Query.__strawberry_definition__ assert definition.name == "Query" [field] = definition.fields assert field.python_name == "users" assert isinstance(field.type, StrawberryOptional) assert isinstance(field.type.of_type, StrawberryList) assert field.type.of_type.of_type is User del User def test_annotated(): global User @strawberry.type class User: name: str def get_users() -> list[User]: # pragma: no cover return [] @strawberry.type class Query: users: Annotated[list[User], object()] = strawberry.field(resolver=get_users) definition = Query.__strawberry_definition__ assert definition.name == "Query" [field] = definition.fields assert field.python_name == "users" assert isinstance(field.type, StrawberryList) assert field.type.of_type is User del User def test_annotated_or_notation(): global User @strawberry.type class User: name: str def get_users() -> list[User] | None: # pragma: no cover return [] @strawberry.type class Query: users: Annotated[list[User] | None, object()] = strawberry.field( resolver=get_users ) definition = Query.__strawberry_definition__ assert definition.name == "Query" [field] = definition.fields assert field.python_name == "users" assert isinstance(field.type, StrawberryOptional) assert isinstance(field.type.of_type, StrawberryList) assert field.type.of_type.of_type is User del User def test_annotated_or_notation_generic_type_alias(): global User @strawberry.type class User: name: str def get_users() -> list[User]: # pragma: no cover return [] @strawberry.type class Query: users: Annotated[list[User] | None, object()] = strawberry.field( resolver=get_users ) definition = Query.__strawberry_definition__ assert definition.name == "Query" [field] = definition.fields assert field.python_name == "users" assert isinstance(field.type, StrawberryOptional) assert isinstance(field.type.of_type, StrawberryList) assert field.type.of_type.of_type is User del User strawberry-graphql-0.287.0/tests/test_info.py000066400000000000000000000011031511033167500212330ustar00rootroot00000000000000from typing import Any import pytest import strawberry def test_can_use_info_with_two_arguments(): CustomInfo = strawberry.Info[int, str] assert CustomInfo.__args__ == (int, str) def test_can_use_info_with_one_argument(): CustomInfo = strawberry.Info[int] assert CustomInfo.__args__ == (int, Any) def test_cannot_use_info_with_more_than_two_arguments(): with pytest.raises( TypeError, match=r"Too many (arguments|parameters) for ; actual 3, expected 2", ): strawberry.Info[int, str, int] # type: ignore strawberry-graphql-0.287.0/tests/test_inspect.py000066400000000000000000000005451511033167500217560ustar00rootroot00000000000000from strawberry.utils.inspect import in_async_context def test_in_async_context_sync(): assert not in_async_context() async def test_in_async_context_async(): assert in_async_context() async def test_in_async_context_async_with_inner_sync_function(): def inner_sync_function(): assert in_async_context() inner_sync_function() strawberry-graphql-0.287.0/tests/test_printer/000077500000000000000000000000001511033167500214165ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/test_printer/__init__.py000066400000000000000000000000001511033167500235150ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/test_printer/test_basic.py000066400000000000000000000213311511033167500241100ustar00rootroot00000000000000import textwrap from uuid import UUID import strawberry from strawberry.printer import print_schema from strawberry.scalars import JSON from strawberry.schema.config import StrawberryConfig from strawberry.types.unset import UNSET from tests.conftest import skip_if_gql_32 def test_simple_required_types(): @strawberry.type class Query: s: str i: int b: bool f: float id: strawberry.ID uid: UUID expected_type = """ type Query { s: String! i: Int! b: Boolean! f: Float! id: ID! uid: UUID! } scalar UUID """ schema = strawberry.Schema(query=Query) assert print_schema(schema) == textwrap.dedent(expected_type).strip() def test_printer_with_camel_case_on(): @strawberry.type class Query: hello_world: str expected_type = """ type Query { helloWorld: String! } """ schema = strawberry.Schema( query=Query, config=StrawberryConfig(auto_camel_case=True) ) assert print_schema(schema) == textwrap.dedent(expected_type).strip() def test_printer_with_camel_case_off(): @strawberry.type class Query: hello_world: str expected_type = """ type Query { hello_world: String! } """ schema = strawberry.Schema( query=Query, config=StrawberryConfig(auto_camel_case=False) ) assert print_schema(schema) == textwrap.dedent(expected_type).strip() def test_optional(): @strawberry.type class Query: s: str | None expected_type = """ type Query { s: String } """ schema = strawberry.Schema(query=Query) assert print_schema(schema) == textwrap.dedent(expected_type).strip() def test_input_simple_required_types(): @strawberry.input class MyInput: s: str i: int b: bool f: float id: strawberry.ID uid: UUID s2: str = None # type: ignore - we do this for testing purposes @strawberry.type class Query: @strawberry.field def search(self, input: MyInput) -> str: return input.s expected_type = """ input MyInput { s: String! i: Int! b: Boolean! f: Float! id: ID! uid: UUID! s2: String! } type Query { search(input: MyInput!): String! } scalar UUID """ schema = strawberry.Schema(query=Query) assert print_schema(schema) == textwrap.dedent(expected_type).strip() def test_input_defaults(): @strawberry.input class MyInput: s: str | None = None i: int = 0 b: bool = False f: float = 0.0 f2: float = 0.1 id: strawberry.ID = strawberry.ID("some_id") id_number: strawberry.ID = strawberry.ID(123) # type: ignore id_number_string: strawberry.ID = strawberry.ID("123") x: int | None = UNSET l: list[str] = strawberry.field(default_factory=list) # noqa: E741 list_with_values: list[str] = strawberry.field( default_factory=lambda: ["a", "b"] ) list_from_generator: list[str] = strawberry.field( default_factory=lambda: (x for x in ["a", "b"]) ) list_from_string: list[str] = "ab" # type: ignore - we do this for testing purposes @strawberry.type class Query: @strawberry.field def search(self, input: MyInput) -> int: return input.i expected_type = """ input MyInput { s: String = null i: Int! = 0 b: Boolean! = false f: Float! = 0 f2: Float! = 0.1 id: ID! = "some_id" idNumber: ID! = 123 idNumberString: ID! = 123 x: Int l: [String!]! = [] listWithValues: [String!]! = ["a", "b"] listFromGenerator: [String!]! = ["a", "b"] listFromString: [String!]! = "ab" } type Query { search(input: MyInput!): Int! } """ schema = strawberry.Schema(query=Query) assert print_schema(schema) == textwrap.dedent(expected_type).strip() @skip_if_gql_32("formatting is different in gql 3.2") def test_input_other_inputs(): @strawberry.input class Nested: s: str @strawberry.input class MyInput: nested: Nested nested2: Nested = strawberry.field(default_factory=lambda: Nested(s="a")) nested3: Nested = strawberry.field(default_factory=lambda: {"s": "a"}) nested4: Nested = "abc" # type: ignore - we do this for testing purposes @strawberry.type class Query: @strawberry.field def search(self, input: MyInput) -> str: return input.nested.s expected_type = """ input MyInput { nested: Nested! nested2: Nested! = { s: "a" } nested3: Nested! = { s: "a" } nested4: Nested! } input Nested { s: String! } type Query { search(input: MyInput!): String! } """ schema = strawberry.Schema(query=Query) assert print_schema(schema) == textwrap.dedent(expected_type).strip() @skip_if_gql_32("formatting is different in gql 3.2") def test_input_defaults_scalars(): @strawberry.input class MyInput: j: JSON = strawberry.field(default_factory=dict) j2: JSON = strawberry.field(default_factory=lambda: {"hello": "world"}) j3: JSON = strawberry.field( default_factory=lambda: {"hello": {"nice": "world"}} ) @strawberry.type class Query: @strawberry.field def search(self, input: MyInput) -> JSON: return input.j expected_type = """ \"\"\" The `JSON` scalar type represents JSON values as specified by [ECMA-404](https://ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf). \"\"\" scalar JSON @specifiedBy(url: "https://ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf") input MyInput { j: JSON! = { } j2: JSON! = { hello: "world" } j3: JSON! = { hello: { nice: "world" } } } type Query { search(input: MyInput!): JSON! } """ schema = strawberry.Schema(query=Query) assert print_schema(schema) == textwrap.dedent(expected_type).strip() @skip_if_gql_32("formatting is different in gql 3.2") def test_arguments_scalar(): @strawberry.input class MyInput: j: JSON = strawberry.field(default_factory=dict) j2: JSON = strawberry.field(default_factory=lambda: {"hello": "world"}) j3: JSON = strawberry.field( default_factory=lambda: {"hello": {"nice": "world"}} ) @strawberry.type class Query: @strawberry.field def search(self, j: JSON = {}) -> JSON: # noqa: B006 return j @strawberry.field def search2(self, j: JSON = {"hello": "world"}) -> JSON: # noqa: B006 return j @strawberry.field def search3(self, j: JSON = {"hello": {"nice": "world"}}) -> JSON: # noqa: B006 return j expected_type = """ \"\"\" The `JSON` scalar type represents JSON values as specified by [ECMA-404](https://ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf). \"\"\" scalar JSON @specifiedBy(url: "https://ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf") type Query { search(j: JSON! = { }): JSON! search2(j: JSON! = { hello: "world" }): JSON! search3(j: JSON! = { hello: { nice: "world" } }): JSON! } """ schema = strawberry.Schema(query=Query) assert print_schema(schema) == textwrap.dedent(expected_type).strip() def test_interface(): @strawberry.interface class Node: id: strawberry.ID @strawberry.type class User(Node): name: str @strawberry.type class Query: user: User expected_type = """ interface Node { id: ID! } type Query { user: User! } type User implements Node { id: ID! name: String! } """ schema = strawberry.Schema(query=Query) assert print_schema(schema) == textwrap.dedent(expected_type).strip() def test_root_objects_with_different_names(): @strawberry.type class Domanda: name: str @strawberry.type class Mutazione: name: str @strawberry.type class Abbonamento: name: str expected_type = """ schema { query: Domanda mutation: Mutazione subscription: Abbonamento } type Abbonamento { name: String! } type Domanda { name: String! } type Mutazione { name: String! } """ schema = strawberry.Schema( query=Domanda, mutation=Mutazione, subscription=Abbonamento ) assert print_schema(schema) == textwrap.dedent(expected_type).strip() strawberry-graphql-0.287.0/tests/test_printer/test_defer_stream.py000066400000000000000000000023041511033167500254660ustar00rootroot00000000000000from __future__ import annotations import textwrap import strawberry from strawberry.schema.config import StrawberryConfig from tests.conftest import skip_if_gql_32 pytestmark = skip_if_gql_32("GraphQL 3.3.0 is required for incremental execution") @strawberry.type class Query: hello: str def test_does_not_print_defer_and_stream_directives_when_experimental_execution_is_disabled(): schema = strawberry.Schema( query=Query, config=StrawberryConfig(enable_experimental_incremental_execution=False), ) expected_type = """ type Query { hello: String! } """ assert str(schema) == textwrap.dedent(expected_type).strip() def test_prints_defer_and_stream_directives_when_experimental_execution_is_enabled(): schema = strawberry.Schema( query=Query, config=StrawberryConfig(enable_experimental_incremental_execution=True), ) expected_type = """ directive @defer(if: Boolean, label: String) on FRAGMENT_SPREAD | INLINE_FRAGMENT directive @stream(if: Boolean, label: String, initialCount: Int = 0) on FIELD type Query { hello: String! } """ assert str(schema) == textwrap.dedent(expected_type).strip() strawberry-graphql-0.287.0/tests/test_printer/test_one_of.py000066400000000000000000000016061511033167500242770ustar00rootroot00000000000000from __future__ import annotations import textwrap import strawberry from strawberry.schema_directives import OneOf @strawberry.input(directives=[OneOf()]) class ExampleInputTagged: a: str | None b: int | None @strawberry.type class ExampleResult: a: str | None b: int | None @strawberry.type class Query: @strawberry.field def test(self, input: ExampleInputTagged) -> ExampleResult: # pragma: no cover return input # type: ignore schema = strawberry.Schema(query=Query) def test_prints_one_of_directive(): expected_type = """ directive @oneOf on INPUT_OBJECT input ExampleInputTagged @oneOf { a: String b: Int } type ExampleResult { a: String b: Int } type Query { test(input: ExampleInputTagged!): ExampleResult! } """ assert str(schema) == textwrap.dedent(expected_type).strip() strawberry-graphql-0.287.0/tests/test_printer/test_schema_directives.py000066400000000000000000000476541511033167500265300ustar00rootroot00000000000000import textwrap from enum import Enum from typing import Annotated, Any import strawberry from strawberry import BasePermission, Info from strawberry.permission import PermissionExtension from strawberry.printer import print_schema from strawberry.schema.config import StrawberryConfig from strawberry.schema_directive import Location from strawberry.types.unset import UNSET from tests.conftest import skip_if_gql_32 def test_print_simple_directive(): @strawberry.schema_directive(locations=[Location.FIELD_DEFINITION]) class Sensitive: reason: str @strawberry.type class Query: first_name: str = strawberry.field(directives=[Sensitive(reason="GDPR")]) expected_output = """ directive @sensitive(reason: String!) on FIELD_DEFINITION type Query { firstName: String! @sensitive(reason: "GDPR") } """ schema = strawberry.Schema(query=Query) assert print_schema(schema) == textwrap.dedent(expected_output).strip() def test_print_directive_with_name(): @strawberry.schema_directive( name="sensitive", locations=[Location.FIELD_DEFINITION] ) class SensitiveDirective: reason: str @strawberry.type class Query: first_name: str = strawberry.field( directives=[SensitiveDirective(reason="GDPR")] ) expected_output = """ directive @sensitive(reason: String!) on FIELD_DEFINITION type Query { firstName: String! @sensitive(reason: "GDPR") } """ schema = strawberry.Schema(query=Query) assert print_schema(schema) == textwrap.dedent(expected_output).strip() @skip_if_gql_32("formatting is different in gql 3.2") def test_directive_on_types(): @strawberry.input class SensitiveValue: key: str value: str @strawberry.schema_directive(locations=[Location.OBJECT, Location.FIELD_DEFINITION]) class SensitiveData: reason: str meta: list[SensitiveValue] | None = UNSET @strawberry.schema_directive(locations=[Location.INPUT_OBJECT]) class SensitiveInput: reason: str meta: list[SensitiveValue] | None = UNSET @strawberry.schema_directive(locations=[Location.INPUT_FIELD_DEFINITION]) class RangeInput: min: int max: int @strawberry.input(directives=[SensitiveInput(reason="GDPR")]) class Input: first_name: str age: int = strawberry.field(directives=[RangeInput(min=1, max=100)]) @strawberry.type(directives=[SensitiveData(reason="GDPR")]) class User: first_name: str age: int phone: str = strawberry.field( directives=[ SensitiveData( reason="PRIVATE", meta=[ SensitiveValue( key="can_share_field", value="phone_share_accepted" ) ], ) ] ) phone_share_accepted: bool @strawberry.type class Query: @strawberry.field def user(self, input: Input) -> User: return User( first_name=input.first_name, age=input.age, phone="+551191551234", phone_share_accepted=False, ) expected_output = """ directive @rangeInput(min: Int!, max: Int!) on INPUT_FIELD_DEFINITION directive @sensitiveData(reason: String!, meta: [SensitiveValue!]) on OBJECT | FIELD_DEFINITION directive @sensitiveInput(reason: String!, meta: [SensitiveValue!]) on INPUT_OBJECT input Input @sensitiveInput(reason: "GDPR") { firstName: String! age: Int! @rangeInput(min: 1, max: 100) } type Query { user(input: Input!): User! } type User @sensitiveData(reason: "GDPR") { firstName: String! age: Int! phone: String! @sensitiveData(reason: "PRIVATE", meta: [{ key: "can_share_field", value: "phone_share_accepted" }]) phoneShareAccepted: Boolean! } input SensitiveValue { key: String! value: String! } """ schema = strawberry.Schema(query=Query) assert print_schema(schema) == textwrap.dedent(expected_output).strip() def test_using_different_names_for_directive_field(): @strawberry.schema_directive(locations=[Location.FIELD_DEFINITION]) class Sensitive: reason: str = strawberry.directive_field(name="as") real_age: str real_age_2: str = strawberry.directive_field(name="real_age") @strawberry.type class Query: first_name: str = strawberry.field( directives=[Sensitive(reason="GDPR", real_age="1", real_age_2="2")] ) expected_output = """ directive @sensitive(as: String!, realAge: String!, real_age: String!) on FIELD_DEFINITION type Query { firstName: String! @sensitive(as: "GDPR", realAge: "1", real_age: "2") } """ schema = strawberry.Schema(query=Query) assert print_schema(schema) == textwrap.dedent(expected_output).strip() def test_respects_schema_config_for_names(): @strawberry.schema_directive(locations=[Location.FIELD_DEFINITION]) class Sensitive: real_age: str @strawberry.type class Query: first_name: str = strawberry.field(directives=[Sensitive(real_age="42")]) expected_output = """ directive @Sensitive(real_age: String!) on FIELD_DEFINITION type Query { first_name: String! @Sensitive(real_age: "42") } """ schema = strawberry.Schema( query=Query, config=StrawberryConfig(auto_camel_case=False) ) assert print_schema(schema) == textwrap.dedent(expected_output).strip() def test_respects_schema_parameter_types_for_arguments_int(): @strawberry.schema_directive(locations=[Location.FIELD_DEFINITION]) class Sensitive: real_age: int @strawberry.type class Query: first_name: str = strawberry.field(directives=[Sensitive(real_age=42)]) expected_output = """ directive @Sensitive(real_age: Int!) on FIELD_DEFINITION type Query { first_name: String! @Sensitive(real_age: 42) } """ schema = strawberry.Schema( query=Query, config=StrawberryConfig(auto_camel_case=False) ) assert print_schema(schema) == textwrap.dedent(expected_output).strip() def test_respects_schema_parameter_types_for_arguments_list_of_ints(): @strawberry.schema_directive(locations=[Location.FIELD_DEFINITION]) class Sensitive: real_age: list[int] @strawberry.type class Query: first_name: str = strawberry.field(directives=[Sensitive(real_age=[42])]) expected_output = """ directive @Sensitive(real_age: [Int!]!) on FIELD_DEFINITION type Query { first_name: String! @Sensitive(real_age: [42]) } """ schema = strawberry.Schema( query=Query, config=StrawberryConfig(auto_camel_case=False) ) assert print_schema(schema) == textwrap.dedent(expected_output).strip() def test_respects_schema_parameter_types_for_arguments_list_of_strings(): @strawberry.schema_directive(locations=[Location.FIELD_DEFINITION]) class Sensitive: real_age: list[str] @strawberry.type class Query: first_name: str = strawberry.field(directives=[Sensitive(real_age=["42"])]) expected_output = """ directive @Sensitive(real_age: [String!]!) on FIELD_DEFINITION type Query { first_name: String! @Sensitive(real_age: ["42"]) } """ schema = strawberry.Schema( query=Query, config=StrawberryConfig(auto_camel_case=False) ) assert print_schema(schema) == textwrap.dedent(expected_output).strip() def test_prints_directive_on_schema(): @strawberry.schema_directive(locations=[Location.SCHEMA]) class Tag: name: str @strawberry.type class Query: first_name: str = strawberry.field(directives=[Tag(name="team-1")]) schema = strawberry.Schema(query=Query, schema_directives=[Tag(name="team-1")]) expected_output = """ directive @tag(name: String!) on SCHEMA schema @tag(name: "team-1") { query: Query } type Query { firstName: String! } """ assert print_schema(schema) == textwrap.dedent(expected_output).strip() def test_prints_multiple_directives_on_schema(): @strawberry.schema_directive(locations=[Location.SCHEMA]) class Tag: name: str @strawberry.type class Query: first_name: str schema = strawberry.Schema( query=Query, schema_directives=[Tag(name="team-1"), Tag(name="team-2")] ) expected_output = """ directive @tag(name: String!) on SCHEMA schema @tag(name: "team-1") @tag(name: "team-2") { query: Query } type Query { firstName: String! } """ assert print_schema(schema) == textwrap.dedent(expected_output).strip() @skip_if_gql_32("formatting is different in gql 3.2") def test_prints_with_types(): @strawberry.input class SensitiveConfiguration: reason: str @strawberry.schema_directive(locations=[Location.FIELD_DEFINITION]) class Sensitive: config: SensitiveConfiguration @strawberry.type class Query: first_name: str = strawberry.field( directives=[Sensitive(config=SensitiveConfiguration(reason="example"))] ) expected_output = """ directive @sensitive(config: SensitiveConfiguration!) on FIELD_DEFINITION type Query { firstName: String! @sensitive(config: { reason: "example" }) } input SensitiveConfiguration { reason: String! } """ schema = strawberry.Schema(query=Query) assert print_schema(schema) == textwrap.dedent(expected_output).strip() def test_prints_with_scalar(): SensitiveConfiguration = strawberry.scalar(str, name="SensitiveConfiguration") @strawberry.schema_directive(locations=[Location.FIELD_DEFINITION]) class Sensitive: config: SensitiveConfiguration @strawberry.type class Query: first_name: str = strawberry.field(directives=[Sensitive(config="Some config")]) expected_output = """ directive @sensitive(config: SensitiveConfiguration!) on FIELD_DEFINITION type Query { firstName: String! @sensitive(config: "Some config") } scalar SensitiveConfiguration """ schema = strawberry.Schema(query=Query) assert print_schema(schema) == textwrap.dedent(expected_output).strip() def test_prints_with_enum(): @strawberry.enum class Reason(str, Enum): EXAMPLE = "example" __slots__ = () @strawberry.schema_directive(locations=[Location.FIELD_DEFINITION]) class Sensitive: reason: Reason @strawberry.type class Query: first_name: str = strawberry.field( directives=[Sensitive(reason=Reason.EXAMPLE)] ) expected_output = """ directive @sensitive(reason: Reason!) on FIELD_DEFINITION type Query { firstName: String! @sensitive(reason: EXAMPLE) } enum Reason { EXAMPLE } """ schema = strawberry.Schema(query=Query) assert print_schema(schema) == textwrap.dedent(expected_output).strip() def test_does_not_print_definition(): @strawberry.schema_directive( locations=[Location.FIELD_DEFINITION], print_definition=False ) class Sensitive: reason: str @strawberry.type class Query: first_name: str = strawberry.field(directives=[Sensitive(reason="GDPR")]) expected_output = """ type Query { firstName: String! @sensitive(reason: "GDPR") } """ schema = strawberry.Schema(query=Query) assert print_schema(schema) == textwrap.dedent(expected_output).strip() def test_print_directive_on_scalar(): @strawberry.schema_directive(locations=[Location.SCALAR]) class Sensitive: reason: str SensitiveString = strawberry.scalar( str, name="SensitiveString", directives=[Sensitive(reason="example")] ) @strawberry.type class Query: first_name: SensitiveString expected_output = """ directive @sensitive(reason: String!) on SCALAR type Query { firstName: SensitiveString! } scalar SensitiveString @sensitive(reason: "example") """ schema = strawberry.Schema(query=Query) assert print_schema(schema) == textwrap.dedent(expected_output).strip() def test_print_directive_on_enum(): @strawberry.schema_directive(locations=[Location.ENUM]) class Sensitive: reason: str @strawberry.enum(directives=[Sensitive(reason="example")]) class SomeEnum(str, Enum): EXAMPLE = "example" __slots__ = () @strawberry.type class Query: first_name: SomeEnum expected_output = """ directive @sensitive(reason: String!) on ENUM type Query { firstName: SomeEnum! } enum SomeEnum @sensitive(reason: "example") { EXAMPLE } """ schema = strawberry.Schema(query=Query) assert print_schema(schema) == textwrap.dedent(expected_output).strip() def test_print_directive_on_enum_value(): @strawberry.schema_directive(locations=[Location.ENUM_VALUE]) class Sensitive: reason: str @strawberry.enum class SomeEnum(Enum): EXAMPLE = strawberry.enum_value( "example", directives=[Sensitive(reason="example")] ) @strawberry.type class Query: first_name: SomeEnum expected_output = """ directive @sensitive(reason: String!) on ENUM_VALUE type Query { firstName: SomeEnum! } enum SomeEnum { EXAMPLE @sensitive(reason: "example") } """ schema = strawberry.Schema(query=Query) assert print_schema(schema) == textwrap.dedent(expected_output).strip() def test_dedupe_multiple_equal_directives(): class MemberRoleRequired(BasePermission): message = "Keine Rechte" def has_permission(self, source, info: Info, **kwargs: Any) -> bool: return True @strawberry.interface class MyInterface: id: strawberry.ID @strawberry.field( extensions=[PermissionExtension(permissions=[MemberRoleRequired()])] ) def hello(self, info: Info) -> str: return "world" @strawberry.type class MyType1(MyInterface): name: str @strawberry.type class MyType2(MyInterface): age: int @strawberry.type class Query: @strawberry.field def my_type(self, info: Info) -> MyInterface: return MyType1(id=strawberry.ID("1"), name="Hello") expected_output = """ directive @memberRoleRequired on FIELD_DEFINITION interface MyInterface { id: ID! hello: String! @memberRoleRequired } type MyType1 implements MyInterface { id: ID! hello: String! @memberRoleRequired name: String! } type MyType2 implements MyInterface { id: ID! hello: String! @memberRoleRequired age: Int! } type Query { myType: MyInterface! } """ schema = strawberry.Schema(Query, types=[MyType1, MyType2]) assert print_schema(schema) == textwrap.dedent(expected_output).strip() retval = schema.execute_sync("{ myType { id hello } }") assert retval.errors is None assert retval.data == {"myType": {"id": "1", "hello": "world"}} def test_print_directive_on_union(): @strawberry.type class A: a: int @strawberry.type class B: b: int @strawberry.schema_directive(locations=[Location.SCALAR]) class Sensitive: reason: str MyUnion = Annotated[ A | B, strawberry.union(name="MyUnion", directives=[Sensitive(reason="example")]), ] @strawberry.type class Query: example: MyUnion expected_output = """ directive @sensitive(reason: String!) on SCALAR type A { a: Int! } type B { b: Int! } union MyUnion @sensitive(reason: "example") = A | B type Query { example: MyUnion! } """ schema = strawberry.Schema(query=Query) assert print_schema(schema) == textwrap.dedent(expected_output).strip() def test_print_directive_on_argument(): @strawberry.schema_directive(locations=[Location.ARGUMENT_DEFINITION]) class Sensitive: reason: str @strawberry.type class Query: @strawberry.field def hello( self, name: Annotated[ str, strawberry.argument(directives=[Sensitive(reason="example")]) ], age: Annotated[ str, strawberry.argument(directives=[Sensitive(reason="example")]) ], ) -> str: return f"Hello {name} of {age}" expected_output = """ directive @sensitive(reason: String!) on ARGUMENT_DEFINITION type Query { hello(name: String! @sensitive(reason: "example"), age: String! @sensitive(reason: "example")): String! } """ schema = strawberry.Schema(query=Query) assert print_schema(schema) == textwrap.dedent(expected_output).strip() def test_print_directive_on_argument_with_description(): @strawberry.schema_directive(locations=[Location.ARGUMENT_DEFINITION]) class Sensitive: reason: str @strawberry.type class Query: @strawberry.field def hello( self, name: Annotated[ str, strawberry.argument( description="Name", directives=[Sensitive(reason="example")] ), ], age: Annotated[ str, strawberry.argument(directives=[Sensitive(reason="example")]) ], ) -> str: return f"Hello {name} of {age}" expected_output = """ directive @sensitive(reason: String!) on ARGUMENT_DEFINITION type Query { hello( \"\"\"Name\"\"\" name: String! @sensitive(reason: "example") age: String! @sensitive(reason: "example") ): String! } """ schema = strawberry.Schema(query=Query) assert print_schema(schema) == textwrap.dedent(expected_output).strip() @skip_if_gql_32("formatting is different in gql 3.2") def test_print_directive_with_unset_value(): @strawberry.input class FooInput: a: str | None = strawberry.UNSET b: str | None = strawberry.UNSET @strawberry.schema_directive(locations=[Location.FIELD_DEFINITION]) class FooDirective: input: FooInput optional_input: FooInput | None = strawberry.UNSET @strawberry.type class Query: @strawberry.field(directives=[FooDirective(input=FooInput(a="something"))]) def foo(self, info) -> str: ... schema = strawberry.Schema(query=Query) expected_output = """ directive @fooDirective(input: FooInput!, optionalInput: FooInput) on FIELD_DEFINITION type Query { foo: String! @fooDirective(input: { a: "something" }) } input FooInput { a: String b: String } """ schema = strawberry.Schema(query=Query) assert print_schema(schema) == textwrap.dedent(expected_output).strip() @skip_if_gql_32("formatting is different in gql 3.2") def test_print_directive_with_snake_case_arguments(): @strawberry.input class FooInput: hello: str hello_world: str @strawberry.schema_directive(locations=[Location.FIELD_DEFINITION]) class FooDirective: input: FooInput optional_input: FooInput | None = strawberry.UNSET @strawberry.type class Query: @strawberry.field( directives=[ FooDirective(input=FooInput(hello="hello", hello_world="hello world")) ] ) def foo(self, info) -> str: ... schema = strawberry.Schema(query=Query) expected_output = """ directive @fooDirective(input: FooInput!, optionalInput: FooInput) on FIELD_DEFINITION type Query { foo: String! @fooDirective(input: { hello: "hello", helloWorld: "hello world" }) } input FooInput { hello: String! helloWorld: String! } """ schema = strawberry.Schema(query=Query) assert print_schema(schema) == textwrap.dedent(expected_output).strip() strawberry-graphql-0.287.0/tests/test_repr.py000066400000000000000000000007721511033167500212630ustar00rootroot00000000000000from enum import Enum import strawberry def test_repr_type(): @strawberry.type class MyType: s: str i: int b: bool f: float id: strawberry.ID assert ( repr(MyType(s="a", i=1, b=True, f=3.2, id="123")) == "test_repr_type..MyType(s='a', i=1, b=True, f=3.2, id='123')" ) def test_repr_enum(): @strawberry.enum() class Test(Enum): A = 1 B = 2 C = 3 assert repr(Test(1)) == "" strawberry-graphql-0.287.0/tests/test_type.py000066400000000000000000000022661511033167500212740ustar00rootroot00000000000000import dataclasses from typing import Optional from typing_extensions import assert_type import pytest import strawberry from strawberry.types.base import StrawberryObjectDefinition, get_object_definition def test_get_object_definition(): @strawberry.type class Fruit: name: str obj_definition = get_object_definition(Fruit) assert_type(obj_definition, Optional[StrawberryObjectDefinition]) assert obj_definition is not None assert isinstance(obj_definition, StrawberryObjectDefinition) def test_get_object_definition_non_strawberry_type(): @dataclasses.dataclass class Fruit: name: str assert get_object_definition(Fruit) is None class OtherFruit: ... assert get_object_definition(OtherFruit) is None def test_get_object_definition_strict(): @strawberry.type class Fruit: name: str obj_definition = get_object_definition(Fruit, strict=True) assert_type(obj_definition, StrawberryObjectDefinition) class OtherFruit: name: str with pytest.raises( TypeError, match=r".* does not have a StrawberryObjectDefinition", ): get_object_definition(OtherFruit, strict=True) strawberry-graphql-0.287.0/tests/tools/000077500000000000000000000000001511033167500200345ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/tools/__init__.py000066400000000000000000000000001511033167500221330ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/tools/test_create_type.py000066400000000000000000000154331511033167500237570ustar00rootroot00000000000000from textwrap import dedent import pytest import strawberry from strawberry.annotation import StrawberryAnnotation from strawberry.tools import create_type from strawberry.types.base import get_object_definition from strawberry.types.field import StrawberryField def test_create_type(): @strawberry.field def name() -> str: return "foo" MyType = create_type("MyType", [name], description="This is a description") definition = get_object_definition(MyType, strict=True) assert definition.name == "MyType" assert definition.description == "This is a description" assert definition.is_input is False assert len(definition.fields) == 1 assert definition.fields[0].python_name == "name" assert definition.fields[0].graphql_name is None assert definition.fields[0].type is str def test_create_type_extend_and_directives(): @strawberry.field def name() -> str: return "foo" MyType = create_type( "MyType", [name], description="This is a description", extend=True, directives=[object()], ) definition = get_object_definition(MyType, strict=True) assert definition.name == "MyType" assert definition.description == "This is a description" assert definition.is_input is False assert definition.extend is True assert len(list(definition.directives)) == 1 assert len(definition.fields) == 1 assert definition.fields[0].python_name == "name" assert definition.fields[0].graphql_name is None assert definition.fields[0].type is str def test_create_input_type(): name = StrawberryField( python_name="name", type_annotation=StrawberryAnnotation(str) ) MyType = create_type( "MyType", [name], is_input=True, description="This is a description" ) definition = get_object_definition(MyType, strict=True) assert definition.name == "MyType" assert definition.description == "This is a description" assert definition.is_input assert len(definition.fields) == 1 assert definition.fields[0].python_name == "name" assert definition.fields[0].graphql_name is None assert definition.fields[0].type is str def test_create_interface_type(): name = StrawberryField( python_name="name", type_annotation=StrawberryAnnotation(str) ) MyType = create_type( "MyType", [name], is_interface=True, description="This is a description" ) definition = get_object_definition(MyType, strict=True) assert definition.name == "MyType" assert definition.description == "This is a description" assert definition.is_input is False assert definition.is_interface assert len(definition.fields) == 1 assert definition.fields[0].python_name == "name" assert definition.fields[0].graphql_name is None assert definition.fields[0].type is str def test_create_variable_type(): def get_name() -> str: return "foo" name = strawberry.field(name="name", resolver=get_name) MyType = create_type("MyType", [name]) definition = get_object_definition(MyType, strict=True) assert len(definition.fields) == 1 assert definition.fields[0].python_name == "get_name" assert definition.fields[0].graphql_name == "name" assert definition.fields[0].type is str def test_create_type_empty_list(): with pytest.raises(ValueError): create_type("MyType", []) def test_requires_resolver_or_name_to_create_type_field(): name = strawberry.field() with pytest.raises(ValueError, match="Field doesn't have a name"): create_type("MyType", [name]) def test_can_create_type_field_from_resolver_only(): def get_name() -> str: return "foo" # pragma: no cover name = strawberry.field(resolver=get_name) MyType = create_type("MyType", [name]) definition = get_object_definition(MyType, strict=True) assert len(definition.fields) == 1 assert definition.fields[0].python_name == "get_name" assert definition.fields[0].graphql_name is None def test_can_create_type_field_from_name_only(): first_name = strawberry.field(name="firstName", graphql_type=str) MyType = create_type("MyType", [first_name]) definition = get_object_definition(MyType, strict=True) assert len(definition.fields) == 1 assert definition.fields[0].python_name == "firstName" assert definition.fields[0].graphql_name == "firstName" def test_can_create_type_field_from_resolver_and_name(): def get_first_name() -> str: return "foo" # pragma: no cover first_name = strawberry.field(name="firstName", resolver=get_first_name) MyType = create_type("MyType", [first_name]) definition = get_object_definition(MyType, strict=True) assert len(definition.fields) == 1 assert definition.fields[0].python_name == "get_first_name" assert definition.fields[0].graphql_name == "firstName" def test_create_type_field_invalid(): with pytest.raises(TypeError): create_type("MyType", [strawberry.type()]) def test_create_mutation_type(): @strawberry.type class User: username: str @strawberry.mutation def make_user(username: str) -> User: return User(username=username) Mutation = create_type("Mutation", [make_user]) definition = get_object_definition(Mutation, strict=True) assert len(definition.fields) == 1 assert definition.fields[0].python_name == "make_user" assert definition.fields[0].graphql_name is None assert definition.fields[0].type == User def test_create_mutation_type_with_params(): @strawberry.type class User: username: str @strawberry.mutation(name="makeNewUser", description="Make a new user") def make_user(username: str) -> User: return User(username=username) Mutation = create_type("Mutation", [make_user]) definition = get_object_definition(Mutation, strict=True) assert len(definition.fields) == 1 assert definition.fields[0].python_name == "make_user" assert definition.fields[0].graphql_name == "makeNewUser" assert definition.fields[0].type == User assert definition.fields[0].description == "Make a new user" def test_create_schema(): @strawberry.type class User: id: strawberry.ID @strawberry.field def get_user_by_id(id: strawberry.ID) -> User: return User(id=id) Query = create_type("Query", [get_user_by_id]) schema = strawberry.Schema(query=Query) sdl = """ type Query { getUserById(id: ID!): User! } type User { id: ID! } """ assert dedent(sdl).strip() == str(schema) result = schema.execute_sync( """ { getUserById(id: "TEST") { id } } """ ) assert not result.errors assert result.data == {"getUserById": {"id": "TEST"}} strawberry-graphql-0.287.0/tests/tools/test_merge_types.py000066400000000000000000000044441511033167500237760ustar00rootroot00000000000000from textwrap import dedent import pytest import strawberry from strawberry.tools import merge_types @strawberry.type class Person: @strawberry.field def name(self) -> str: return "Eve" @strawberry.field def age(self) -> int: return 42 @strawberry.type class SimpleGreeter: @strawberry.field def hi(self) -> str: return "Hi" @strawberry.type class ComplexGreeter: @strawberry.field def hi(self, name: str = "world") -> str: return f"Hello, {name}!" @strawberry.field def bye(self, name: str = "world") -> str: return f"Bye, {name}!" def test_custom_name(): """The resulting type should have a custom name is one is specified""" custom_name = "SuperQuery" ComboQuery = merge_types(custom_name, (ComplexGreeter, Person)) assert ComboQuery.__name__ == custom_name def test_inheritance(): """It should merge multiple types following the regular inheritance rules""" ComboQuery = merge_types("SuperType", (ComplexGreeter, Person)) definition = ComboQuery.__strawberry_definition__ assert len(definition.fields) == 4 actuals = [(field.python_name, field.type) for field in definition.fields] expected = [("hi", str), ("bye", str), ("name", str), ("age", int)] assert actuals == expected def test_empty_list(): """It should raise when the `types` argument is empty""" with pytest.raises(ValueError): merge_types("EmptyType", ()) def test_schema(): """It should create a valid, usable schema based on a merged query""" ComboQuery = merge_types("SuperSchema", (ComplexGreeter, Person)) schema = strawberry.Schema(query=ComboQuery) sdl = """ schema { query: SuperSchema } type SuperSchema { hi(name: String! = "world"): String! bye(name: String! = "world"): String! name: String! age: Int! } """ assert dedent(sdl).strip() == str(schema) result = schema.execute_sync("query { hi }") assert not result.errors assert result.data == {"hi": "Hello, world!"} def test_fields_override(): """It should warn when merging results in overriding fields""" with pytest.warns(Warning): # noqa: PT030 merge_types("FieldsOverride", (ComplexGreeter, SimpleGreeter)) strawberry-graphql-0.287.0/tests/typecheckers/000077500000000000000000000000001511033167500213655ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/typecheckers/__init__.py000066400000000000000000000000001511033167500234640ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/typecheckers/test_auto.py000066400000000000000000000027261511033167500237550ustar00rootroot00000000000000from inline_snapshot import snapshot from .utils.marks import requires_mypy, requires_pyright, skip_on_windows from .utils.typecheck import Result, typecheck pytestmark = [skip_on_windows, requires_pyright, requires_mypy] CODE = """ import strawberry @strawberry.type class SomeType: foobar: strawberry.auto obj1 = SomeType(foobar=1) obj2 = SomeType(foobar="some text") obj3 = SomeType(foobar={"some key": "some value"}) reveal_type(obj1.foobar) reveal_type(obj2.foobar) reveal_type(obj3.foobar) """ def test_auto(): result = typecheck(CODE) assert result.pyright == snapshot( [ Result( type="information", message='Type of "obj1.foobar" is "Any"', line=14, column=13, ), Result( type="information", message='Type of "obj2.foobar" is "Any"', line=15, column=13, ), Result( type="information", message='Type of "obj3.foobar" is "Any"', line=16, column=13, ), ] ) assert result.mypy == snapshot( [ Result(type="note", message='Revealed type is "Any"', line=14, column=13), Result(type="note", message='Revealed type is "Any"', line=15, column=13), Result(type="note", message='Revealed type is "Any"', line=16, column=13), ] ) strawberry-graphql-0.287.0/tests/typecheckers/test_directives.py000066400000000000000000000022151511033167500251370ustar00rootroot00000000000000from inline_snapshot import snapshot from .utils.marks import requires_mypy, requires_pyright, skip_on_windows from .utils.typecheck import Result, typecheck pytestmark = [skip_on_windows, requires_pyright, requires_mypy] CODE = """ import strawberry from strawberry.directive import DirectiveLocation @strawberry.directive( locations=[DirectiveLocation.FRAGMENT_DEFINITION], description="description.", ) def make_int(value: str) -> int: '''description.''' try: return int(value) except ValueError: return 0 reveal_type(make_int) """ def test(): results = typecheck(CODE) assert results.pyright == snapshot( [ Result( type="information", message='Type of "make_int" is "StrawberryDirective[int]"', line=16, column=13, ) ] ) assert results.mypy == snapshot( [ Result( type="note", message='Revealed type is "strawberry.directive.StrawberryDirective[builtins.int]"', line=16, column=13, ) ] ) strawberry-graphql-0.287.0/tests/typecheckers/test_enum.py000066400000000000000000000157421511033167500237530ustar00rootroot00000000000000from inline_snapshot import snapshot from .utils.marks import requires_mypy, requires_pyright, skip_on_windows from .utils.typecheck import Result, typecheck pytestmark = [skip_on_windows, requires_pyright, requires_mypy] CODE_WITH_DECORATOR = """ from enum import Enum import strawberry @strawberry.enum class IceCreamFlavour(Enum): VANILLA = "vanilla" STRAWBERRY = "strawberry" CHOCOLATE = "chocolate" reveal_type(IceCreamFlavour) reveal_type(IceCreamFlavour.VANILLA) """ def test_enum_with_decorator(): results = typecheck(CODE_WITH_DECORATOR) assert results.pyright == snapshot( [ Result( type="information", message='Type of "IceCreamFlavour" is "type[IceCreamFlavour]"', line=12, column=13, ), Result( type="information", message='Type of "IceCreamFlavour.VANILLA" is "Literal[IceCreamFlavour.VANILLA]"', line=13, column=13, ), ] ) assert results.mypy == snapshot( [ Result( type="note", message='Revealed type is "def (value: builtins.object) -> mypy_test.IceCreamFlavour"', line=12, column=13, ), Result( type="note", message='Revealed type is "Literal[mypy_test.IceCreamFlavour.VANILLA]?"', line=13, column=13, ), ] ) CODE_WITH_DECORATOR_AND_NAME = """ from enum import Enum import strawberry @strawberry.enum(name="IceCreamFlavour") class Flavour(Enum): VANILLA = "vanilla" STRAWBERRY = "strawberry" CHOCOLATE = "chocolate" reveal_type(Flavour) reveal_type(Flavour.VANILLA) """ def test_enum_with_decorator_and_name(): results = typecheck(CODE_WITH_DECORATOR_AND_NAME) assert results.pyright == snapshot( [ Result( type="information", message='Type of "Flavour" is "type[Flavour]"', line=12, column=13, ), Result( type="information", message='Type of "Flavour.VANILLA" is "Literal[Flavour.VANILLA]"', line=13, column=13, ), ] ) assert results.mypy == snapshot( [ Result( type="note", message='Revealed type is "def (value: builtins.object) -> mypy_test.Flavour"', line=12, column=13, ), Result( type="note", message='Revealed type is "Literal[mypy_test.Flavour.VANILLA]?"', line=13, column=13, ), ] ) CODE_WITH_MANUAL_DECORATOR = """ from enum import Enum import strawberry class IceCreamFlavour(Enum): VANILLA = "vanilla" STRAWBERRY = "strawberry" CHOCOLATE = "chocolate" reveal_type(strawberry.enum(IceCreamFlavour)) reveal_type(strawberry.enum(IceCreamFlavour).VANILLA) """ def test_enum_with_manual_decorator(): results = typecheck(CODE_WITH_MANUAL_DECORATOR) assert results.pyright == snapshot( [ Result( type="information", message='Type of "strawberry.enum(IceCreamFlavour)" is "type[IceCreamFlavour]"', line=11, column=13, ), Result( type="information", message='Type of "strawberry.enum(IceCreamFlavour).VANILLA" is "Literal[IceCreamFlavour.VANILLA]"', line=12, column=13, ), ] ) assert results.mypy == snapshot( [ Result( type="note", message='Revealed type is "def (value: builtins.object) -> mypy_test.IceCreamFlavour"', line=11, column=13, ), Result( type="note", message='Revealed type is "Literal[mypy_test.IceCreamFlavour.VANILLA]?"', line=12, column=13, ), ] ) CODE_WITH_MANUAL_DECORATOR_AND_NAME = """ from enum import Enum import strawberry class Flavour(Enum): VANILLA = "vanilla" STRAWBERRY = "strawberry" CHOCOLATE = "chocolate" reveal_type(strawberry.enum(name="IceCreamFlavour")(Flavour)) reveal_type(strawberry.enum(name="IceCreamFlavour")(Flavour).VANILLA) """ def test_enum_with_manual_decorator_and_name(): results = typecheck(CODE_WITH_MANUAL_DECORATOR_AND_NAME) assert results.pyright == snapshot( [ Result( type="information", message='Type of "strawberry.enum(name="IceCreamFlavour")(Flavour)" is "type[Flavour]"', line=11, column=13, ), Result( type="information", message='Type of "strawberry.enum(name="IceCreamFlavour")(Flavour).VANILLA" is "Literal[Flavour.VANILLA]"', line=12, column=13, ), ] ) assert results.mypy == snapshot( [ Result( type="note", message='Revealed type is "def (value: builtins.object) -> mypy_test.Flavour"', line=11, column=13, ), Result( type="note", message='Revealed type is "Literal[mypy_test.Flavour.VANILLA]?"', line=12, column=13, ), ] ) CODE_WITH_DEPRECATION_REASON = """ from enum import Enum import strawberry @strawberry.enum class IceCreamFlavour(Enum): VANILLA = "vanilla" STRAWBERRY = strawberry.enum_value( "strawberry", deprecation_reason="We ran out" ) CHOCOLATE = "chocolate" reveal_type(IceCreamFlavour) reveal_type(IceCreamFlavour.STRAWBERRY) """ def test_enum_deprecated(): results = typecheck(CODE_WITH_DEPRECATION_REASON) assert results.pyright == snapshot( [ Result( type="information", message='Type of "IceCreamFlavour" is "type[IceCreamFlavour]"', line=14, column=13, ), Result( type="information", message='Type of "IceCreamFlavour.STRAWBERRY" is "Literal[IceCreamFlavour.STRAWBERRY]"', line=15, column=13, ), ] ) assert results.mypy == snapshot( [ Result( type="note", message='Revealed type is "def (value: builtins.object) -> mypy_test.IceCreamFlavour"', line=14, column=13, ), Result( type="note", message='Revealed type is "Literal[mypy_test.IceCreamFlavour.STRAWBERRY]?"', line=15, column=13, ), ] ) strawberry-graphql-0.287.0/tests/typecheckers/test_fastapi.py000066400000000000000000000046061511033167500244330ustar00rootroot00000000000000from inline_snapshot import snapshot from .utils.marks import requires_mypy, requires_pyright, skip_on_windows from .utils.typecheck import Result, typecheck pytestmark = [skip_on_windows, requires_pyright, requires_mypy] CODE_ROUTER_WITH_CONTEXT = """ import strawberry from strawberry.fastapi import GraphQLRouter, BaseContext @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "Hello World" class Context(BaseContext): pass def get_context() -> Context: return Context() router = GraphQLRouter( strawberry.Schema( query=Query, ), context_getter=get_context, ) reveal_type(router) """ def test_router_with_context(): results = typecheck(CODE_ROUTER_WITH_CONTEXT) assert results.pyright == snapshot( [ Result( type="information", message='Type of "router" is "GraphQLRouter[Context, None]"', line=29, column=13, ), ] ) assert results.mypy == snapshot( [ Result( type="note", message='Revealed type is "strawberry.fastapi.router.GraphQLRouter[mypy_test.Context, None]"', line=29, column=13, ), ] ) CODE_ROUTER_WITH_ASYNC_CONTEXT = """ import strawberry from strawberry.fastapi import GraphQLRouter, BaseContext @strawberry.type class Query: @strawberry.field def hello(self) -> str: return "Hello World" class Context(BaseContext): pass async def get_context() -> Context: return Context() router = GraphQLRouter[Context]( strawberry.Schema( query=Query, ), context_getter=get_context, ) reveal_type(router) """ def test_router_with_async_context(): results = typecheck(CODE_ROUTER_WITH_ASYNC_CONTEXT) assert results.pyright == snapshot( [ Result( type="information", message='Type of "router" is "GraphQLRouter[Context, None]"', line=29, column=13, ), ] ) assert results.mypy == snapshot( [ Result( type="note", message='Revealed type is "strawberry.fastapi.router.GraphQLRouter[mypy_test.Context, None]"', line=29, column=13, ), ] ) strawberry-graphql-0.287.0/tests/typecheckers/test_federation.py000066400000000000000000000124621511033167500251230ustar00rootroot00000000000000from inline_snapshot import snapshot from .utils.marks import requires_mypy, requires_pyright, skip_on_windows from .utils.typecheck import Result, typecheck pytestmark = [skip_on_windows, requires_pyright, requires_mypy] CODE = """ import strawberry def get_user_age() -> int: return 0 @strawberry.federation.type class User: name: str age: int = strawberry.field(resolver=get_user_age) something_else: int = strawberry.federation.field(resolver=get_user_age) User(name="Patrick") User(n="Patrick") reveal_type(User) reveal_type(User.__init__) """ def test_federation_type(): results = typecheck(CODE) assert results.pyright == snapshot( [ Result( type="error", message='Argument missing for parameter "name"', line=16, column=1, ), Result(type="error", message='No parameter named "n"', line=16, column=6), Result( type="information", message='Type of "User" is "type[User]"', line=18, column=13, ), Result( type="information", message='Type of "User.__init__" is "(self: User, *, name: str) -> None"', line=19, column=13, ), ] ) assert results.mypy == snapshot( [ Result( type="error", message='Unexpected keyword argument "n" for "User"', line=16, column=1, ), Result( type="note", message='Revealed type is "def (*, name: builtins.str) -> mypy_test.User"', line=18, column=13, ), Result( type="note", message='Revealed type is "def (self: mypy_test.User, *, name: builtins.str)"', line=19, column=13, ), ] ) CODE_INTERFACE = """ import strawberry @strawberry.federation.interface class User: name: str age: int User(name="Patrick", age=1) User(n="Patrick", age=1) reveal_type(User) reveal_type(User.__init__) """ def test_federation_interface(): results = typecheck(CODE_INTERFACE) assert results.pyright == snapshot( [ Result( type="error", message='Argument missing for parameter "name"', line=12, column=1, ), Result(type="error", message='No parameter named "n"', line=12, column=6), Result( type="information", message='Type of "User" is "type[User]"', line=14, column=13, ), Result( type="information", message='Type of "User.__init__" is "(self: User, *, name: str, age: int) -> None"', line=15, column=13, ), ] ) assert results.mypy == snapshot( [ Result( type="error", message='Unexpected keyword argument "n" for "User"', line=12, column=1, ), Result( type="note", message='Revealed type is "def (*, name: builtins.str, age: builtins.int) -> mypy_test.User"', line=14, column=13, ), Result( type="note", message='Revealed type is "def (self: mypy_test.User, *, name: builtins.str, age: builtins.int)"', line=15, column=13, ), ] ) CODE_INPUT = """ import strawberry @strawberry.federation.input class User: name: str User(name="Patrick") User(n="Patrick") reveal_type(User) reveal_type(User.__init__) """ def test_federation_input(): results = typecheck(CODE_INPUT) assert results.pyright == snapshot( [ Result( type="error", message='Argument missing for parameter "name"', line=10, column=1, ), Result(type="error", message='No parameter named "n"', line=10, column=6), Result( type="information", message='Type of "User" is "type[User]"', line=12, column=13, ), Result( type="information", message='Type of "User.__init__" is "(self: User, *, name: str) -> None"', line=13, column=13, ), ] ) assert results.mypy == snapshot( [ Result( type="error", message='Unexpected keyword argument "n" for "User"', line=10, column=1, ), Result( type="note", message='Revealed type is "def (*, name: builtins.str) -> mypy_test.User"', line=12, column=13, ), Result( type="note", message='Revealed type is "def (self: mypy_test.User, *, name: builtins.str)"', line=13, column=13, ), ] ) strawberry-graphql-0.287.0/tests/typecheckers/test_federation_fields.py000066400000000000000000000074001511033167500264450ustar00rootroot00000000000000from inline_snapshot import snapshot from .utils.marks import requires_mypy, requires_pyright, skip_on_windows from .utils.typecheck import Result, typecheck pytestmark = [skip_on_windows, requires_pyright, requires_mypy] CODE = """ import strawberry def some_resolver(root: "User") -> str: return "An address" def some_resolver_2() -> str: return "Another address" @strawberry.federation.type class User: age: int = strawberry.federation.field(description="Age") name: str address: str = strawberry.federation.field(resolver=some_resolver) another_address: str = strawberry.federation.field(resolver=some_resolver_2) @strawberry.federation.input class UserInput: age: int = strawberry.federation.field(description="Age") name: str User(name="Patrick", age=1) User(n="Patrick", age=1) UserInput(name="Patrick", age=1) UserInput(n="Patrick", age=1) reveal_type(User) reveal_type(User.__init__) reveal_type(UserInput) reveal_type(UserInput.__init__) """ def test(): results = typecheck(CODE) assert results.pyright == snapshot( [ Result( type="error", message='Argument missing for parameter "name"', line=24, column=1, ), Result(type="error", message='No parameter named "n"', line=24, column=6), Result( type="error", message='Argument missing for parameter "name"', line=27, column=1, ), Result(type="error", message='No parameter named "n"', line=27, column=11), Result( type="information", message='Type of "User" is "type[User]"', line=29, column=13, ), Result( type="information", message='Type of "User.__init__" is "(self: User, *, age: int, name: str) -> None"', line=30, column=13, ), Result( type="information", message='Type of "UserInput" is "type[UserInput]"', line=32, column=13, ), Result( type="information", message='Type of "UserInput.__init__" is "(self: UserInput, *, age: int, name: str) -> None"', line=33, column=13, ), ] ) assert results.mypy == snapshot( [ Result( type="error", message='Unexpected keyword argument "n" for "User"', line=24, column=1, ), Result( type="error", message='Unexpected keyword argument "n" for "UserInput"', line=27, column=1, ), Result( type="note", message='Revealed type is "def (*, age: builtins.int, name: builtins.str) -> mypy_test.User"', line=29, column=13, ), Result( type="note", message='Revealed type is "def (self: mypy_test.User, *, age: builtins.int, name: builtins.str)"', line=30, column=13, ), Result( type="note", message='Revealed type is "def (*, age: builtins.int, name: builtins.str) -> mypy_test.UserInput"', line=32, column=13, ), Result( type="note", message='Revealed type is "def (self: mypy_test.UserInput, *, age: builtins.int, name: builtins.str)"', line=33, column=13, ), ] ) strawberry-graphql-0.287.0/tests/typecheckers/test_federation_params.py000066400000000000000000000017711511033167500264670ustar00rootroot00000000000000from inline_snapshot import snapshot from .utils.marks import requires_mypy, requires_pyright, skip_on_windows from .utils.typecheck import Result, typecheck pytestmark = [skip_on_windows, requires_pyright, requires_mypy] CODE = """ import strawberry @strawberry.federation.type(name="User") class UserModel: name: str UserModel(name="Patrick") UserModel(n="Patrick") """ def test(): results = typecheck(CODE) assert results.pyright == snapshot( [ Result( type="error", message='Argument missing for parameter "name"', line=11, column=1, ), Result(type="error", message='No parameter named "n"', line=11, column=11), ] ) assert results.mypy == snapshot( [ Result( type="error", message='Unexpected keyword argument "n" for "UserModel"', line=11, column=1, ) ] ) strawberry-graphql-0.287.0/tests/typecheckers/test_fields.py000066400000000000000000000034521511033167500242500ustar00rootroot00000000000000from inline_snapshot import snapshot from .utils.marks import requires_mypy, requires_pyright, skip_on_windows from .utils.typecheck import Result, typecheck pytestmark = [skip_on_windows, requires_pyright, requires_mypy] CODE = """ import strawberry @strawberry.type class User: name: str User(name="Patrick") User(n="Patrick") reveal_type(User) reveal_type(User.__init__) """ def test(): results = typecheck(CODE) assert results.pyright == snapshot( [ Result( type="error", message='Argument missing for parameter "name"', line=11, column=1, ), Result(type="error", message='No parameter named "n"', line=11, column=6), Result( type="information", message='Type of "User" is "type[User]"', line=13, column=13, ), Result( type="information", message='Type of "User.__init__" is "(self: User, *, name: str) -> None"', line=14, column=13, ), ] ) assert results.mypy == snapshot( [ Result( type="error", message='Unexpected keyword argument "n" for "User"', line=11, column=1, ), Result( type="note", message='Revealed type is "def (*, name: builtins.str) -> mypy_test.User"', line=13, column=13, ), Result( type="note", message='Revealed type is "def (self: mypy_test.User, *, name: builtins.str)"', line=14, column=13, ), ] ) strawberry-graphql-0.287.0/tests/typecheckers/test_fields_input.py000066400000000000000000000034531511033167500254700ustar00rootroot00000000000000from inline_snapshot import snapshot from .utils.marks import requires_mypy, requires_pyright, skip_on_windows from .utils.typecheck import Result, typecheck pytestmark = [skip_on_windows, requires_pyright, requires_mypy] CODE = """ import strawberry @strawberry.input class User: name: str User(name="Patrick") User(n="Patrick") reveal_type(User) reveal_type(User.__init__) """ def test(): results = typecheck(CODE) assert results.pyright == snapshot( [ Result( type="error", message='Argument missing for parameter "name"', line=11, column=1, ), Result(type="error", message='No parameter named "n"', line=11, column=6), Result( type="information", message='Type of "User" is "type[User]"', line=13, column=13, ), Result( type="information", message='Type of "User.__init__" is "(self: User, *, name: str) -> None"', line=14, column=13, ), ] ) assert results.mypy == snapshot( [ Result( type="error", message='Unexpected keyword argument "n" for "User"', line=11, column=1, ), Result( type="note", message='Revealed type is "def (*, name: builtins.str) -> mypy_test.User"', line=13, column=13, ), Result( type="note", message='Revealed type is "def (self: mypy_test.User, *, name: builtins.str)"', line=14, column=13, ), ] ) strawberry-graphql-0.287.0/tests/typecheckers/test_fields_keyword.py000066400000000000000000000024351511033167500260140ustar00rootroot00000000000000from inline_snapshot import snapshot from .utils.marks import requires_mypy, requires_pyright, skip_on_windows from .utils.typecheck import Result, typecheck pytestmark = [skip_on_windows, requires_pyright, requires_mypy] CODE = """ import strawberry @strawberry.type class User: name: str User("Patrick") reveal_type(User.__init__) """ def test(): results = typecheck(CODE) assert results.pyright == snapshot( [ Result( type="error", message="Expected 0 positional arguments", line=10, column=6, ), Result( type="information", message='Type of "User.__init__" is "(self: User, *, name: str) -> None"', line=12, column=13, ), ] ) assert results.mypy == snapshot( [ Result( type="error", message='Too many positional arguments for "User"', line=10, column=1, ), Result( type="note", message='Revealed type is "def (self: mypy_test.User, *, name: builtins.str)"', line=12, column=13, ), ] ) strawberry-graphql-0.287.0/tests/typecheckers/test_fields_resolver.py000066400000000000000000000036121511033167500261670ustar00rootroot00000000000000from inline_snapshot import snapshot from .utils.marks import requires_mypy, requires_pyright, skip_on_windows from .utils.typecheck import Result, typecheck pytestmark = [skip_on_windows, requires_pyright, requires_mypy] CODE = """ import strawberry def get_user_age() -> int: return 0 @strawberry.type class User: name: str age: int = strawberry.field(resolver=get_user_age) User(name="Patrick") User(n="Patrick") reveal_type(User) reveal_type(User.__init__) """ def test(): results = typecheck(CODE) assert results.pyright == snapshot( [ Result( type="error", message='Argument missing for parameter "name"', line=15, column=1, ), Result(type="error", message='No parameter named "n"', line=15, column=6), Result( type="information", message='Type of "User" is "type[User]"', line=17, column=13, ), Result( type="information", message='Type of "User.__init__" is "(self: User, *, name: str) -> None"', line=18, column=13, ), ] ) assert results.mypy == snapshot( [ Result( type="error", message='Unexpected keyword argument "n" for "User"', line=15, column=1, ), Result( type="note", message='Revealed type is "def (*, name: builtins.str) -> mypy_test.User"', line=17, column=13, ), Result( type="note", message='Revealed type is "def (self: mypy_test.User, *, name: builtins.str)"', line=18, column=13, ), ] ) strawberry-graphql-0.287.0/tests/typecheckers/test_fields_resolver_async.py000066400000000000000000000047371511033167500273750ustar00rootroot00000000000000from inline_snapshot import snapshot from .utils.marks import requires_mypy, requires_pyright, skip_on_windows from .utils.typecheck import Result, typecheck pytestmark = [skip_on_windows, requires_pyright, requires_mypy] CODE = """ import strawberry async def get_user_age() -> int: return 0 @strawberry.type class User: name: str age: int = strawberry.field(resolver=get_user_age) something: str = strawberry.field(resolver=get_user_age) User(name="Patrick") User(n="Patrick") reveal_type(User) reveal_type(User.__init__) """ def test(): results = typecheck(CODE) assert results.pyright == snapshot( [ Result( type="error", message=( 'Type "int" is not assignable to declared type "str"\n' '\xa0\xa0"int" is not assignable to "str"' ), line=12, column=22, ), Result( type="error", message='Argument missing for parameter "name"', line=16, column=1, ), Result(type="error", message='No parameter named "n"', line=16, column=6), Result( type="information", message='Type of "User" is "type[User]"', line=18, column=13, ), Result( type="information", message='Type of "User.__init__" is "(self: User, *, name: str) -> None"', line=19, column=13, ), ] ) assert results.mypy == snapshot( [ Result( type="error", message='Incompatible types in assignment (expression has type "int", variable has type "str")', line=12, column=22, ), Result( type="error", message='Unexpected keyword argument "n" for "User"', line=16, column=1, ), Result( type="note", message='Revealed type is "def (*, name: builtins.str) -> mypy_test.User"', line=18, column=13, ), Result( type="note", message='Revealed type is "def (self: mypy_test.User, *, name: builtins.str)"', line=19, column=13, ), ] ) strawberry-graphql-0.287.0/tests/typecheckers/test_info.py000066400000000000000000000054551511033167500237420ustar00rootroot00000000000000from inline_snapshot import snapshot from .utils.marks import requires_mypy, requires_pyright, skip_on_windows from .utils.typecheck import Result, typecheck pytestmark = [skip_on_windows, requires_pyright, requires_mypy] def test_with_params(): CODE = """ import strawberry def example(info: strawberry.Info[None, None]) -> None: reveal_type(info.context) reveal_type(info.root_value) """ results = typecheck(CODE) assert results.pyright == snapshot( [ Result( type="information", message='Type of "info.context" is "None"', line=5, column=17, ), Result( type="information", message='Type of "info.root_value" is "None"', line=6, column=17, ), ] ) assert results.mypy == snapshot( [ Result(type="note", message='Revealed type is "None"', line=5, column=17), Result(type="note", message='Revealed type is "None"', line=6, column=17), ] ) def test_with_one_param(): CODE = """ import strawberry def example(info: strawberry.Info[None]) -> None: reveal_type(info.context) reveal_type(info.root_value) """ results = typecheck(CODE) assert results.pyright == snapshot( [ Result( type="information", message='Type of "info.context" is "None"', line=5, column=17, ), Result( type="information", message='Type of "info.root_value" is "Any"', line=6, column=17, ), ] ) assert results.mypy == snapshot( [ Result(type="note", message='Revealed type is "None"', line=5, column=17), Result(type="note", message='Revealed type is "Any"', line=6, column=17), ] ) def test_without_params(): CODE = """ import strawberry def example(info: strawberry.Info) -> None: reveal_type(info.context) reveal_type(info.root_value) """ results = typecheck(CODE) assert results.pyright == snapshot( [ Result( type="information", message='Type of "info.context" is "Any"', line=5, column=17, ), Result( type="information", message='Type of "info.root_value" is "Any"', line=6, column=17, ), ] ) assert results.mypy == snapshot( [ Result(type="note", message='Revealed type is "Any"', line=5, column=17), Result(type="note", message='Revealed type is "Any"', line=6, column=17), ] ) strawberry-graphql-0.287.0/tests/typecheckers/test_interface.py000066400000000000000000000031161511033167500247370ustar00rootroot00000000000000from inline_snapshot import snapshot from .utils.marks import requires_mypy, requires_pyright, skip_on_windows from .utils.typecheck import Result, typecheck pytestmark = [skip_on_windows, requires_pyright, requires_mypy] CODE = """ import strawberry @strawberry.interface class Node: id: strawberry.ID reveal_type(Node) """ def test(): results = typecheck(CODE) assert results.pyright == snapshot( [ Result( type="information", message='Type of "Node" is "type[Node]"', line=9, column=13, ) ] ) assert results.mypy == snapshot( [ Result( type="note", message='Revealed type is "def (*, id: strawberry.scalars.ID) -> mypy_test.Node"', line=9, column=13, ) ] ) CODE_2 = """ import strawberry @strawberry.interface(name="nodeinterface") class Node: id: strawberry.ID reveal_type(Node) """ def test_calling(): results = typecheck(CODE_2) assert results.pyright == snapshot( [ Result( type="information", message='Type of "Node" is "type[Node]"', line=9, column=13, ) ] ) assert results.mypy == snapshot( [ Result( type="note", message='Revealed type is "def (*, id: strawberry.scalars.ID) -> mypy_test.Node"', line=9, column=13, ) ] ) strawberry-graphql-0.287.0/tests/typecheckers/test_maybe.py000066400000000000000000000026271511033167500241020ustar00rootroot00000000000000from inline_snapshot import snapshot from .utils.marks import requires_mypy, requires_pyright, skip_on_windows from .utils.typecheck import Result, typecheck pytestmark = [skip_on_windows, requires_pyright, requires_mypy] CODE = """ import strawberry @strawberry.type class SomeType: foobar: strawberry.Maybe[str] obj = SomeType(foobar=strawberry.Some("some text")) reveal_type(obj.foobar) if obj.foobar: reveal_type(obj.foobar) """ def test_maybe() -> None: result = typecheck(CODE) assert result.pyright == snapshot( [ Result( type="information", message='Type of "obj.foobar" is "Some[str] | None"', line=12, column=13, ), Result( type="information", message='Type of "obj.foobar" is "Some[str]"', line=15, column=17, ), ] ) assert result.mypy == snapshot( [ Result( type="note", message='Revealed type is "strawberry.types.maybe.Some[builtins.str] | None"', line=12, column=13, ), Result( type="note", message='Revealed type is "strawberry.types.maybe.Some[builtins.str]"', line=15, column=17, ), ] ) strawberry-graphql-0.287.0/tests/typecheckers/test_params.py000066400000000000000000000030541511033167500242630ustar00rootroot00000000000000from inline_snapshot import snapshot from .utils.marks import requires_mypy, requires_pyright, skip_on_windows from .utils.typecheck import Result, typecheck pytestmark = [skip_on_windows, requires_pyright, requires_mypy] CODE = """ import strawberry @strawberry.type(name="User") class UserModel: name: str @strawberry.input(name="User") class UserInput: name: str UserModel(name="Patrick") UserModel(n="Patrick") UserInput(name="Patrick") UserInput(n="Patrick") """ def test(): results = typecheck(CODE) assert results.pyright == snapshot( [ Result( type="error", message='Argument missing for parameter "name"', line=16, column=1, ), Result(type="error", message='No parameter named "n"', line=16, column=11), Result( type="error", message='Argument missing for parameter "name"', line=19, column=1, ), Result(type="error", message='No parameter named "n"', line=19, column=11), ] ) assert results.mypy == snapshot( [ Result( type="error", message='Unexpected keyword argument "n" for "UserModel"', line=16, column=1, ), Result( type="error", message='Unexpected keyword argument "n" for "UserInput"', line=19, column=1, ), ] ) strawberry-graphql-0.287.0/tests/typecheckers/test_private.py000066400000000000000000000034051511033167500244520ustar00rootroot00000000000000from inline_snapshot import snapshot from .utils.marks import requires_mypy, requires_pyright, skip_on_windows from .utils.typecheck import Result, typecheck pytestmark = [skip_on_windows, requires_pyright, requires_mypy] CODE = """ import strawberry @strawberry.type class User: name: str age: strawberry.Private[int] patrick = User(name="Patrick", age=1) User(n="Patrick") reveal_type(patrick.name) reveal_type(patrick.age) """ def test(): results = typecheck(CODE) assert results.pyright == snapshot( [ Result( type="error", message='Arguments missing for parameters "name", "age"', line=12, column=1, ), Result(type="error", message='No parameter named "n"', line=12, column=6), Result( type="information", message='Type of "patrick.name" is "str"', line=14, column=13, ), Result( type="information", message='Type of "patrick.age" is "int"', line=15, column=13, ), ] ) assert results.mypy == snapshot( [ Result( type="error", message='Unexpected keyword argument "n" for "User"', line=12, column=1, ), Result( type="note", message='Revealed type is "builtins.str"', line=14, column=13, ), Result( type="note", message='Revealed type is "builtins.int"', line=15, column=13, ), ] ) strawberry-graphql-0.287.0/tests/typecheckers/test_relay.py000066400000000000000000000262461511033167500241240ustar00rootroot00000000000000from inline_snapshot import snapshot from .utils.marks import requires_mypy, requires_pyright, skip_on_windows from .utils.typecheck import Result, typecheck pytestmark = [skip_on_windows, requires_pyright, requires_mypy] CODE = """ from typing import ( Any, AsyncIterator, AsyncGenerator, AsyncIterable, Generator, Iterable, Iterator, Optional, Union, ) import strawberry from strawberry import relay from typing_extensions import Self @strawberry.type class Fruit(relay.Node): id: relay.NodeID[int] name: str color: str @strawberry.type class FruitCustomPaginationConnection(relay.Connection[Fruit]): @strawberry.field def something(self) -> str: return "foobar" @classmethod def resolve_connection( cls, nodes: Union[ Iterator[Fruit], AsyncIterator[Fruit], Iterable[Fruit], AsyncIterable[Fruit], ], *, info: Optional[strawberry.Info] = None, before: Optional[str] = None, after: Optional[str] = None, first: Optional[int] = None, last: Optional[int] = None, **kwargs: Any, ) -> Self: ... class FruitAlike: ... def fruits_resolver() -> list[Fruit]: ... @strawberry.type class Query: node: relay.Node nodes: list[relay.Node] node_optional: Optional[relay.Node] nodes_optional: list[Optional[relay.Node]] fruits: relay.Connection[Fruit] = strawberry.relay.connection( resolver=fruits_resolver, ) fruits_conn: relay.Connection[Fruit] = relay.connection( resolver=fruits_resolver, ) fruits_custom_pagination: FruitCustomPaginationConnection @relay.connection(relay.Connection[Fruit]) def fruits_custom_resolver( self, info: strawberry.Info, name_endswith: Optional[str] = None, ) -> list[Fruit]: ... @relay.connection(relay.Connection[Fruit]) def fruits_custom_resolver_iterator( self, info: strawberry.Info, name_endswith: Optional[str] = None, ) -> Iterator[Fruit]: ... @relay.connection(relay.Connection[Fruit]) def fruits_custom_resolver_iterable( self, info: strawberry.Info, name_endswith: Optional[str] = None, ) -> Iterable[Fruit]: ... @relay.connection(relay.Connection[Fruit]) def fruits_custom_resolver_generator( self, info: strawberry.Info, name_endswith: Optional[str] = None, ) -> Generator[Fruit, None, None]: ... @relay.connection(relay.Connection[Fruit]) async def fruits_custom_resolver_async_iterator( self, info: strawberry.Info, name_endswith: Optional[str] = None, ) -> AsyncIterator[Fruit]: ... @relay.connection(relay.Connection[Fruit]) async def fruits_custom_resolver_async_iterable( self, info: strawberry.Info, name_endswith: Optional[str] = None, ) -> AsyncIterable[Fruit]: ... @relay.connection(relay.Connection[Fruit]) async def fruits_custom_resolver_async_generator( self, info: strawberry.Info, name_endswith: Optional[str] = None, ) -> AsyncGenerator[Fruit, None]: ... reveal_type(Query.node) reveal_type(Query.nodes) reveal_type(Query.node_optional) reveal_type(Query.nodes_optional) reveal_type(Query.fruits) reveal_type(Query.fruits_conn) reveal_type(Query.fruits_custom_pagination) reveal_type(Query.fruits_custom_resolver) reveal_type(Query.fruits_custom_resolver_iterator) reveal_type(Query.fruits_custom_resolver_iterable) reveal_type(Query.fruits_custom_resolver_generator) reveal_type(Query.fruits_custom_resolver_async_iterator) reveal_type(Query.fruits_custom_resolver_async_iterable) reveal_type(Query.fruits_custom_resolver_async_generator) """ def test(): results = typecheck(CODE) assert results.pyright == snapshot( [ Result( type="information", message='Type of "Query.node" is "Node"', line=130, column=13, ), Result( type="information", message='Type of "Query.nodes" is "list[Node]"', line=131, column=13, ), Result( type="information", message='Type of "Query.node_optional" is "Node | None"', line=132, column=13, ), Result( type="information", message='Type of "Query.nodes_optional" is "list[Node | None]"', line=133, column=13, ), Result( type="information", message='Type of "Query.fruits" is "Connection[Fruit]"', line=134, column=13, ), Result( type="information", message='Type of "Query.fruits_conn" is "Connection[Fruit]"', line=135, column=13, ), Result( type="information", message='Type of "Query.fruits_custom_pagination" is "FruitCustomPaginationConnection"', line=136, column=13, ), Result( type="information", message='Type of "Query.fruits_custom_resolver" is "Any"', line=137, column=13, ), Result( type="information", message='Type of "Query.fruits_custom_resolver_iterator" is "Any"', line=138, column=13, ), Result( type="information", message='Type of "Query.fruits_custom_resolver_iterable" is "Any"', line=139, column=13, ), Result( type="information", message='Type of "Query.fruits_custom_resolver_generator" is "Any"', line=140, column=13, ), Result( type="information", message='Type of "Query.fruits_custom_resolver_async_iterator" is "Any"', line=141, column=13, ), Result( type="information", message='Type of "Query.fruits_custom_resolver_async_iterable" is "Any"', line=142, column=13, ), Result( type="information", message='Type of "Query.fruits_custom_resolver_async_generator" is "Any"', line=143, column=13, ), ] ) assert results.mypy == snapshot( [ Result(type="error", message="Missing return statement", line=33, column=5), Result(type="error", message="Missing return statement", line=56, column=1), Result( type="error", message='Untyped decorator makes function "fruits_custom_resolver" untyped', line=74, column=6, ), Result(type="error", message="Missing return statement", line=75, column=5), Result( type="error", message='Untyped decorator makes function "fruits_custom_resolver_iterator" untyped', line=82, column=6, ), Result(type="error", message="Missing return statement", line=83, column=5), Result( type="error", message='Untyped decorator makes function "fruits_custom_resolver_iterable" untyped', line=90, column=6, ), Result(type="error", message="Missing return statement", line=91, column=5), Result( type="error", message='Untyped decorator makes function "fruits_custom_resolver_generator" untyped', line=98, column=6, ), Result(type="error", message="Missing return statement", line=99, column=5), Result( type="error", message='Untyped decorator makes function "fruits_custom_resolver_async_iterator" untyped', line=106, column=6, ), Result( type="error", message="Missing return statement", line=107, column=5 ), Result( type="error", message='Untyped decorator makes function "fruits_custom_resolver_async_iterable" untyped', line=114, column=6, ), Result( type="error", message="Missing return statement", line=115, column=5 ), Result( type="error", message='Untyped decorator makes function "fruits_custom_resolver_async_generator" untyped', line=122, column=6, ), Result( type="error", message="Missing return statement", line=123, column=5 ), Result( type="note", message='Revealed type is "strawberry.relay.types.Node"', line=130, column=13, ), Result( type="note", message='Revealed type is "builtins.list[strawberry.relay.types.Node]"', line=131, column=13, ), Result( type="note", message='Revealed type is "strawberry.relay.types.Node | None"', line=132, column=13, ), Result( type="note", message='Revealed type is "builtins.list[strawberry.relay.types.Node | None]"', line=133, column=13, ), Result( type="note", message='Revealed type is "strawberry.relay.types.Connection[mypy_test.Fruit]"', line=134, column=13, ), Result( type="note", message='Revealed type is "strawberry.relay.types.Connection[mypy_test.Fruit]"', line=135, column=13, ), Result( type="note", message='Revealed type is "mypy_test.FruitCustomPaginationConnection"', line=136, column=13, ), Result(type="note", message='Revealed type is "Any"', line=137, column=13), Result(type="note", message='Revealed type is "Any"', line=138, column=13), Result(type="note", message='Revealed type is "Any"', line=139, column=13), Result(type="note", message='Revealed type is "Any"', line=140, column=13), Result(type="note", message='Revealed type is "Any"', line=141, column=13), Result(type="note", message='Revealed type is "Any"', line=142, column=13), Result(type="note", message='Revealed type is "Any"', line=143, column=13), ] ) strawberry-graphql-0.287.0/tests/typecheckers/test_scalars.py000066400000000000000000000072451511033167500244360ustar00rootroot00000000000000from inline_snapshot import snapshot from .utils.marks import requires_mypy, requires_pyright, skip_on_windows from .utils.typecheck import Result, typecheck pytestmark = [skip_on_windows, requires_pyright, requires_mypy] CODE = """ import strawberry from strawberry.scalars import ID, JSON, Base16, Base32, Base64 @strawberry.type class SomeType: id: ID json: JSON base16: Base16 base32: Base32 base64: Base64 obj = SomeType( id=ID("123"), json=JSON({"foo": "bar"}), base16=Base16(b""), base32=Base32(b""), base64=Base64(b""), ) reveal_type(obj.id) reveal_type(obj.json) reveal_type(obj.base16) reveal_type(obj.base16) reveal_type(obj.base64) """ def test(): results = typecheck(CODE) # NOTE: This is also guaranteeing that those scalars could be used to annotate # the attributes. Pyright 1.1.224+ doesn't allow non-types to be used there assert results.pyright == snapshot( [ Result( type="information", message='Type of "obj.id" is "ID"', line=23, column=13, ), Result( type="information", message='Type of "obj.json" is "JSON"', line=24, column=13, ), Result( type="information", message='Type of "obj.base16" is "Base16"', line=25, column=13, ), Result( type="information", message='Type of "obj.base16" is "Base16"', line=26, column=13, ), Result( type="information", message='Type of "obj.base64" is "Base64"', line=27, column=13, ), ] ) assert results.mypy == snapshot( [ Result( type="note", message='Revealed type is "strawberry.scalars.ID"', line=23, column=13, ), Result(type="note", message='Revealed type is "Any"', line=24, column=13), Result(type="note", message='Revealed type is "Any"', line=25, column=13), Result(type="note", message='Revealed type is "Any"', line=26, column=13), Result(type="note", message='Revealed type is "Any"', line=27, column=13), ] ) CODE_SCHEMA_OVERRIDES = """ import strawberry from datetime import datetime, timezone EpochDateTime = strawberry.scalar( datetime, ) @strawberry.type class Query: a: datetime schema = strawberry.Schema(query=Query, scalar_overrides={ datetime: EpochDateTime, }) reveal_type(EpochDateTime) """ def test_schema_overrides(): # TODO: change strict to true when we improve type hints for scalar results = typecheck(CODE_SCHEMA_OVERRIDES, strict=False) assert results.pyright == snapshot( [ Result( type="information", message='Type of "EpochDateTime" is "type[datetime]"', line=16, column=13, ) ] ) assert results.mypy == snapshot( [ Result( type="note", message='Revealed type is "def (year: typing.SupportsIndex, month: typing.SupportsIndex, day: typing.SupportsIndex, hour: typing.SupportsIndex =, minute: typing.SupportsIndex =, second: typing.SupportsIndex =, microsecond: typing.SupportsIndex =, tzinfo: datetime.tzinfo | None =, *, fold: builtins.int =) -> datetime.datetime"', line=17, column=13, ) ] ) strawberry-graphql-0.287.0/tests/typecheckers/test_type.py000066400000000000000000000102061511033167500237560ustar00rootroot00000000000000from inline_snapshot import snapshot from .utils.marks import requires_mypy, requires_pyright, skip_on_windows from .utils.typecheck import Result, typecheck pytestmark = [skip_on_windows, requires_pyright, requires_mypy] CODE = """ import strawberry from strawberry.types.base import StrawberryOptional, StrawberryList @strawberry.type class Fruit: name: str reveal_type(StrawberryOptional(Fruit)) reveal_type(StrawberryList(Fruit)) reveal_type(StrawberryOptional(StrawberryList(Fruit))) reveal_type(StrawberryList(StrawberryOptional(Fruit))) reveal_type(StrawberryOptional(str)) reveal_type(StrawberryList(str)) reveal_type(StrawberryOptional(StrawberryList(str))) reveal_type(StrawberryList(StrawberryOptional(str))) """ def test(): results = typecheck(CODE) assert results.pyright == snapshot( [ Result( type="information", message='Type of "StrawberryOptional(Fruit)" is "StrawberryOptional"', line=11, column=13, ), Result( type="information", message='Type of "StrawberryList(Fruit)" is "StrawberryList"', line=12, column=13, ), Result( type="information", message='Type of "StrawberryOptional(StrawberryList(Fruit))" is "StrawberryOptional"', line=13, column=13, ), Result( type="information", message='Type of "StrawberryList(StrawberryOptional(Fruit))" is "StrawberryList"', line=14, column=13, ), Result( type="information", message='Type of "StrawberryOptional(str)" is "StrawberryOptional"', line=16, column=13, ), Result( type="information", message='Type of "StrawberryList(str)" is "StrawberryList"', line=17, column=13, ), Result( type="information", message='Type of "StrawberryOptional(StrawberryList(str))" is "StrawberryOptional"', line=18, column=13, ), Result( type="information", message='Type of "StrawberryList(StrawberryOptional(str))" is "StrawberryList"', line=19, column=13, ), ] ) assert results.mypy == snapshot( [ Result( type="note", message='Revealed type is "strawberry.types.base.StrawberryOptional"', line=11, column=13, ), Result( type="note", message='Revealed type is "strawberry.types.base.StrawberryList"', line=12, column=13, ), Result( type="note", message='Revealed type is "strawberry.types.base.StrawberryOptional"', line=13, column=13, ), Result( type="note", message='Revealed type is "strawberry.types.base.StrawberryList"', line=14, column=13, ), Result( type="note", message='Revealed type is "strawberry.types.base.StrawberryOptional"', line=16, column=13, ), Result( type="note", message='Revealed type is "strawberry.types.base.StrawberryList"', line=17, column=13, ), Result( type="note", message='Revealed type is "strawberry.types.base.StrawberryOptional"', line=18, column=13, ), Result( type="note", message='Revealed type is "strawberry.types.base.StrawberryList"', line=19, column=13, ), ] ) strawberry-graphql-0.287.0/tests/typecheckers/test_union.py000066400000000000000000000026721511033167500241350ustar00rootroot00000000000000from inline_snapshot import snapshot from .utils.marks import requires_mypy, requires_pyright, skip_on_windows from .utils.typecheck import Result, typecheck pytestmark = [skip_on_windows, requires_pyright, requires_mypy] CODE = """ import strawberry from typing_extensions import TypeAlias, Annotated from typing import Union @strawberry.type class User: name: str @strawberry.type class Error: message: str UserOrError: TypeAlias = Annotated[ Union[User, Error], strawberry.union("UserOrError") ] reveal_type(UserOrError) x: UserOrError = User(name="Patrick") reveal_type(x) """ def test(): results = typecheck(CODE) assert results.pyright == snapshot( [ Result( type="information", message='Type of "UserOrError" is "Annotated"', line=19, column=13, ), Result( type="information", message='Type of "x" is "User"', line=23, column=13 ), ] ) assert results.mypy == snapshot( [ Result( type="note", message='Revealed type is "typing._SpecialForm"', line=19, column=13, ), Result( type="note", message='Revealed type is "mypy_test.User | mypy_test.Error"', line=23, column=13, ), ] ) strawberry-graphql-0.287.0/tests/typecheckers/utils/000077500000000000000000000000001511033167500225255ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/typecheckers/utils/__init__.py000066400000000000000000000000001511033167500246240ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/typecheckers/utils/marks.py000066400000000000000000000010271511033167500242140ustar00rootroot00000000000000import shutil import sys import pytest def pyright_exist() -> bool: return shutil.which("pyright") is not None def mypy_exists() -> bool: return shutil.which("mypy") is not None skip_on_windows = pytest.mark.skipif( sys.platform == "win32", reason="Do not run pyright on windows due to path issues", ) requires_pyright = pytest.mark.skipif( not pyright_exist(), reason="These tests require pyright", ) requires_mypy = pytest.mark.skipif( not mypy_exists(), reason="These tests require mypy", ) strawberry-graphql-0.287.0/tests/typecheckers/utils/mypy.py000066400000000000000000000041471511033167500241030ustar00rootroot00000000000000from __future__ import annotations import json import os import pathlib import subprocess import tempfile from typing import TypedDict from .result import Result class PyrightCLIResult(TypedDict): version: str time: str generalDiagnostics: list[GeneralDiagnostic] summary: Summary class GeneralDiagnostic(TypedDict): file: str severity: str message: str range: Range class Range(TypedDict): start: EndOrStart end: EndOrStart class EndOrStart(TypedDict): line: int character: int class Summary(TypedDict): filesAnalyzed: int errorCount: int warningCount: int informationCount: int timeInSec: float def run_mypy(code: str, strict: bool = True) -> list[Result]: args = ["mypy", "--output=json"] if strict: args.append("--strict") with tempfile.TemporaryDirectory() as directory: module_path = pathlib.Path(directory) / "mypy_test.py" module_path.write_text(code) process_result = subprocess.run( [*args, str(module_path)], check=False, capture_output=True, env={ "PYTHONWARNINGS": "error,ignore::SyntaxWarning", "PATH": os.environ["PATH"], }, ) full_output = ( process_result.stdout.decode("utf-8") + "\n" + process_result.stderr.decode("utf-8") ) full_output = full_output.strip() results: list[Result] = [] try: for line in full_output.split("\n"): mypy_result = json.loads(line) results.append( Result( type=mypy_result["severity"].strip(), message=mypy_result["message"].strip(), line=mypy_result["line"], column=mypy_result["column"] + 1, ) ) except json.JSONDecodeError as e: raise ValueError(f"Invalid JSON: {full_output}") from e results.sort(key=lambda x: (x.line, x.column, x.message)) return results strawberry-graphql-0.287.0/tests/typecheckers/utils/pyright.py000066400000000000000000000032661511033167500245740ustar00rootroot00000000000000from __future__ import annotations import json import os import subprocess import tempfile from typing import TypedDict, cast from .result import Result, ResultType class PyrightCLIResult(TypedDict): version: str time: str generalDiagnostics: list[GeneralDiagnostic] summary: Summary class GeneralDiagnostic(TypedDict): file: str severity: str message: str range: Range class Range(TypedDict): start: EndOrStart end: EndOrStart class EndOrStart(TypedDict): line: int character: int class Summary(TypedDict): filesAnalyzed: int errorCount: int warningCount: int informationCount: int timeInSec: float def run_pyright(code: str, strict: bool = True) -> list[Result]: if strict: code = "# pyright: strict\n" + code with tempfile.NamedTemporaryFile("w", suffix=".py", delete=False) as f: f.write(code) process_result = subprocess.run( ["pyright", "--outputjson", f.name], capture_output=True, check=False ) assert not process_result.stderr.decode("utf-8") os.unlink(f.name) # noqa: PTH108 pyright_result: PyrightCLIResult = json.loads(process_result.stdout.decode("utf-8")) result = [ Result( type=cast("ResultType", diagnostic["severity"].strip()), message=diagnostic["message"].strip(), line=diagnostic["range"]["start"]["line"], column=diagnostic["range"]["start"]["character"] + 1, ) for diagnostic in pyright_result["generalDiagnostics"] ] # make sure that results are sorted by line and column and then message result.sort(key=lambda x: (x.line, x.column, x.message)) return result strawberry-graphql-0.287.0/tests/typecheckers/utils/result.py000066400000000000000000000003411511033167500244130ustar00rootroot00000000000000from dataclasses import dataclass from typing import Literal ResultType = Literal[ "error", "information", "note", ] @dataclass class Result: type: ResultType message: str line: int column: int strawberry-graphql-0.287.0/tests/typecheckers/utils/typecheck.py000066400000000000000000000013061511033167500250560ustar00rootroot00000000000000from __future__ import annotations import concurrent.futures from dataclasses import dataclass from .mypy import run_mypy from .pyright import run_pyright from .result import Result @dataclass class TypecheckResult: pyright: list[Result] mypy: list[Result] def typecheck(code: str, strict: bool = True) -> TypecheckResult: with concurrent.futures.ThreadPoolExecutor() as executor: pyright_future = executor.submit(run_pyright, code, strict=strict) mypy_future = executor.submit(run_mypy, code, strict=strict) pyright_results = pyright_future.result() mypy_results = mypy_future.result() return TypecheckResult(pyright=pyright_results, mypy=mypy_results) strawberry-graphql-0.287.0/tests/types/000077500000000000000000000000001511033167500200405ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/types/__init__.py000066400000000000000000000000001511033167500221370ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/types/cross_module_resolvers/000077500000000000000000000000001511033167500246425ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/types/cross_module_resolvers/README.md000066400000000000000000000025571511033167500261320ustar00rootroot00000000000000# Module tests This directory contains the modules needed for the `test_cross_module_resolvers.py` file in the directory above. ## Problem In an earlier strawberry version the resolver did add itself as the origin of fields. This caused the type resolving to fail when using a resolver from a different module: ```python from other_module import generic_resolver @strawberry.field class Foo: bar: "Bar" = strawberry.field(resolver=generic_resolver) @strawberry.field class Bar: awesome: bool ``` Since the `origin` of the `Foo.bar` field was set to `generic_resolver` the `Bar` type was not looked up relative to the `Foo` class but the `generic_resolver` causing the type lookup to fail. ## Robust tests **Important**: For these tests not to mask any incorrect resolution behavior, all type names are unique across all modules. E.g. when importing `a.AObject` into the `c` module it is renamed to `C_AObject`. This ensures that we can discern which module the object is coming from and any incorrect resolution behavior can be detected. ## Submodules This module contains four submodules which are used to test various cross-module references: - `a` contains standalone types - `b` contains standalone types - `c` contains types that either inherit from the types in the `a` and `b` module or contain references to them - `x` contains typeless (generic) resolvers strawberry-graphql-0.287.0/tests/types/cross_module_resolvers/__init__.py000066400000000000000000000000001511033167500267410ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/types/cross_module_resolvers/a_mod.py000066400000000000000000000004161511033167500262740ustar00rootroot00000000000000import strawberry def a_resolver() -> list["AObject"]: return [] @strawberry.type class ABase: a_name: str @strawberry.type class AObject(ABase): a_age: int @strawberry.field def a_is_of_full_age(self) -> bool: return self.a_age >= 18 strawberry-graphql-0.287.0/tests/types/cross_module_resolvers/b_mod.py000066400000000000000000000004701511033167500262750ustar00rootroot00000000000000import strawberry def b_resolver() -> list["BObject"]: return [] @strawberry.type class BBase: b_name: str = strawberry.field() @strawberry.type class BObject(BBase): b_age: int = strawberry.field() @strawberry.field def b_is_of_full_age(self) -> bool: return self.b_age >= 18 strawberry-graphql-0.287.0/tests/types/cross_module_resolvers/c_mod.py000066400000000000000000000026221511033167500262770ustar00rootroot00000000000000import strawberry from . import a_mod, b_mod, x_mod from .a_mod import AObject as C_AObject from .b_mod import BObject as C_BObject def c_inheritance_resolver() -> list["CInheritance"]: pass def c_composition_resolver() -> list["CComposition"]: pass def c_composition_by_name_resolver() -> list["CCompositionByName"]: pass @strawberry.type class CInheritance(a_mod.AObject, b_mod.BObject): pass @strawberry.type class CComposition: a_list: list[a_mod.AObject] b_list: list[b_mod.BObject] @strawberry.type class CCompositionByName: a_list: list["C_AObject"] b_list: list["C_BObject"] @strawberry.field def a_method(self) -> list["C_AObject"]: return self.a_list @strawberry.field def b_method(self) -> list["C_BObject"]: return self.b_list @strawberry.type class CCompositionByNameWithResolvers: a_list: list["C_AObject"] = strawberry.field(resolver=a_mod.a_resolver) b_list: list["C_BObject"] = strawberry.field(resolver=b_mod.b_resolver) @strawberry.type class CCompositionByNameWithTypelessResolvers: a_list: list["C_AObject"] = strawberry.field(resolver=x_mod.typeless_resolver) b_list: list["C_BObject"] = strawberry.field(resolver=x_mod.typeless_resolver) @strawberry.type class CCompositionOnlyResolvers: a_list = strawberry.field(resolver=a_mod.a_resolver) b_list = strawberry.field(resolver=b_mod.b_resolver) strawberry-graphql-0.287.0/tests/types/cross_module_resolvers/test_cross_module_resolvers.py000066400000000000000000000105401511033167500330550ustar00rootroot00000000000000"""The following tests ensure that the types are resolved using the correct module. Concrete types should be non-problematic and are only included here for completeness. A problematic case is when a type is a string (forward reference) and can only be resolved at schema construction. """ import strawberry from . import a_mod, b_mod, c_mod, x_mod def test_a(): @strawberry.type class Query: a_list: list[a_mod.AObject] [field] = Query.__strawberry_definition__.fields assert field.type == list[a_mod.AObject] def test_a_resolver(): @strawberry.type class Query: a_list: list[a_mod.AObject] = strawberry.field(resolver=a_mod.a_resolver) [field] = Query.__strawberry_definition__.fields assert field.type == list[a_mod.AObject] def test_a_only_resolver(): @strawberry.type class Query: a_list = strawberry.field(resolver=a_mod.a_resolver) [field] = Query.__strawberry_definition__.fields assert field.type == list[a_mod.AObject] def test_a_typeless_resolver(): @strawberry.type class Query: a_list: list[a_mod.AObject] = strawberry.field(resolver=x_mod.typeless_resolver) [field] = Query.__strawberry_definition__.fields assert field.type == list[a_mod.AObject] def test_c_composition_by_name(): [ a_field, b_field, a_method, b_method, ] = c_mod.CCompositionByName.__strawberry_definition__.fields assert a_field.type == list[a_mod.AObject] assert b_field.type == list[b_mod.BObject] assert a_method.type == list[a_mod.AObject] assert b_method.type == list[b_mod.BObject] def test_c_inheritance(): [ a_name, a_age, a_is_of_full_age, b_name, b_age, b_is_of_full_age, ] = c_mod.CInheritance.__strawberry_definition__.fields assert a_name.origin == a_mod.ABase assert a_age.origin == a_mod.AObject assert a_is_of_full_age.origin == a_mod.AObject assert b_name.origin == b_mod.BBase assert b_age.origin == b_mod.BObject assert b_is_of_full_age.origin == b_mod.BObject def test_c_inheritance_resolver(): @strawberry.type class Query: c: list[c_mod.CInheritance] = strawberry.field( resolver=c_mod.c_inheritance_resolver ) [field] = Query.__strawberry_definition__.fields assert field.type == list[c_mod.CInheritance] def test_c_inheritance_typeless_resolver(): @strawberry.type class Query: c: list[c_mod.CInheritance] = strawberry.field(resolver=x_mod.typeless_resolver) [field] = Query.__strawberry_definition__.fields assert field.type == list[c_mod.CInheritance] def test_c_inheritance_resolver_only(): @strawberry.type class Query: c = strawberry.field(resolver=c_mod.c_inheritance_resolver) [field] = Query.__strawberry_definition__.fields assert field.type == list[c_mod.CInheritance] def test_c_composition_resolver(): @strawberry.type class Query: c: list[c_mod.CComposition] = strawberry.field( resolver=c_mod.c_composition_resolver ) [field] = Query.__strawberry_definition__.fields assert field.type == list[c_mod.CComposition] [a_field, b_field] = field.type.of_type.__strawberry_definition__.fields assert a_field.type == list[a_mod.AObject] assert b_field.type == list[b_mod.BObject] def test_c_composition_by_name_with_resolvers(): [ a_field, b_field, ] = c_mod.CCompositionByNameWithResolvers.__strawberry_definition__.fields assert a_field.type == list[a_mod.AObject] assert b_field.type == list[b_mod.BObject] def test_c_composition_by_name_with_typeless_resolvers(): [ a_field, b_field, ] = c_mod.CCompositionByNameWithTypelessResolvers.__strawberry_definition__.fields assert a_field.type == list[a_mod.AObject] assert b_field.type == list[b_mod.BObject] def test_c_composition_only_resolvers(): [ a_field, b_field, ] = c_mod.CCompositionOnlyResolvers.__strawberry_definition__.fields assert a_field.type == list[a_mod.AObject] assert b_field.type == list[b_mod.BObject] def test_x_resolver(): @strawberry.type class Query: c: list[a_mod.AObject] = strawberry.field(resolver=x_mod.typeless_resolver) [c_field] = Query.__strawberry_definition__.fields assert c_field.type == list[a_mod.AObject] strawberry-graphql-0.287.0/tests/types/cross_module_resolvers/x_mod.py000066400000000000000000000001031511033167500263140ustar00rootroot00000000000000def typeless_resolver() -> list: # pragma: no cover return [] strawberry-graphql-0.287.0/tests/types/resolving/000077500000000000000000000000001511033167500220505ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/types/resolving/__init__.py000066400000000000000000000000001511033167500241470ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/types/resolving/test_enums.py000066400000000000000000000007241511033167500246130ustar00rootroot00000000000000from enum import Enum import strawberry from strawberry.annotation import StrawberryAnnotation def test_basic(): @strawberry.enum class NumaNuma(Enum): MA = "ma" I = "i" # noqa: E741 A = "a" HI = "hi" annotation = StrawberryAnnotation(NumaNuma) resolved = annotation.resolve() # TODO: Remove reference to .enum_definition with StrawberryEnumDefinition assert resolved is NumaNuma.__strawberry_definition__ strawberry-graphql-0.287.0/tests/types/resolving/test_forward_references.py000066400000000000000000000015051511033167500273270ustar00rootroot00000000000000import pytest import strawberry from strawberry.annotation import StrawberryAnnotation def test_forward_reference(): global ForwardClass annotation = StrawberryAnnotation("ForwardClass", namespace=globals()) @strawberry.type class ForwardClass: backward: bool resolved = annotation.resolve() assert resolved is ForwardClass del ForwardClass @pytest.mark.xfail(reason="Combining locals() and globals() strangely makes this fail") def test_forward_reference_locals_and_globals(): global BackwardClass namespace = {**locals(), **globals()} annotation = StrawberryAnnotation("BackwardClass", namespace=namespace) @strawberry.type class BackwardClass: backward: bool resolved = annotation.resolve() assert resolved is BackwardClass del BackwardClass strawberry-graphql-0.287.0/tests/types/resolving/test_generics.py000066400000000000000000000073641511033167500252720ustar00rootroot00000000000000from enum import Enum from typing import Generic, Optional, TypeVar, Union import pytest import strawberry from strawberry.annotation import StrawberryAnnotation from strawberry.types.base import ( StrawberryList, StrawberryObjectDefinition, StrawberryOptional, StrawberryTypeVar, get_object_definition, has_object_definition, ) from strawberry.types.enum import StrawberryEnumDefinition from strawberry.types.field import StrawberryField from strawberry.types.union import StrawberryUnion def test_basic_generic(): T = TypeVar("T") annotation = StrawberryAnnotation(T) resolved = annotation.resolve() assert isinstance(resolved, StrawberryTypeVar) assert resolved.is_graphql_generic assert resolved.type_var is T assert resolved == T def test_generic_lists(): T = TypeVar("T") annotation = StrawberryAnnotation(list[T]) resolved = annotation.resolve() assert isinstance(resolved, StrawberryList) assert isinstance(resolved.of_type, StrawberryTypeVar) assert resolved.is_graphql_generic assert resolved == list[T] def test_generic_objects(): T = TypeVar("T") @strawberry.type class FooBar(Generic[T]): thing: T annotation = StrawberryAnnotation(FooBar) resolved = annotation.resolve() # TODO: Simplify with StrawberryObject assert isinstance(resolved, type) assert has_object_definition(resolved) assert isinstance(resolved.__strawberry_definition__, StrawberryObjectDefinition) assert resolved.__strawberry_definition__.is_graphql_generic field: StrawberryField = resolved.__strawberry_definition__.fields[0] assert isinstance(field.type, StrawberryTypeVar) assert field.type == T def test_generic_optionals(): T = TypeVar("T") annotation = StrawberryAnnotation(Optional[T]) resolved = annotation.resolve() assert isinstance(resolved, StrawberryOptional) assert isinstance(resolved.of_type, StrawberryTypeVar) assert resolved.is_graphql_generic assert resolved == Optional[T] def test_generic_unions(): S = TypeVar("S") T = TypeVar("T") annotation = StrawberryAnnotation(Union[S, T]) resolved = annotation.resolve() assert isinstance(resolved, StrawberryUnion) assert resolved.types == (S, T) assert resolved.is_graphql_generic assert resolved == Union[S, T] def test_generic_with_enums(): T = TypeVar("T") @strawberry.enum class VehicleMake(Enum): FORD = "ford" TOYOTA = "toyota" HONDA = "honda" @strawberry.type class GenericForEnum(Generic[T]): generic_slot: T annotation = StrawberryAnnotation(GenericForEnum[VehicleMake]) resolved = annotation.resolve() # TODO: Simplify with StrawberryObject assert isinstance(resolved, type) assert has_object_definition(resolved) assert isinstance(resolved.__strawberry_definition__, StrawberryObjectDefinition) generic_slot_field: StrawberryField = resolved.__strawberry_definition__.fields[0] assert isinstance(generic_slot_field.type, StrawberryEnumDefinition) assert generic_slot_field.type is VehicleMake.__strawberry_definition__ def test_cant_create_concrete_of_non_strawberry_object(): T = TypeVar("T") @strawberry.type class Foo(Generic[T]): generic_slot: T with pytest.raises(ValueError): StrawberryAnnotation(Foo).create_concrete_type(int) def test_inline_resolver(): T = TypeVar("T") @strawberry.type class Edge(Generic[T]): @strawberry.field def node(self) -> T: # type: ignore # pragma: no cover ... resolved = StrawberryAnnotation(Edge).resolve() type_definition = get_object_definition(resolved, strict=True) assert type_definition.is_graphql_generic strawberry-graphql-0.287.0/tests/types/resolving/test_lists.py000066400000000000000000000107251511033167500246240ustar00rootroot00000000000000from collections.abc import Sequence from typing import Optional, Union import strawberry from strawberry.annotation import StrawberryAnnotation from strawberry.types.base import StrawberryList def test_basic_list(): annotation = StrawberryAnnotation(list[str]) resolved = annotation.resolve() assert isinstance(resolved, StrawberryList) assert resolved.of_type is str assert resolved == StrawberryList(of_type=str) assert resolved == list[str] def test_basic_tuple(): annotation = StrawberryAnnotation(tuple[str]) resolved = annotation.resolve() assert isinstance(resolved, StrawberryList) assert resolved.of_type is str assert resolved == StrawberryList(of_type=str) assert resolved == tuple[str] def test_basic_sequence(): annotation = StrawberryAnnotation(Sequence[str]) resolved = annotation.resolve() assert isinstance(resolved, StrawberryList) assert resolved.of_type is str assert resolved == StrawberryList(of_type=str) assert resolved == Sequence[str] def test_list_of_optional(): annotation = StrawberryAnnotation(list[int | None]) resolved = annotation.resolve() assert isinstance(resolved, StrawberryList) assert resolved.of_type == Optional[int] assert resolved == StrawberryList(of_type=Optional[int]) assert resolved == list[int | None] def test_sequence_of_optional(): annotation = StrawberryAnnotation(Sequence[int | None]) resolved = annotation.resolve() assert isinstance(resolved, StrawberryList) assert resolved.of_type == Optional[int] assert resolved == StrawberryList(of_type=Optional[int]) assert resolved == Sequence[int | None] def test_tuple_of_optional(): annotation = StrawberryAnnotation(tuple[int | None]) resolved = annotation.resolve() assert isinstance(resolved, StrawberryList) assert resolved.of_type == Optional[int] assert resolved == StrawberryList(of_type=Optional[int]) assert resolved == tuple[int | None] def test_list_of_lists(): annotation = StrawberryAnnotation(list[list[float]]) resolved = annotation.resolve() assert isinstance(resolved, StrawberryList) assert resolved.of_type == list[float] assert resolved == StrawberryList(of_type=list[float]) assert resolved == list[list[float]] def test_sequence_of_sequence(): annotation = StrawberryAnnotation(Sequence[Sequence[float]]) resolved = annotation.resolve() assert isinstance(resolved, StrawberryList) assert resolved.of_type == Sequence[float] assert resolved == StrawberryList(of_type=Sequence[float]) assert resolved == Sequence[Sequence[float]] def test_tuple_of_tuple(): annotation = StrawberryAnnotation(tuple[tuple[float]]) resolved = annotation.resolve() assert isinstance(resolved, StrawberryList) assert resolved.of_type == tuple[float] assert resolved == StrawberryList(of_type=tuple[float]) assert resolved == tuple[tuple[float]] def test_list_of_union(): @strawberry.type class Animal: feet: bool @strawberry.type class Fungus: spore: bool annotation = StrawberryAnnotation(list[Animal | Fungus]) resolved = annotation.resolve() assert isinstance(resolved, StrawberryList) assert resolved.of_type == Union[Animal, Fungus] assert resolved == StrawberryList(of_type=Union[Animal, Fungus]) assert resolved == list[Animal | Fungus] def test_sequence_of_union(): @strawberry.type class Animal: feet: bool @strawberry.type class Fungus: spore: bool annotation = StrawberryAnnotation(Sequence[Animal | Fungus]) resolved = annotation.resolve() assert isinstance(resolved, StrawberryList) assert resolved.of_type == Union[Animal, Fungus] assert resolved == StrawberryList(of_type=Union[Animal, Fungus]) assert resolved == Sequence[Animal | Fungus] def test_list_builtin(): annotation = StrawberryAnnotation(list[str]) resolved = annotation.resolve() assert isinstance(resolved, StrawberryList) assert resolved.of_type is str assert resolved == StrawberryList(of_type=str) assert resolved == list[str] assert resolved == list[str] def test_tuple_builtin(): annotation = StrawberryAnnotation(tuple[str]) resolved = annotation.resolve() assert isinstance(resolved, StrawberryList) assert resolved.of_type is str assert resolved == StrawberryList(of_type=str) assert resolved == tuple[str] assert resolved == tuple[str] strawberry-graphql-0.287.0/tests/types/resolving/test_literals.py000066400000000000000000000016351511033167500253050ustar00rootroot00000000000000from typing import Optional, Union from strawberry.annotation import StrawberryAnnotation def test_bool(): annotation = StrawberryAnnotation(bool) resolved = annotation.resolve() assert resolved is bool def test_float(): annotation = StrawberryAnnotation(float) resolved = annotation.resolve() assert resolved is float def test_int(): annotation = StrawberryAnnotation(int) resolved = annotation.resolve() assert resolved is int def test_str(): annotation = StrawberryAnnotation(str) resolved = annotation.resolve() assert resolved is str def test_none(): annotation = StrawberryAnnotation(None) annotation.resolve() annotation = StrawberryAnnotation(type(None)) annotation.resolve() annotation = StrawberryAnnotation(Optional[int]) annotation.resolve() annotation = StrawberryAnnotation(Union[None, int]) annotation.resolve() strawberry-graphql-0.287.0/tests/types/resolving/test_optionals.py000066400000000000000000000135641511033167500255020ustar00rootroot00000000000000from typing import Optional, Union import strawberry from strawberry.annotation import StrawberryAnnotation from strawberry.types.base import StrawberryOptional from strawberry.types.unset import UnsetType def test_basic_optional(): annotation = StrawberryAnnotation(Optional[str]) resolved = annotation.resolve() assert isinstance(resolved, StrawberryOptional) assert resolved.of_type is str assert resolved == StrawberryOptional(of_type=str) assert resolved == Optional[str] def test_optional_with_unset(): annotation = StrawberryAnnotation(Union[UnsetType, str | None]) resolved = annotation.resolve() assert isinstance(resolved, StrawberryOptional) assert resolved.of_type is str assert resolved == StrawberryOptional(of_type=str) assert resolved == Optional[str] def test_optional_with_type_of_unset(): annotation = StrawberryAnnotation(Union[type[strawberry.UNSET], str | None]) resolved = annotation.resolve() assert isinstance(resolved, StrawberryOptional) assert resolved.of_type is str assert resolved == StrawberryOptional(of_type=str) assert resolved == Optional[str] def test_optional_with_unset_as_union(): annotation = StrawberryAnnotation(Union[UnsetType, None, str]) resolved = annotation.resolve() assert isinstance(resolved, StrawberryOptional) assert resolved.of_type is str assert resolved == StrawberryOptional(of_type=str) assert resolved == Optional[str] def test_optional_list(): annotation = StrawberryAnnotation(Optional[list[bool]]) resolved = annotation.resolve() assert isinstance(resolved, StrawberryOptional) assert resolved.of_type == list[bool] assert resolved == StrawberryOptional(of_type=list[bool]) assert resolved == Optional[list[bool]] def test_optional_optional(): """Optional[Optional[...]] is squashed by Python to just Optional[...]""" annotation = StrawberryAnnotation(Optional[bool | None]) resolved = annotation.resolve() assert isinstance(resolved, StrawberryOptional) assert resolved.of_type is bool assert resolved == StrawberryOptional(of_type=bool) assert resolved == Optional[bool | None] assert resolved == Optional[bool] def test_optional_union(): @strawberry.type class CoolType: foo: float @strawberry.type class UncoolType: bar: bool annotation = StrawberryAnnotation(Optional[CoolType | UncoolType]) resolved = annotation.resolve() assert isinstance(resolved, StrawberryOptional) assert resolved.of_type == Union[CoolType, UncoolType] assert resolved == StrawberryOptional(of_type=Union[CoolType, UncoolType]) assert resolved == Optional[CoolType | UncoolType] # TODO: move to a field test file def test_type_add_type_definition_with_fields(): @strawberry.type class Query: name: str | None age: int | None definition = Query.__strawberry_definition__ assert definition.name == "Query" [field1, field2] = definition.fields assert field1.python_name == "name" assert field1.graphql_name is None assert isinstance(field1.type, StrawberryOptional) assert field1.type.of_type is str assert field2.python_name == "age" assert field2.graphql_name is None assert isinstance(field2.type, StrawberryOptional) assert field2.type.of_type is int # TODO: move to a field test file def test_passing_custom_names_to_fields(): @strawberry.type class Query: x: str | None = strawberry.field(name="name") y: int | None = strawberry.field(name="age") definition = Query.__strawberry_definition__ assert definition.name == "Query" [field1, field2] = definition.fields assert field1.python_name == "x" assert field1.graphql_name == "name" assert isinstance(field1.type, StrawberryOptional) assert field1.type.of_type is str assert field2.python_name == "y" assert field2.graphql_name == "age" assert isinstance(field2.type, StrawberryOptional) assert field2.type.of_type is int # TODO: move to a field test file def test_passing_nothing_to_fields(): @strawberry.type class Query: name: str | None = strawberry.field() age: int | None = strawberry.field() definition = Query.__strawberry_definition__ assert definition.name == "Query" [field1, field2] = definition.fields assert field1.python_name == "name" assert field1.graphql_name is None assert isinstance(field1.type, StrawberryOptional) assert field1.type.of_type is str assert field2.python_name == "age" assert field2.graphql_name is None assert isinstance(field2.type, StrawberryOptional) assert field2.type.of_type is int # TODO: move to a resolver test file def test_resolver_fields(): @strawberry.type class Query: @strawberry.field def name(self) -> str | None: return "Name" definition = Query.__strawberry_definition__ assert definition.name == "Query" [field] = definition.fields assert field.python_name == "name" assert field.graphql_name is None assert isinstance(field.type, StrawberryOptional) assert field.type.of_type is str # TODO: move to a resolver test file def test_resolver_fields_arguments(): @strawberry.type class Query: @strawberry.field def name(self, argument: str | None) -> str | None: return "Name" definition = Query.__strawberry_definition__ assert definition.name == "Query" [field] = definition.fields assert field.python_name == "name" assert field.graphql_name is None assert isinstance(field.type, StrawberryOptional) assert field.type.of_type is str [argument] = field.arguments assert argument.python_name == "argument" assert argument.graphql_name is None assert isinstance(argument.type, StrawberryOptional) assert argument.type.of_type is str strawberry-graphql-0.287.0/tests/types/resolving/test_string_annotations.py000066400000000000000000000114231511033167500274050ustar00rootroot00000000000000from typing import Optional, TypeVar import strawberry from strawberry.annotation import StrawberryAnnotation from strawberry.types.base import ( StrawberryList, StrawberryOptional, StrawberryTypeVar, ) def test_basic_string(): annotation = StrawberryAnnotation("str") resolved = annotation.resolve() assert resolved is str def test_list_of_string(): annotation = StrawberryAnnotation(list["int"]) resolved = annotation.resolve() assert isinstance(resolved, StrawberryList) assert resolved.of_type is int assert resolved == StrawberryList(of_type=int) assert resolved == list[int] def test_list_of_string_of_type(): @strawberry.type class NameGoesHere: foo: bool annotation = StrawberryAnnotation(list["NameGoesHere"], namespace=locals()) resolved = annotation.resolve() assert isinstance(resolved, StrawberryList) assert resolved.of_type is NameGoesHere assert resolved == StrawberryList(of_type=NameGoesHere) assert resolved == list[NameGoesHere] def test_optional_of_string(): annotation = StrawberryAnnotation(Optional["bool"]) resolved = annotation.resolve() assert isinstance(resolved, StrawberryOptional) assert resolved.of_type is bool assert resolved == StrawberryOptional(of_type=bool) assert resolved == Optional[bool] def test_string_of_object(): @strawberry.type class StrType: thing: int annotation = StrawberryAnnotation("StrType", namespace=locals()) resolved = annotation.resolve() assert resolved is StrType def test_string_of_type_var(): T = TypeVar("T") annotation = StrawberryAnnotation("T", namespace=locals()) resolved = annotation.resolve() assert isinstance(resolved, StrawberryTypeVar) assert resolved.type_var is T assert resolved == T def test_string_of_list(): namespace = {**locals(), **globals()} annotation = StrawberryAnnotation("list[float]", namespace=namespace) resolved = annotation.resolve() assert isinstance(resolved, StrawberryList) assert resolved.of_type is float assert resolved == StrawberryList(of_type=float) assert resolved == list[float] def test_string_of_list_of_type(): @strawberry.type class BlahBlah: foo: bool namespace = {**locals(), **globals()} annotation = StrawberryAnnotation("list[BlahBlah]", namespace=namespace) resolved = annotation.resolve() assert isinstance(resolved, StrawberryList) assert resolved.of_type is BlahBlah assert resolved == StrawberryList(of_type=BlahBlah) assert resolved == list[BlahBlah] def test_string_of_optional(): namespace = {**locals(), **globals()} annotation = StrawberryAnnotation("Optional[int]", namespace=namespace) resolved = annotation.resolve() assert isinstance(resolved, StrawberryOptional) assert resolved.of_type is int assert resolved == StrawberryOptional(of_type=int) assert resolved == Optional[int] # TODO: Move to object tests to test namespace logic def test_basic_types(): @strawberry.type class Query: name: "str" age: "int" definition = Query.__strawberry_definition__ assert definition.name == "Query" [field1, field2] = definition.fields assert field1.python_name == "name" assert field1.type is str assert field2.python_name == "age" assert field2.type is int # TODO: Move to object tests to test namespace logic def test_optional(): @strawberry.type class Query: name: "str | None" age: "int | None" definition = Query.__strawberry_definition__ assert definition.name == "Query" [field1, field2] = definition.fields assert field1.python_name == "name" assert isinstance(field1.type, StrawberryOptional) assert field1.type.of_type is str assert field2.python_name == "age" assert isinstance(field2.type, StrawberryOptional) assert field2.type.of_type is int # TODO: Move to object tests to test namespace logic def test_basic_list(): @strawberry.type class Query: names: "list[str]" definition = Query.__strawberry_definition__ assert definition.name == "Query" [field] = definition.fields assert field.python_name == "names" assert isinstance(field.type, StrawberryList) assert field.type.of_type is str # TODO: Move to object tests to test namespace logic def test_list_of_types(): global User @strawberry.type class User: name: str @strawberry.type class Query: users: "list[User]" definition = Query.__strawberry_definition__ assert definition.name == "Query" [field] = definition.fields assert field.python_name == "users" assert isinstance(field.type, StrawberryList) assert field.type.of_type is User del User strawberry-graphql-0.287.0/tests/types/resolving/test_union_pipe.py000066400000000000000000000050141511033167500256260ustar00rootroot00000000000000import typing from typing import Union import pytest import strawberry from strawberry.annotation import StrawberryAnnotation from strawberry.exceptions.invalid_union_type import InvalidUnionTypeError from strawberry.schema.types.base_scalars import Date, DateTime from strawberry.types.base import StrawberryOptional from strawberry.types.union import StrawberryUnion def test_union_short_syntax(): @strawberry.type class User: name: str @strawberry.type class Error: name: str annotation = StrawberryAnnotation(User | Error) resolved = annotation.resolve() assert isinstance(resolved, StrawberryUnion) assert resolved.types == (User, Error) assert resolved == StrawberryUnion( name=None, type_annotations=(StrawberryAnnotation(User), StrawberryAnnotation(Error)), ) assert resolved == Union[User, Error] def test_union_none(): @strawberry.type class User: name: str annotation = StrawberryAnnotation(User | None) resolved = annotation.resolve() assert isinstance(resolved, StrawberryOptional) assert resolved.of_type == User assert resolved == StrawberryOptional( of_type=User, ) assert resolved == Union[User, None] def test_strawberry_union_and_none(): @strawberry.type class User: name: str @strawberry.type class Error: name: str UserOrError = typing.Annotated[User | Error, strawberry.union(name="UserOrError")] annotation = StrawberryAnnotation(UserOrError | None) resolved = annotation.resolve() assert isinstance(resolved, StrawberryOptional) assert resolved == StrawberryOptional( of_type=StrawberryUnion( name="UserOrError", type_annotations=(StrawberryAnnotation(User), StrawberryAnnotation(Error)), ) ) @pytest.mark.raises_strawberry_exception( InvalidUnionTypeError, match="Type `int` cannot be used in a GraphQL Union", ) def test_raises_error_when_piping_with_scalar(): @strawberry.type class User: name: str @strawberry.type class Error: name: str UserOrError = typing.Annotated[User | Error, strawberry.union("UserOrError")] @strawberry.type class Query: user: UserOrError | int strawberry.Schema(query=Query) @pytest.mark.raises_strawberry_exception( InvalidUnionTypeError, match="Type `date` cannot be used in a GraphQL Union", ) def test_raises_error_when_piping_with_custom_scalar(): StrawberryAnnotation(Date | DateTime) strawberry-graphql-0.287.0/tests/types/resolving/test_unions.py000066400000000000000000000064511511033167500250020ustar00rootroot00000000000000from typing import Annotated, Generic, TypeVar, Union import pytest import strawberry from strawberry.annotation import StrawberryAnnotation from strawberry.exceptions import InvalidUnionTypeError from strawberry.types.base import get_object_definition from strawberry.types.union import StrawberryUnion, union def test_python_union(): @strawberry.type class User: name: str @strawberry.type class Error: name: str annotation = StrawberryAnnotation(Union[User, Error]) resolved = annotation.resolve() assert isinstance(resolved, StrawberryUnion) assert resolved.types == (User, Error) assert resolved == StrawberryUnion( name=None, type_annotations=(StrawberryAnnotation(User), StrawberryAnnotation(Error)), ) assert resolved == Union[User, Error] def test_python_union_short_syntax(): @strawberry.type class User: name: str @strawberry.type class Error: name: str annotation = StrawberryAnnotation(User | Error) resolved = annotation.resolve() assert isinstance(resolved, StrawberryUnion) assert resolved.types == (User, Error) assert resolved == StrawberryUnion( name=None, type_annotations=(StrawberryAnnotation(User), StrawberryAnnotation(Error)), ) assert resolved == Union[User, Error] def test_strawberry_union(): @strawberry.type class User: name: str @strawberry.type class Error: name: str cool_union = Annotated[User | Error, union(name="CoolUnion")] annotation = StrawberryAnnotation(cool_union) resolved = annotation.resolve() assert isinstance(resolved, StrawberryUnion) assert resolved.types == (User, Error) assert resolved == StrawberryUnion( name="CoolUnion", type_annotations=(StrawberryAnnotation(User), StrawberryAnnotation(Error)), ) def test_union_with_generic(): T = TypeVar("T") @strawberry.type class Error: message: str @strawberry.type class Edge(Generic[T]): node: T Result = Annotated[Error | Edge[str], strawberry.union("Result")] strawberry_union = StrawberryAnnotation(Result).resolve() assert isinstance(strawberry_union, StrawberryUnion) assert strawberry_union.graphql_name == "Result" assert strawberry_union.types[0] == Error assert ( get_object_definition(strawberry_union.types[1], strict=True).is_graphql_generic is False ) @pytest.mark.raises_strawberry_exception( InvalidUnionTypeError, match="Type `int` cannot be used in a GraphQL Union" ) def test_error_with_scalar_types(): Something = Annotated[ int | str | float | bool, strawberry.union("Something"), ] @strawberry.type class Query: something: Something strawberry.Schema(query=Query) @pytest.mark.raises_strawberry_exception( InvalidUnionTypeError, match="Type `int` cannot be used in a GraphQL Union" ) def test_error_with_scalar_types_pipe(): # TODO: using Something as the name of the union makes the source finder # use the union type defined above Something2 = Annotated[ int | str | float | bool, strawberry.union("Something2"), ] @strawberry.type class Query: something: Something2 strawberry.Schema(query=Query) strawberry-graphql-0.287.0/tests/types/resolving/test_unions_deprecated.py000066400000000000000000000064631511033167500271650ustar00rootroot00000000000000from dataclasses import dataclass from typing import Generic, NewType, TypeVar, Union import pytest import strawberry from strawberry.annotation import StrawberryAnnotation from strawberry.exceptions import InvalidUnionTypeError from strawberry.types.union import StrawberryUnion, union pytestmark = pytest.mark.filterwarnings( "ignore:Passing types to `strawberry.union` is deprecated." ) def test_strawberry_union(): @strawberry.type class User: name: str @strawberry.type class Error: name: str cool_union = union(name="CoolUnion", types=(User, Error)) annotation = StrawberryAnnotation(cool_union) resolved = annotation.resolve() assert isinstance(resolved, StrawberryUnion) assert resolved.types == (User, Error) assert resolved == StrawberryUnion( name="CoolUnion", type_annotations=(StrawberryAnnotation(User), StrawberryAnnotation(Error)), ) assert resolved != Union[User, Error] # Name will be different def test_named_union_with_deprecated_api_using_types_parameter(): @strawberry.type class A: a: int @strawberry.type class B: b: int Result = strawberry.union("Result", types=(A, B)) strawberry_union = Result assert isinstance(strawberry_union, StrawberryUnion) assert strawberry_union.graphql_name == "Result" assert strawberry_union.types == (A, B) def test_union_with_generic_with_deprecated_api_using_types_parameter(): T = TypeVar("T") @strawberry.type class Error: message: str @strawberry.type class Edge(Generic[T]): node: T Result = strawberry.union("Result", types=(Error, Edge[str])) strawberry_union = Result assert isinstance(strawberry_union, StrawberryUnion) assert strawberry_union.graphql_name == "Result" assert strawberry_union.types[0] == Error assert ( strawberry_union.types[1].__strawberry_definition__.is_graphql_generic is False ) def test_cannot_use_union_directly(): @strawberry.type class A: a: int @strawberry.type class B: b: int Result = strawberry.union("Result", (A, B)) with pytest.raises(ValueError, match=r"Cannot use union type directly"): Result() # type: ignore def test_error_with_empty_type_list(): with pytest.raises(TypeError, match="No types passed to `union`"): strawberry.union("Result", ()) @pytest.mark.raises_strawberry_exception( InvalidUnionTypeError, match="Type `int` cannot be used in a GraphQL Union" ) def test_error_with_scalar_types(): strawberry.union( "Result", ( int, str, float, bool, ), ) @pytest.mark.raises_strawberry_exception( InvalidUnionTypeError, match="Type `CustomScalar` cannot be used in a GraphQL Union" ) def test_error_with_custom_scalar_types(): CustomScalar = strawberry.scalar( NewType("CustomScalar", str), serialize=lambda v: str(v), parse_value=lambda v: str(v), ) strawberry.union("Result", (CustomScalar,)) @pytest.mark.raises_strawberry_exception( InvalidUnionTypeError, match="Type `A` cannot be used in a GraphQL Union" ) def test_error_with_non_strawberry_type(): @dataclass class A: a: int strawberry.union("Result", (A,)) strawberry-graphql-0.287.0/tests/types/test_annotation.py000066400000000000000000000031451511033167500236260ustar00rootroot00000000000000import itertools from enum import Enum from typing import Optional, TypeVar import pytest import strawberry from strawberry.annotation import StrawberryAnnotation from strawberry.types.unset import UnsetType class Bleh: pass @strawberry.enum class NumaNuma(Enum): MA = "ma" I = "i" # noqa: E741 A = "a" HI = "hi" T = TypeVar("T") types = [ int, str, None, Optional[str], UnsetType, "int", T, Bleh, NumaNuma, ] @pytest.mark.parametrize( ("type1", "type2"), itertools.combinations_with_replacement(types, 2) ) def test_annotation_hash(type1: object | str, type2: object | str): annotation1 = StrawberryAnnotation(type1) annotation2 = StrawberryAnnotation(type2) assert ( hash(annotation1) == hash(annotation2) if annotation1 == annotation2 else hash(annotation1) != hash(annotation2) ), "Equal type must imply equal hash" def test_eq_on_other_type(): class Foo: # noqa: PLW1641 def __eq__(self, other): # Anything that is a strawberry annotation is equal to Foo return isinstance(other, StrawberryAnnotation) assert Foo() != object() assert object() != Foo() assert Foo() != 123 != Foo() assert Foo() != 123 assert Foo() == StrawberryAnnotation(int) assert StrawberryAnnotation(int) == Foo() def test_eq_on_non_annotation(): assert StrawberryAnnotation(int) is not int assert StrawberryAnnotation(int) != 123 def test_set_anntation(): annotation = StrawberryAnnotation(int) annotation.annotation = str assert annotation.annotation is str strawberry-graphql-0.287.0/tests/types/test_argument_types.py000066400000000000000000000066321511033167500245260ustar00rootroot00000000000000import warnings from enum import Enum from typing import Optional, TypeVar import pytest import strawberry from strawberry.types.info import Info def test_enum(): @strawberry.enum class Locale(Enum): UNITED_STATES = "en_US" UK = "en_UK" AUSTRALIA = "en_AU" @strawberry.mutation def set_locale(locale: Locale) -> bool: _ = locale return True argument = set_locale.arguments[0] # TODO: Remove reference to .__strawberry_definition__ with StrawberryEnumDefinition assert argument.type is Locale.__strawberry_definition__ def test_forward_reference(): global SearchInput @strawberry.field def search(search_input: "SearchInput") -> bool: _ = search_input return True @strawberry.input class SearchInput: query: str argument = search.arguments[0] assert argument.type is SearchInput del SearchInput def test_list(): @strawberry.field def get_longest_word(words: list[str]) -> str: _ = words return "I cheated" argument = get_longest_word.arguments[0] assert argument.type == list[str] def test_literal(): @strawberry.field def get_name(id_: int) -> str: _ = id_ return "Lord Buckethead" argument = get_name.arguments[0] assert argument.type is int def test_object(): @strawberry.type class PersonInput: proper_noun: bool @strawberry.field def get_id(person_input: PersonInput) -> int: _ = person_input return 0 argument = get_id.arguments[0] assert argument.type is PersonInput def test_optional(): @strawberry.field def set_age(age: int | None) -> bool: _ = age return True argument = set_age.arguments[0] assert argument.type == Optional[int] def test_type_var(): T = TypeVar("T") @strawberry.field def set_value(value: T) -> bool: _ = value return True argument = set_value.arguments[0] assert argument.type == T ContextType = TypeVar("ContextType") RootValueType = TypeVar("RootValueType") class CustomInfo(Info[ContextType, RootValueType]): """Subclassed Info type used to test dependency injection.""" @pytest.mark.parametrize( "annotation", [CustomInfo, CustomInfo[None, None], Info, Info[None, None]], ) def test_custom_info(annotation): """Test to ensure that subclassed Info does not raise warning.""" with warnings.catch_warnings(): warnings.filterwarnings("error") def get_info(info) -> bool: _ = info return True get_info.__annotations__["info"] = annotation get_info_field = strawberry.field(get_info) assert not get_info_field.arguments # Should have no arguments matched info_parameter = get_info_field.base_resolver.info_parameter assert info_parameter is not None assert info_parameter.name == "info" def test_custom_info_negative(): """Test to ensure deprecation warning is emitted.""" with pytest.warns( DeprecationWarning, match=r"Argument name-based matching of 'info'" ): @strawberry.field def get_info(info) -> bool: _ = info return True assert not get_info.arguments # Should have no arguments matched info_parameter = get_info.base_resolver.info_parameter assert info_parameter is not None assert info_parameter.name == "info" strawberry-graphql-0.287.0/tests/types/test_cast.py000066400000000000000000000011671511033167500224100ustar00rootroot00000000000000import strawberry from strawberry.types.cast import get_strawberry_type_cast def test_cast(): @strawberry.type class SomeType: ... class OtherType: ... obj = OtherType assert get_strawberry_type_cast(obj) is None cast_obj = strawberry.cast(SomeType, obj) assert cast_obj is obj assert get_strawberry_type_cast(cast_obj) is SomeType def test_cast_none_obj(): @strawberry.type class SomeType: ... obj = None assert get_strawberry_type_cast(obj) is None cast_obj = strawberry.cast(SomeType, obj) assert cast_obj is None assert get_strawberry_type_cast(obj) is None strawberry-graphql-0.287.0/tests/types/test_convert_to_dictionary.py000066400000000000000000000025031511033167500260600ustar00rootroot00000000000000from enum import Enum import strawberry from strawberry import asdict def test_convert_simple_type_to_dictionary(): @strawberry.type class People: name: str age: int lorem = People(name="Alex", age=30) assert asdict(lorem) == { "name": "Alex", "age": 30, } def test_convert_complex_type_to_dictionary(): @strawberry.enum class Count(Enum): TWO = "two" FOUR = "four" @strawberry.type class Animal: legs: Count @strawberry.type class People: name: str animals: list[Animal] lorem = People( name="Kevin", animals=[Animal(legs=Count.TWO), Animal(legs=Count.FOUR)] ) assert asdict(lorem) == { "name": "Kevin", "animals": [ {"legs": Count.TWO}, {"legs": Count.FOUR}, ], } def test_convert_input_to_dictionary(): @strawberry.input class QnaInput: title: str description: str tags: list[str] | None = strawberry.field(default=None) title = "Where is the capital of United Kingdom?" description = "London is the capital of United Kingdom." qna = QnaInput(title=title, description=description) assert asdict(qna) == { "title": title, "description": description, "tags": None, } strawberry-graphql-0.287.0/tests/types/test_deferred_annotations.py000066400000000000000000000013401511033167500256440ustar00rootroot00000000000000from sys import modules from types import ModuleType import strawberry deferred_module_source = """ from __future__ import annotations import strawberry @strawberry.type class User: username: str email: str @strawberry.interface class UserContent: created_by: User """ def test_deferred_other_module(): mod = ModuleType("tests.deferred_module") modules[mod.__name__] = mod try: exec(deferred_module_source, mod.__dict__) # noqa: S102 @strawberry.type class Post(mod.UserContent): title: str body: str definition = Post.__strawberry_definition__ assert definition.fields[0].type == mod.User finally: del modules[mod.__name__] strawberry-graphql-0.287.0/tests/types/test_execution.py000066400000000000000000000114241511033167500234560ustar00rootroot00000000000000import strawberry from strawberry.extensions import SchemaExtension @strawberry.type class Query: @strawberry.field def ping(self) -> str: return "pong" def test_execution_context_operation_name_and_type(): operation_name = None operation_type = None class MyExtension(SchemaExtension): def on_operation(self): yield nonlocal operation_name nonlocal operation_type execution_context = self.execution_context operation_name = execution_context.operation_name operation_type = execution_context.operation_type.value schema = strawberry.Schema(Query, extensions=[MyExtension]) result = schema.execute_sync("{ ping }") assert not result.errors assert operation_name is None assert operation_type == "query" # Try again with an operation_name result = schema.execute_sync("query MyOperation { ping }") assert not result.errors assert operation_name == "MyOperation" assert operation_type == "query" # Try again with an operation_name override result = schema.execute_sync( """ query MyOperation { ping } query MyOperation2 { ping } """, operation_name="MyOperation2", ) assert not result.errors assert operation_name == "MyOperation2" assert operation_type == "query" def test_execution_context_operation_type_mutation(): operation_name = None operation_type = None class MyExtension(SchemaExtension): def on_operation(self): yield nonlocal operation_name nonlocal operation_type execution_context = self.execution_context operation_name = execution_context.operation_name operation_type = execution_context.operation_type.value @strawberry.type class Mutation: @strawberry.mutation def my_mutation(self) -> str: return "hi" schema = strawberry.Schema(Query, mutation=Mutation, extensions=[MyExtension]) result = schema.execute_sync("mutation { myMutation }") assert not result.errors assert operation_name is None assert operation_type == "mutation" # Try again with an operation_name result = schema.execute_sync("mutation MyMutation { myMutation }") assert not result.errors assert operation_name == "MyMutation" assert operation_type == "mutation" # Try again with an operation_name override result = schema.execute_sync( """ mutation MyMutation { myMutation } mutation MyMutation2 { myMutation } """, operation_name="MyMutation2", ) assert not result.errors assert operation_name == "MyMutation2" assert operation_type == "mutation" def test_execution_context_operation_name_and_type_with_fragments(): operation_name = None operation_type = None class MyExtension(SchemaExtension): def on_operation(self): yield nonlocal operation_name nonlocal operation_type execution_context = self.execution_context operation_name = execution_context.operation_name operation_type = execution_context.operation_type.value schema = strawberry.Schema(Query, extensions=[MyExtension]) result = schema.execute_sync( """ fragment MyFragment on Query { ping } query MyOperation { ping ...MyFragment } """ ) assert not result.errors assert operation_name == "MyOperation" assert operation_type == "query" def test_error_when_accessing_operation_type_before_parsing(): class MyExtension(SchemaExtension): def on_operation(self): execution_context = self.execution_context # This should raise a RuntimeError execution_context.operation_type schema = strawberry.Schema(Query, extensions=[MyExtension]) result = schema.execute_sync("mutation { myMutation }") assert len(result.errors) == 1 assert isinstance(result.errors[0].original_error, RuntimeError) assert result.errors[0].message == "No GraphQL document available" def test_error_when_accessing_operation_type_with_invalid_operation_name(): class MyExtension(SchemaExtension): def on_parse(self): yield execution_context = self.execution_context # This should raise a RuntimeError execution_context.operation_type schema = strawberry.Schema(Query, extensions=[MyExtension]) result = schema.execute_sync("query { ping }", operation_name="MyQuery") assert len(result.errors) == 1 assert isinstance(result.errors[0].original_error, RuntimeError) assert result.errors[0].message == "Can't get GraphQL operation type" strawberry-graphql-0.287.0/tests/types/test_field_types.py000066400000000000000000000041661511033167500237670ustar00rootroot00000000000000from enum import Enum from typing import Optional, TypeVar import strawberry from strawberry.annotation import StrawberryAnnotation from strawberry.types.field import StrawberryField from strawberry.types.union import StrawberryUnion def test_enum(): @strawberry.enum class Egnum(Enum): a = "A" b = "B" annotation = StrawberryAnnotation(Egnum) field = StrawberryField(type_annotation=annotation) # TODO: Remove reference to .__strawberry_definition__ with StrawberryEnumDefinition assert field.type is Egnum.__strawberry_definition__ def test_forward_reference(): global RefForward annotation = StrawberryAnnotation("RefForward", namespace=globals()) field = StrawberryField(type_annotation=annotation) @strawberry.type class RefForward: ref: int assert field.type is RefForward del RefForward def test_list(): annotation = StrawberryAnnotation(list[int]) field = StrawberryField(type_annotation=annotation) assert field.type == list[int] def test_literal(): annotation = StrawberryAnnotation(bool) field = StrawberryField(type_annotation=annotation) assert field.type is bool def test_object(): @strawberry.type class TypeyType: value: str annotation = StrawberryAnnotation(TypeyType) field = StrawberryField(type_annotation=annotation) assert field.type is TypeyType def test_optional(): annotation = StrawberryAnnotation(Optional[float]) field = StrawberryField(type_annotation=annotation) assert field.type == Optional[float] def test_type_var(): T = TypeVar("T") annotation = StrawberryAnnotation(T) field = StrawberryField(type_annotation=annotation) assert field.type == T def test_union(): @strawberry.type class Un: fi: int @strawberry.type class Ion: eld: float union = StrawberryUnion( name="UnionName", type_annotations=(StrawberryAnnotation(Un), StrawberryAnnotation(Ion)), ) annotation = StrawberryAnnotation(union) field = StrawberryField(type_annotation=annotation) assert field.type is union strawberry-graphql-0.287.0/tests/types/test_lazy_types.py000066400000000000000000000153111511033167500236550ustar00rootroot00000000000000# type: ignore import enum import textwrap from typing import Annotated, Generic, TypeAlias, TypeVar import strawberry from strawberry.annotation import StrawberryAnnotation from strawberry.types.base import get_object_definition from strawberry.types.field import StrawberryField from strawberry.types.fields.resolver import StrawberryResolver from strawberry.types.lazy_type import LazyType from strawberry.types.union import StrawberryUnion, union T = TypeVar("T") # This type is in the same file but should adequately test the logic. @strawberry.type class LaziestType: something: bool @strawberry.type class LazyGenericType(Generic[T]): something: T LazyTypeAlias: TypeAlias = LazyGenericType[int] @strawberry.enum class LazyEnum(enum.Enum): BREAD = "BREAD" def test_lazy_type(): LazierType = LazyType("LaziestType", "tests.types.test_lazy_types") annotation = StrawberryAnnotation(LazierType) resolved = annotation.resolve() # Currently StrawberryAnnotation(LazyType).resolve() returns the unresolved # LazyType. We may want to find a way to directly return the referenced object # without a second resolving step. assert isinstance(resolved, LazyType) assert resolved is LazierType assert resolved.resolve_type() is LaziestType def test_lazy_type_alias(): LazierType = LazyType("LazyTypeAlias", "tests.types.test_lazy_types") annotation = StrawberryAnnotation(LazierType) resolved = annotation.resolve() # Currently StrawberryAnnotation(LazyType).resolve() returns the unresolved # LazyType. We may want to find a way to directly return the referenced object # without a second resolving step. assert isinstance(resolved, LazyType) resolved_type = resolved.resolve_type() assert resolved_type.__origin__ is LazyGenericType assert resolved_type.__args__ == (int,) def test_lazy_type_function(): LethargicType = Annotated[ "LaziestType", strawberry.lazy("tests.types.test_lazy_types") ] annotation = StrawberryAnnotation(LethargicType) resolved = annotation.resolve() assert isinstance(resolved, LazyType) assert resolved.resolve_type() is LaziestType def test_lazy_type_enum(): LazierType = LazyType("LazyEnum", "tests.types.test_lazy_types") annotation = StrawberryAnnotation(LazierType) resolved = annotation.resolve() # Currently StrawberryAnnotation(LazyType).resolve() returns the unresolved # LazyType. We may want to find a way to directly return the referenced object # without a second resolving step. assert isinstance(resolved, LazyType) assert resolved is LazierType assert resolved.resolve_type() is LazyEnum def test_lazy_type_argument(): LazierType = LazyType("LaziestType", "tests.types.test_lazy_types") @strawberry.mutation def slack_off(emotion: LazierType) -> bool: _ = emotion return True argument = slack_off.arguments[0] assert isinstance(argument.type, LazyType) assert argument.type is LazierType assert argument.type.resolve_type() is LaziestType def test_lazy_type_field(): LazierType = LazyType("LaziestType", "tests.types.test_lazy_types") annotation = StrawberryAnnotation(LazierType) field = StrawberryField(type_annotation=annotation) assert isinstance(field.type, LazyType) assert field.type is LazierType assert field.type.resolve_type() is LaziestType def test_lazy_type_generic(): T = TypeVar("T") @strawberry.type class GenericType(Generic[T]): item: T LazierType = LazyType("LaziestType", "tests.types.test_lazy_types") ResolvedType = GenericType[LazierType] annotation = StrawberryAnnotation(ResolvedType) resolved = annotation.resolve() definition = get_object_definition(resolved) assert definition items_field: StrawberryField = definition.fields[0] assert items_field.type is LazierType assert items_field.type.resolve_type() is LaziestType def test_lazy_type_object(): LazierType = LazyType("LaziestType", "tests.types.test_lazy_types") @strawberry.type class WaterParkFeature: river: LazierType field: StrawberryField = WaterParkFeature.__strawberry_definition__.fields[0] assert isinstance(field.type, LazyType) assert field.type is LazierType assert field.type.resolve_type() is LaziestType def test_lazy_type_resolver(): LazierType = LazyType("LaziestType", "tests.types.test_lazy_types") def slaking_pokemon() -> LazierType: raise NotImplementedError resolver = StrawberryResolver(slaking_pokemon) assert isinstance(resolver.type, LazyType) assert resolver.type is LazierType assert resolver.type.resolve_type() is LaziestType def test_lazy_type_in_union(): ActiveType = LazyType("LaziestType", "tests.types.test_lazy_types") ActiveEnum = LazyType("LazyEnum", "tests.types.test_lazy_types") something = Annotated[ActiveType | ActiveEnum, union(name="CoolUnion")] annotation = StrawberryAnnotation(something) resolved = annotation.resolve() assert isinstance(resolved, StrawberryUnion) [type1, type2] = resolved.types assert type1 is ActiveType assert type2 is ActiveEnum assert type1.resolve_type() is LaziestType assert type2.resolve_type() is LazyEnum def test_lazy_function_in_union(): ActiveType = Annotated[ "LaziestType", strawberry.lazy("tests.types.test_lazy_types") ] ActiveEnum = Annotated["LazyEnum", strawberry.lazy("tests.types.test_lazy_types")] something = Annotated[ActiveType | ActiveEnum, union(name="CoolUnion")] annotation = StrawberryAnnotation(something) resolved = annotation.resolve() assert isinstance(resolved, StrawberryUnion) [type1, type2] = resolved.types assert type1.resolve_type() is LaziestType assert type2.resolve_type() is LazyEnum def test_optional_lazy_type_using_or_operator(): from tests.schema.test_lazy.type_a import TypeA @strawberry.type class SomeType: foo: Annotated[TypeA, strawberry.lazy("tests.schema.test_lazy.type_a")] | None @strawberry.type class AnotherType: foo: TypeA | None = None @strawberry.type class Query: some_type: SomeType another_type: AnotherType schema = strawberry.Schema(query=Query) expected = """\ type AnotherType { foo: TypeA } type Query { someType: SomeType! anotherType: AnotherType! } type SomeType { foo: TypeA } type TypeA { listOfB: [TypeB!] typeB: TypeB! } type TypeB { typeA: TypeA! typeAList: [TypeA!]! typeCList: [TypeC!]! } type TypeC { name: String! } """ assert str(schema).strip() == textwrap.dedent(expected).strip() strawberry-graphql-0.287.0/tests/types/test_lazy_types_future_annotations.py000066400000000000000000000024451511033167500276700ustar00rootroot00000000000000from __future__ import annotations import textwrap from typing import Annotated import strawberry def test_optional_lazy_type_using_or_operator(): from tests.schema.test_lazy.type_a import TypeA global SomeType, AnotherType try: @strawberry.type class SomeType: foo: ( Annotated[TypeA, strawberry.lazy("tests.schema.test_lazy.type_a")] | None ) @strawberry.type class AnotherType: foo: TypeA | None = None @strawberry.type class Query: some_type: SomeType another_type: AnotherType schema = strawberry.Schema(query=Query) expected = """\ type AnotherType { foo: TypeA } type Query { someType: SomeType! anotherType: AnotherType! } type SomeType { foo: TypeA } type TypeA { listOfB: [TypeB!] typeB: TypeB! } type TypeB { typeA: TypeA! typeAList: [TypeA!]! typeCList: [TypeC!]! } type TypeC { name: String! } """ assert str(schema).strip() == textwrap.dedent(expected).strip() finally: del SomeType, AnotherType strawberry-graphql-0.287.0/tests/types/test_object_types.py000066400000000000000000000117121511033167500241450ustar00rootroot00000000000000# type: ignore import dataclasses import re from enum import Enum from typing import Annotated, Optional, TypeVar import pytest import strawberry from strawberry.types.base import get_object_definition, has_object_definition from strawberry.types.field import StrawberryField def test_enum(): @strawberry.enum class Count(Enum): TWO = "two" FOUR = "four" @strawberry.type class Animal: legs: Count field: StrawberryField = get_object_definition(Animal).fields[0] # TODO: Remove reference to .__strawberry_definition__ with StrawberryEnumDefinition assert field.type is Count.__strawberry_definition__ def test_forward_reference(): global FromTheFuture @strawberry.type class TimeTraveler: origin: "FromTheFuture" @strawberry.type class FromTheFuture: year: int field: StrawberryField = get_object_definition(TimeTraveler).fields[0] assert field.type is FromTheFuture del FromTheFuture def test_list(): @strawberry.type class Santa: making_a: list[str] field: StrawberryField = get_object_definition(Santa).fields[0] assert field.type == list[str] def test_literal(): @strawberry.type class Fabric: thread_type: str field: StrawberryField = get_object_definition(Fabric).fields[0] assert field.type is str def test_object(): @strawberry.type class Object: proper_noun: bool @strawberry.type class TransitiveVerb: subject: Object field: StrawberryField = get_object_definition(TransitiveVerb).fields[0] assert field.type is Object def test_optional(): @strawberry.type class HasChoices: decision: bool | None field: StrawberryField = get_object_definition(HasChoices).fields[0] assert field.type == Optional[bool] def test_type_var(): T = TypeVar("T") @strawberry.type class Gossip: spill_the: T field: StrawberryField = get_object_definition(Gossip).fields[0] assert field.type == T def test_union(): @strawberry.type class Europe: name: str @strawberry.type class UK: name: str EU = Annotated[Europe | UK, strawberry.union("EU")] @strawberry.type class WishfulThinking: desire: EU field: StrawberryField = get_object_definition(WishfulThinking).fields[0] assert field.type == EU def test_fields_with_defaults(): @strawberry.type class Country: name: str = "United Kingdom" currency_code: str country = Country(currency_code="GBP") assert country.name == "United Kingdom" assert country.currency_code == "GBP" country = Country(name="United States of America", currency_code="USD") assert country.name == "United States of America" assert country.currency_code == "USD" def test_fields_with_defaults_inheritance(): @strawberry.interface class A: text: str delay: int | None = None @strawberry.type class B(A): attachments: list[A] | None = None @strawberry.type class C(A): fields: list[B] c_inst = C( text="some text", fields=[B(text="more text")], ) assert dataclasses.asdict(c_inst) == { "text": "some text", "delay": None, "fields": [ { "text": "more text", "attachments": None, "delay": None, } ], } def test_positional_args_not_allowed(): @strawberry.type class Thing: name: str with pytest.raises( TypeError, match=re.escape("__init__() takes 1 positional argument but 2 were given"), ): Thing("something") def test_object_preserves_annotations(): @strawberry.type class Object: a: bool b: Annotated[str, "something"] c: bool = strawberry.field(graphql_type=int) d: Annotated[str, "something"] = strawberry.field(graphql_type=int) assert Object.__annotations__ == { "a": bool, "b": Annotated[str, "something"], "c": bool, "d": Annotated[str, "something"], } def test_has_object_definition_returns_true_for_object_type(): @strawberry.type class Palette: name: str assert has_object_definition(Palette) is True def test_has_object_definition_returns_false_for_enum(): @strawberry.enum class Color(Enum): RED = "red" GREEN = "green" assert has_object_definition(Color) is False def test_has_object_definition_returns_true_for_interface(): @strawberry.interface class Node: id: str assert has_object_definition(Node) is True def test_has_object_definition_returns_true_for_input(): @strawberry.input class CreateUserInput: name: str assert has_object_definition(CreateUserInput) is True def test_has_object_definition_returns_false_for_scalar(): from strawberry.scalars import JSON assert has_object_definition(JSON) is False strawberry-graphql-0.287.0/tests/types/test_parent_type.py000066400000000000000000000036011511033167500240030ustar00rootroot00000000000000import textwrap from typing import ForwardRef import pytest import strawberry from strawberry.parent import resolve_parent_forward_arg def test_parent_type(): global User try: def get_full_name(user: strawberry.Parent["User"]) -> str: return f"{user.first_name} {user.last_name}" @strawberry.type class User: first_name: str last_name: str full_name: str = strawberry.field(resolver=get_full_name) @strawberry.type class Query: @strawberry.field def user(self) -> User: return User(first_name="John", last_name="Doe") # noqa: F821 schema = strawberry.Schema(query=Query) expected = """\ type Query { user: User! } type User { firstName: String! lastName: String! fullName: String! } """ assert textwrap.dedent(str(schema)).strip() == textwrap.dedent(expected).strip() query = "{ user { firstName, lastName, fullName } }" result = schema.execute_sync(query) assert not result.errors assert result.data == { "user": { "firstName": "John", "lastName": "Doe", "fullName": "John Doe", } } finally: del User @pytest.mark.parametrize( ("annotation", "expected"), [ ("strawberry.Parent[str]", strawberry.Parent["str"]), ("Parent[str]", strawberry.Parent["str"]), (ForwardRef("strawberry.Parent[str]"), strawberry.Parent["str"]), (ForwardRef("Parent[str]"), strawberry.Parent["str"]), (strawberry.Parent["User"], strawberry.Parent["User"]), ], ) def test_resolve_parent_forward_arg(annotation, expected): assert resolve_parent_forward_arg(annotation) == expected strawberry-graphql-0.287.0/tests/types/test_parent_type_future_annotations.py000066400000000000000000000024771511033167500300240ustar00rootroot00000000000000from __future__ import annotations import textwrap import strawberry def test_parent_type(): global User try: def get_full_name(user: strawberry.Parent[User]) -> str: return f"{user.first_name} {user.last_name}" @strawberry.type class User: first_name: str last_name: str full_name: str = strawberry.field(resolver=get_full_name) @strawberry.type class Query: @strawberry.field def user(self) -> User: return User(first_name="John", last_name="Doe") # noqa: F821 schema = strawberry.Schema(query=Query) expected = """\ type Query { user: User! } type User { firstName: String! lastName: String! fullName: String! } """ assert textwrap.dedent(str(schema)).strip() == textwrap.dedent(expected).strip() query = "{ user { firstName, lastName, fullName } }" result = schema.execute_sync(query) assert not result.errors assert result.data == { "user": { "firstName": "John", "lastName": "Doe", "fullName": "John Doe", } } finally: del User strawberry-graphql-0.287.0/tests/types/test_resolver_types.py000066400000000000000000000045161511033167500245440ustar00rootroot00000000000000from enum import Enum from typing import Optional, TypeVar, Union from asgiref.sync import sync_to_async import strawberry from strawberry.types.fields.resolver import StrawberryResolver def test_enum(): @strawberry.enum class Language(Enum): ENGLISH = "english" ITALIAN = "italian" JAPANESE = "japanese" def get_spoken_language() -> Language: return Language.ENGLISH resolver = StrawberryResolver(get_spoken_language) # TODO: Remove reference to .__strawberry_definition__ with StrawberryEnumDefinition assert resolver.type is Language.__strawberry_definition__ def test_forward_references(): global FutureUmpire def get_sportsball_official() -> "FutureUmpire": return FutureUmpire("ref") # noqa: F821 @strawberry.type class FutureUmpire: name: str resolver = StrawberryResolver(get_sportsball_official) assert resolver.type is FutureUmpire del FutureUmpire def test_list(): def get_collection_types() -> list[str]: return ["list", "tuple", "dict", "set"] resolver = StrawberryResolver(get_collection_types) assert resolver.type == list[str] def test_literal(): def version() -> float: return 1.0 resolver = StrawberryResolver(version) assert resolver.type is float def test_object(): @strawberry.type class Polygon: edges: int faces: int def get_2d_object() -> Polygon: return Polygon(12, 6) resolver = StrawberryResolver(get_2d_object) assert resolver.type is Polygon def test_optional(): def stock_market_tool() -> str | None: ... resolver = StrawberryResolver(stock_market_tool) assert resolver.type == Optional[str] def test_type_var(): T = TypeVar("T") def caffeinated_drink() -> T: ... resolver = StrawberryResolver(caffeinated_drink) assert resolver.type == T def test_union(): @strawberry.type class Venn: foo: int @strawberry.type class Diagram: bar: float def get_overlap() -> Venn | Diagram: ... resolver = StrawberryResolver(get_overlap) assert resolver.type == Union[Venn, Diagram] def test_sync_to_async_resolver(): @sync_to_async def async_resolver() -> str: return "patrick" resolver = StrawberryResolver(async_resolver) assert resolver.is_async strawberry-graphql-0.287.0/tests/utils/000077500000000000000000000000001511033167500200345ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/utils/__init__.py000066400000000000000000000000001511033167500221330ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/utils/test_arguments_converter.py000066400000000000000000000262751511033167500255550ustar00rootroot00000000000000from enum import Enum from typing import Annotated, Optional import pytest import strawberry from strawberry.annotation import StrawberryAnnotation from strawberry.exceptions import UnsupportedTypeError from strawberry.schema.config import StrawberryConfig from strawberry.schema.types.scalar import DEFAULT_SCALAR_REGISTRY from strawberry.types.arguments import StrawberryArgument, convert_arguments from strawberry.types.lazy_type import LazyType from strawberry.types.unset import UNSET def test_simple_types(): args = {"integer": 1, "string": "abc", "float": 1.2, "bool": True} arguments = [ StrawberryArgument( graphql_name="integer", type_annotation=StrawberryAnnotation(int), python_name="integer", ), StrawberryArgument( graphql_name="string", type_annotation=StrawberryAnnotation(str), python_name="string", ), StrawberryArgument( graphql_name="float", type_annotation=StrawberryAnnotation(float), python_name="float", ), StrawberryArgument( graphql_name="bool", type_annotation=StrawberryAnnotation(bool), python_name="bool", ), ] assert convert_arguments( args, arguments, scalar_registry=DEFAULT_SCALAR_REGISTRY, config=StrawberryConfig(), ) == { "integer": 1, "string": "abc", "float": 1.2, "bool": True, } def test_list(): args = { "integerList": [1, 2], "stringList": ["abc", "cde"], "optionalIntegerList": [1, 2], "optionalStringList": ["abc", "cde", None], } arguments = [ StrawberryArgument( graphql_name="integerList", python_name="integer_list", type_annotation=StrawberryAnnotation(list[int]), ), StrawberryArgument( graphql_name="stringList", python_name="string_list", type_annotation=StrawberryAnnotation(list[str]), ), StrawberryArgument( graphql_name="optionalIntegerList", python_name="optional_integer_list", type_annotation=StrawberryAnnotation(list[int | None]), ), StrawberryArgument( graphql_name="optionalStringList", python_name="optional_string_list", type_annotation=StrawberryAnnotation(list[str | None]), ), ] assert convert_arguments( args, arguments, scalar_registry=DEFAULT_SCALAR_REGISTRY, config=StrawberryConfig(), ) == { "integer_list": [1, 2], "string_list": ["abc", "cde"], "optional_integer_list": [1, 2], "optional_string_list": ["abc", "cde", None], } @strawberry.input class LaziestType: something: bool def test_lazy(): LazierType = LazyType["LaziestType", __name__] args = { "lazyArg": {"something": True}, } arguments = [ StrawberryArgument( graphql_name="lazyArg", python_name="lazy_arg", type_annotation=StrawberryAnnotation(LazierType), ), ] assert convert_arguments( args, arguments, scalar_registry=DEFAULT_SCALAR_REGISTRY, config=StrawberryConfig(), ) == {"lazy_arg": LaziestType(something=True)} def test_annotated(): LazierType = Annotated["LaziestType", strawberry.lazy(__name__)] args = { "lazyArg": {"something": True}, } arguments = [ StrawberryArgument( graphql_name="lazyArg", python_name="lazy_arg", type_annotation=StrawberryAnnotation(LazierType), ), ] assert convert_arguments( args, arguments, scalar_registry=DEFAULT_SCALAR_REGISTRY, config=StrawberryConfig(), ) == {"lazy_arg": LaziestType(something=True)} def test_input_types(): @strawberry.input class MyInput: abc: str say_hello_to: str fun: str was: int = strawberry.field(name="having") args = { "input": {"abc": "example", "sayHelloTo": "Patrick", "having": 10, "fun": "yes"} } arguments = [ StrawberryArgument( graphql_name=None, python_name="input", type_annotation=StrawberryAnnotation(MyInput), ), ] assert convert_arguments( args, arguments, scalar_registry=DEFAULT_SCALAR_REGISTRY, config=StrawberryConfig(), ) == {"input": MyInput(abc="example", say_hello_to="Patrick", was=10, fun="yes")} def test_optional_input_types(): @strawberry.input class MyInput: abc: str args = {"input": {"abc": "example"}} arguments = [ StrawberryArgument( graphql_name=None, python_name="input", type_annotation=StrawberryAnnotation(Optional[MyInput]), ), ] assert convert_arguments( args, arguments, scalar_registry=DEFAULT_SCALAR_REGISTRY, config=StrawberryConfig(), ) == {"input": MyInput(abc="example")} def test_list_of_input_types(): @strawberry.input class MyInput: abc: str args = {"inputList": [{"abc": "example"}]} arguments = [ StrawberryArgument( graphql_name="inputList", python_name="input_list", type_annotation=StrawberryAnnotation(list[MyInput]), ), ] assert convert_arguments( args, arguments, scalar_registry=DEFAULT_SCALAR_REGISTRY, config=StrawberryConfig(), ) == {"input_list": [MyInput(abc="example")]} def test_optional_list_of_input_types(): @strawberry.input class MyInput: abc: str args = {"inputList": [{"abc": "example"}]} arguments = [ StrawberryArgument( graphql_name="inputList", python_name="input_list", type_annotation=StrawberryAnnotation(Optional[list[MyInput]]), ), ] assert convert_arguments( args, arguments, scalar_registry=DEFAULT_SCALAR_REGISTRY, config=StrawberryConfig(), ) == {"input_list": [MyInput(abc="example")]} def test_nested_input_types(): @strawberry.enum class ChangeType(Enum): MAJOR = "major" MINOR = "minor" PATCH = "patch" @strawberry.input class ReleaseInfo: change_type: ChangeType changelog: str @strawberry.enum class ReleaseFileStatus(Enum): MISSING = "missing" INVALID = "invalid" OK = "ok" @strawberry.input class AddReleaseFileCommentInput: pr_number: int status: ReleaseFileStatus release_info: ReleaseInfo | None args = { "input": { "prNumber": 12, "status": ReleaseFileStatus.OK, "releaseInfo": { "changeType": ChangeType.MAJOR, "changelog": "example", }, } } arguments = [ StrawberryArgument( graphql_name=None, python_name="input", type_annotation=StrawberryAnnotation(AddReleaseFileCommentInput), ), ] assert convert_arguments( args, arguments, scalar_registry=DEFAULT_SCALAR_REGISTRY, config=StrawberryConfig(), ) == { "input": AddReleaseFileCommentInput( pr_number=12, status=ReleaseFileStatus.OK, release_info=ReleaseInfo(change_type=ChangeType.MAJOR, changelog="example"), ) } args = { "input": { "prNumber": 12, "status": ReleaseFileStatus.OK, "releaseInfo": None, } } arguments = [ StrawberryArgument( graphql_name=None, python_name="input", type_annotation=StrawberryAnnotation(AddReleaseFileCommentInput), ), ] assert convert_arguments( args, arguments, scalar_registry=DEFAULT_SCALAR_REGISTRY, config=StrawberryConfig(), ) == { "input": AddReleaseFileCommentInput( pr_number=12, status=ReleaseFileStatus.OK, release_info=None ) } def test_nested_list_of_complex_types(): @strawberry.input class Number: value: int @strawberry.input class Input: numbers: list[Number] args = {"input": {"numbers": [{"value": 1}, {"value": 2}]}} arguments = [ StrawberryArgument( graphql_name=None, python_name="input", type_annotation=StrawberryAnnotation(Input), ), ] assert convert_arguments( args, arguments, scalar_registry=DEFAULT_SCALAR_REGISTRY, config=StrawberryConfig(), ) == {"input": Input(numbers=[Number(value=1), Number(value=2)])} def test_uses_default_for_optional_types_when_nothing_is_passed(): @strawberry.input class Number: value: int @strawberry.input class Input: numbers: Number | None = UNSET numbers_second: Number | None = UNSET # case 1 args = {"input": {}} arguments = [ StrawberryArgument( graphql_name=None, python_name="input", type_annotation=StrawberryAnnotation(Input), ), ] assert convert_arguments( args, arguments, scalar_registry=DEFAULT_SCALAR_REGISTRY, config=StrawberryConfig(), ) == {"input": Input(numbers=UNSET, numbers_second=UNSET)} # case 2 args = {"input": {"numbersSecond": None}} arguments = [ StrawberryArgument( graphql_name=None, python_name="input", type_annotation=StrawberryAnnotation(Input), ), ] assert convert_arguments( args, arguments, scalar_registry=DEFAULT_SCALAR_REGISTRY, config=StrawberryConfig(), ) == {"input": Input(numbers=UNSET, numbers_second=None)} def test_when_optional(): @strawberry.input class Number: value: int @strawberry.input class Input: numbers: Number | None = UNSET numbers_second: Number | None = UNSET args = {} arguments = [ StrawberryArgument( graphql_name=None, python_name="input", type_annotation=StrawberryAnnotation(Optional[Input]), ) ] assert ( convert_arguments( args, arguments, scalar_registry=DEFAULT_SCALAR_REGISTRY, config=StrawberryConfig(), ) == {} ) @pytest.mark.raises_strawberry_exception( UnsupportedTypeError, match=r" conversion is not supported", ) def test_fails_when_passing_non_strawberry_classes(): class Input: numbers: list[int] args = { "input": { "numbers": [1, 2], } } arguments = [ StrawberryArgument( graphql_name=None, python_name="input", type_annotation=StrawberryAnnotation(Optional[Input]), ) ] assert ( convert_arguments( args, arguments, scalar_registry=DEFAULT_SCALAR_REGISTRY, config=StrawberryConfig(), ) == {} ) strawberry-graphql-0.287.0/tests/utils/test_get_first_operation.py000066400000000000000000000016411511033167500255150ustar00rootroot00000000000000from graphql import OperationType, parse from strawberry.utils.operation import get_first_operation def test_document_without_operation_definition_nodes(): document = parse( """ fragment Test on Query { hello } """ ) assert get_first_operation(document) is None def test_single_operation_definition_node(): document = parse( """ query Operation1 { hello } """ ) node = get_first_operation(document) assert node is not None assert node.operation == OperationType.QUERY def test_multiple_operation_definition_nodes(): document = parse( """ mutation Operation1 { hello } query Operation2 { hello } """ ) node = get_first_operation(document) assert node is not None assert node.operation == OperationType.MUTATION strawberry-graphql-0.287.0/tests/utils/test_get_operation_type.py000066400000000000000000000042421511033167500253470ustar00rootroot00000000000000import pytest from graphql import parse from strawberry.types.graphql import OperationType from strawberry.utils.operation import get_operation_type mutation_collision = parse(""" fragment UserAgent on UserAgentType { id } mutation UserAgent { setUserAgent { ...UserAgent } } """) query_collision = parse(""" fragment UserAgent on UserAgentType { id } query UserAgent { userAgent { ...UserAgent } } """) subscription_collision = parse(""" fragment UserAgent on UserAgentType { id } subscription UserAgent { userAgent { ...UserAgent } } """) mutation_no_collision = parse(""" fragment UserAgentFragment on UserAgentType { id } mutation UserAgent { setUserAgent { ...UserAgentFragment } } """) query_no_collision = parse(""" fragment UserAgentFragment on UserAgentType { id } query UserAgent { userAgent { ...UserAgentFragment } } """) subscription_no_collision = parse(""" fragment UserAgentFragment on UserAgentType { id } subscription UserAgent { userAgent { ...UserAgentFragment } } """) @pytest.mark.parametrize( ("document", "operation", "expectation"), [ (query_collision, "UserAgent", OperationType.QUERY), (query_no_collision, "UserAgent", OperationType.QUERY), (mutation_collision, "UserAgent", OperationType.MUTATION), (mutation_no_collision, "UserAgent", OperationType.MUTATION), (subscription_collision, "UserAgent", OperationType.SUBSCRIPTION), (subscription_no_collision, "UserAgent", OperationType.SUBSCRIPTION), (query_collision, None, OperationType.QUERY), (mutation_collision, None, OperationType.MUTATION), (subscription_collision, None, OperationType.SUBSCRIPTION), ], ) def test_get_operation_type_with_fragment_name_collision( document, operation, expectation ): assert get_operation_type(document, operation) == expectation def test_get_operation_type_only_fragments(): only_fragments = parse(""" fragment Foo on Bar { id } """) with pytest.raises(RuntimeError) as excinfo: get_operation_type(only_fragments) assert "Can't get GraphQL operation type" in str(excinfo.value) strawberry-graphql-0.287.0/tests/utils/test_importer.py000066400000000000000000000025361511033167500233140ustar00rootroot00000000000000import pytest from strawberry.utils.importer import import_module_symbol from tests.fixtures.sample_package.sample_module import sample_instance, schema def test_symbol_import(): selector = "tests.fixtures.sample_package.sample_module:schema" schema_symbol = import_module_symbol(selector) assert schema_symbol == schema def test_default_symbol_import(): selector = "tests.fixtures.sample_package.sample_module" schema_symbol = import_module_symbol(selector, default_symbol_name="schema") assert schema_symbol == schema def test_nested_symbol_import(): selector = "tests.fixtures.sample_package.sample_module:sample_instance.schema" schema_symbol = import_module_symbol(selector) assert schema_symbol == sample_instance.schema def test_not_specifying_a_symbol(): selector = "tests.fixtures.sample_package.sample_module" with pytest.raises(ValueError) as exc: import_module_symbol(selector) assert "Selector does not include a symbol name" in str(exc.value) def test_invalid_module_import(): selector = "not.existing.module:schema" with pytest.raises(ImportError): import_module_symbol(selector) def test_invalid_symbol_import(): selector = "tests.fixtures.sample_package.sample_module:not.existing.symbol" with pytest.raises(AttributeError): import_module_symbol(selector) strawberry-graphql-0.287.0/tests/utils/test_inspect.py000066400000000000000000000063431511033167500231200ustar00rootroot00000000000000from typing import Generic, TypeVar import pytest import strawberry from strawberry.utils.inspect import get_specialized_type_var_map _T = TypeVar("_T") _K = TypeVar("_K") @pytest.mark.parametrize("value", [object, type(None), int, str, type("Foo", (), {})]) def test_get_specialized_type_var_map_non_generic(value: type): assert get_specialized_type_var_map(value) is None def test_get_specialized_type_var_map_generic_not_specialized(): @strawberry.type class Foo(Generic[_T]): ... assert get_specialized_type_var_map(Foo) == {} def test_get_specialized_type_var_map_generic(): @strawberry.type class Foo(Generic[_T]): ... @strawberry.type class Bar(Foo[int]): ... assert get_specialized_type_var_map(Bar) == {"_T": int} def test_get_specialized_type_var_map_from_alias(): @strawberry.type class Foo(Generic[_T]): ... SpecializedFoo = Foo[int] assert get_specialized_type_var_map(SpecializedFoo) == {"_T": int} def test_get_specialized_type_var_map_from_alias_with_inheritance(): @strawberry.type class Foo(Generic[_T]): ... SpecializedFoo = Foo[int] @strawberry.type class Bar(SpecializedFoo): ... assert get_specialized_type_var_map(Bar) == {"_T": int} def test_get_specialized_type_var_map_generic_subclass(): @strawberry.type class Foo(Generic[_T]): ... @strawberry.type class Bar(Foo[int]): ... @strawberry.type class BarSubclass(Bar): ... assert get_specialized_type_var_map(BarSubclass) == {"_T": int} def test_get_specialized_type_var_map_double_generic(): @strawberry.type class Foo(Generic[_T]): ... @strawberry.type class Bar(Foo[_T]): ... @strawberry.type class Bin(Bar[int]): ... assert get_specialized_type_var_map(Bin) == {"_T": int} def test_get_specialized_type_var_map_double_generic_subclass(): @strawberry.type class Foo(Generic[_T]): ... @strawberry.type class Bar(Foo[_T]): ... @strawberry.type class Bin(Bar[int]): ... @strawberry.type class BinSubclass(Bin): ... assert get_specialized_type_var_map(Bin) == {"_T": int} def test_get_specialized_type_var_map_double_generic_passthrough(): @strawberry.type class Foo(Generic[_T]): ... @strawberry.type class Bar(Foo[_K], Generic[_K]): ... @strawberry.type class Bin(Bar[int]): ... assert get_specialized_type_var_map(Bin) == { "_T": int, "_K": int, } def test_get_specialized_type_var_map_multiple_inheritance(): @strawberry.type class Foo(Generic[_T]): ... @strawberry.type class Bar(Generic[_K]): ... @strawberry.type class Bin(Foo[int]): ... @strawberry.type class Baz(Bin, Bar[str]): ... assert get_specialized_type_var_map(Baz) == { "_T": int, "_K": str, } def test_get_specialized_type_var_map_multiple_inheritance_subclass(): @strawberry.type class Foo(Generic[_T]): ... @strawberry.type class Bar(Generic[_K]): ... @strawberry.type class Bin(Foo[int]): ... @strawberry.type class Baz(Bin, Bar[str]): ... @strawberry.type class BazSubclass(Baz): ... assert get_specialized_type_var_map(BazSubclass) == { "_T": int, "_K": str, } strawberry-graphql-0.287.0/tests/utils/test_locate_definition.py000066400000000000000000000067501511033167500251340ustar00rootroot00000000000000from pathlib import Path from inline_snapshot import snapshot from strawberry.utils.importer import import_module_symbol from strawberry.utils.locate_definition import locate_definition from tests.typecheckers.utils.marks import skip_on_windows pytestmark = skip_on_windows def _simplify_path(path: str) -> str: path = Path(path) root = Path(__file__).parents[1] return str(path.relative_to(root)) def test_find_model_name() -> None: schema = import_module_symbol( "tests.fixtures.sample_package.sample_module", default_symbol_name="schema" ) result = locate_definition(schema, "User") assert _simplify_path(result) == snapshot( "fixtures/sample_package/sample_module.py:38:7" ) def test_find_model_name_enum() -> None: schema = import_module_symbol( "tests.fixtures.sample_package.sample_module", default_symbol_name="schema" ) result = locate_definition(schema, "Role") assert _simplify_path(result) == snapshot( "fixtures/sample_package/sample_module.py:32:7" ) def test_find_model_name_scalar() -> None: schema = import_module_symbol( "tests.fixtures.sample_package.sample_module", default_symbol_name="schema" ) result = locate_definition(schema, "ExampleScalar") assert _simplify_path(result) == snapshot( "fixtures/sample_package/sample_module.py:7:13" ) def test_find_model_field() -> None: schema = import_module_symbol( "tests.fixtures.sample_package.sample_module", default_symbol_name="schema" ) result = locate_definition(schema, "User.name") assert _simplify_path(result) == snapshot( "fixtures/sample_package/sample_module.py:39:5" ) def test_find_model_field_scalar() -> None: schema = import_module_symbol( "tests.fixtures.sample_package.sample_module", default_symbol_name="schema" ) result = locate_definition(schema, "User.example_scalar") assert _simplify_path(result) == snapshot( "fixtures/sample_package/sample_module.py:42:5" ) def test_find_model_field_with_resolver() -> None: schema = import_module_symbol( "tests.fixtures.sample_package.sample_module", default_symbol_name="schema" ) result = locate_definition(schema, "Query.user") assert _simplify_path(result) == snapshot( "fixtures/sample_package/sample_module.py:50:5" ) def test_find_missing_model() -> None: schema = import_module_symbol( "tests.fixtures.sample_package.sample_module", default_symbol_name="schema" ) result = locate_definition(schema, "Missing") assert result is None def test_find_missing_model_field() -> None: schema = import_module_symbol( "tests.fixtures.sample_package.sample_module", default_symbol_name="schema" ) result = locate_definition(schema, "Missing.field") assert result is None def test_find_union() -> None: schema = import_module_symbol( "tests.fixtures.sample_package.sample_module", default_symbol_name="schema" ) result = locate_definition(schema, "UnionExample") assert _simplify_path(result) == snapshot( "fixtures/sample_package/sample_module.py:23:16" ) def test_find_inline_union() -> None: schema = import_module_symbol( "tests.fixtures.sample_package.sample_module", default_symbol_name="schema" ) result = locate_definition(schema, "InlineUnion") assert _simplify_path(result) == snapshot( "fixtures/sample_package/sample_module.py:44:19" ) strawberry-graphql-0.287.0/tests/utils/test_logging.py000066400000000000000000000006251511033167500230760ustar00rootroot00000000000000import logging from graphql.error import GraphQLError from strawberry.utils.logging import StrawberryLogger def test_strawberry_logger_error(caplog): caplog.set_level(logging.ERROR, logger="strawberry.execution") exc = GraphQLError("test exception") StrawberryLogger.error(exc) assert caplog.record_tuples == [ ("strawberry.execution", logging.ERROR, "test exception") ] strawberry-graphql-0.287.0/tests/utils/test_typing.py000066400000000000000000000064651511033167500227720ustar00rootroot00000000000000import typing from typing import Annotated, ClassVar, ForwardRef, Optional, Union import strawberry from strawberry.types.lazy_type import LazyType from strawberry.utils.typing import eval_type, get_optional_annotation, is_classvar @strawberry.type class Fruit: ... def test_get_optional_annotation(): # Pair Union assert get_optional_annotation(Optional[str | bool]) == Union[str, bool] # More than pair Union assert get_optional_annotation(Optional[str | int | bool]) == Union[str, int, bool] def test_eval_type(): class Foo: ... assert eval_type(ForwardRef("str")) is str assert eval_type(str) is str assert eval_type(ForwardRef("Foo"), None, locals()) is Foo assert eval_type(Foo, None, locals()) is Foo assert eval_type(ForwardRef("Optional[Foo]"), globals(), locals()) == Optional[Foo] assert eval_type(Optional["Foo"], globals(), locals()) == Optional[Foo] assert ( eval_type(ForwardRef("Union[Foo, str]"), globals(), locals()) == Union[Foo, str] ) assert eval_type(Union["Foo", "str"], globals(), locals()) == Union[Foo, str] assert ( eval_type(ForwardRef("Optional[Union[Foo, str]]"), globals(), locals()) == Union[Foo, str, None] ) assert ( eval_type(ForwardRef("Annotated[str, 'foobar']"), globals(), locals()) is Annotated[str, "foobar"] ) assert ( eval_type( ForwardRef("Annotated[Fruit, strawberry.lazy('tests.utils.test_typing')]"), {"strawberry": strawberry, "Annotated": Annotated}, None, ) == Annotated[ LazyType("Fruit", "tests.utils.test_typing"), strawberry.lazy("tests.utils.test_typing"), ] ) assert ( eval_type( ForwardRef("Annotated[strawberry.auto, 'foobar']"), {"strawberry": strawberry, "Annotated": Annotated}, None, ) == Annotated[strawberry.auto, "foobar"] ) assert ( eval_type( ForwardRef("Annotated[datetime, strawberry.lazy('datetime')]"), {"strawberry": strawberry, "Annotated": Annotated}, None, ) == Annotated[ LazyType("datetime", "datetime"), strawberry.lazy("datetime"), ] ) def test_eval_type_with_deferred_annotations(): assert ( eval_type( ForwardRef( "Annotated['Fruit', strawberry.lazy('tests.utils.test_typing')]" ), {"strawberry": strawberry, "Annotated": Annotated}, None, ) == Annotated[ LazyType("Fruit", "tests.utils.test_typing"), strawberry.lazy("tests.utils.test_typing"), ] ) assert ( eval_type( ForwardRef("Annotated['datetime', strawberry.lazy('datetime')]"), {"strawberry": strawberry, "Annotated": Annotated}, None, ) == Annotated[ LazyType("datetime", "datetime"), strawberry.lazy("datetime"), ] ) def test_is_classvar(): class Foo: attr1: str attr2: ClassVar[str] attr3: typing.ClassVar[str] assert not is_classvar(Foo, Foo.__annotations__["attr1"]) assert is_classvar(Foo, Foo.__annotations__["attr2"]) assert is_classvar(Foo, Foo.__annotations__["attr3"]) strawberry-graphql-0.287.0/tests/utils/test_typing_forward_refs.py000066400000000000000000000035371511033167500255320ustar00rootroot00000000000000from __future__ import annotations import typing from typing import ClassVar, ForwardRef, Optional, Union from strawberry.scalars import JSON from strawberry.utils.typing import eval_type, is_classvar def test_eval_type(): class Foo: ... assert eval_type(ForwardRef("Foo | None"), globals(), locals()) == Optional[Foo] assert eval_type(ForwardRef("Foo | str"), globals(), locals()) == Union[Foo, str] assert ( eval_type(ForwardRef("Foo | str | None"), globals(), locals()) == Union[Foo, str, None] ) assert ( eval_type(ForwardRef("list[Foo | str] | None"), globals(), locals()) == Union[list[Foo | str], None] ) assert ( eval_type(ForwardRef("list[Foo | str] | None | int"), globals(), locals()) == Union[list[Foo | str], int, None] ) assert eval_type(ForwardRef("JSON | None"), globals(), locals()) == Optional[JSON] def test_eval_type_generic_type_alias(): class Foo: ... assert eval_type(ForwardRef("Foo | None"), globals(), locals()) == Optional[Foo] assert eval_type(ForwardRef("Foo | str"), globals(), locals()) == Union[Foo, str] assert ( eval_type(ForwardRef("Foo | str | None"), globals(), locals()) == Union[Foo, str, None] ) assert ( eval_type(ForwardRef("list[Foo | str] | None"), globals(), locals()) == Union[list[Foo | str], None] # type: ignore ) assert ( eval_type(ForwardRef("list[Foo | str] | None | int"), globals(), locals()) == Union[list[Foo | str], int, None] # type: ignore ) def test_is_classvar(): class Foo: attr1: str attr2: ClassVar[str] attr3: typing.ClassVar[str] assert not is_classvar(Foo, Foo.__annotations__["attr1"]) assert is_classvar(Foo, Foo.__annotations__["attr2"]) assert is_classvar(Foo, Foo.__annotations__["attr3"]) strawberry-graphql-0.287.0/tests/views/000077500000000000000000000000001511033167500200315ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/views/__init__.py000066400000000000000000000000001511033167500221300ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/views/schema.py000066400000000000000000000235211511033167500216460ustar00rootroot00000000000000import asyncio import contextlib from collections.abc import AsyncGenerator from enum import Enum from typing import Any from graphql import GraphQLError from graphql.version import VersionInfo, version_info import strawberry from strawberry.extensions import SchemaExtension from strawberry.file_uploads import Upload from strawberry.permission import BasePermission from strawberry.schema.config import StrawberryConfig from strawberry.subscriptions.protocols.graphql_transport_ws.types import PingMessage from strawberry.types import ExecutionContext class AlwaysFailPermission(BasePermission): message = "You are not authorized" def has_permission(self, source: Any, info: strawberry.Info, **kwargs: Any) -> bool: return False class MyExtension(SchemaExtension): def get_results(self) -> dict[str, str]: return {"example": "example"} def _read_file(text_file: Upload) -> str: with contextlib.suppress(ModuleNotFoundError): from starlette.datastructures import UploadFile # allow to keep this function synchronous, starlette's files have # async methods for reading if isinstance(text_file, UploadFile): text_file = text_file.file._file # type: ignore with contextlib.suppress(ModuleNotFoundError): from litestar.datastructures import UploadFile as LitestarUploadFile if isinstance(text_file, LitestarUploadFile): text_file = text_file.file # type: ignore with contextlib.suppress(ModuleNotFoundError): from sanic.request import File as SanicUploadFile if isinstance(text_file, SanicUploadFile): return text_file.body.decode() return text_file.read().decode() @strawberry.enum class Flavor(Enum): VANILLA = "vanilla" STRAWBERRY = "strawberry" CHOCOLATE = "chocolate" @strawberry.input class FolderInput: files: list[Upload] @strawberry.type class DebugInfo: num_active_result_handlers: int is_connection_init_timeout_task_done: bool | None @strawberry.type class Hero: id: strawberry.ID @strawberry.field @staticmethod def name(fail: bool = False) -> str: if fail: raise ValueError("Failed to get name") return "Thiago Bellini" @strawberry.type class Query: @strawberry.field def greetings(self) -> str: return "hello" @strawberry.field def hello(self, name: str | None = None) -> str: return f"Hello {name or 'world'}" @strawberry.field async def async_hello(self, name: str | None = None, delay: float = 0) -> str: await asyncio.sleep(delay) return f"Hello {name or 'world'}" @strawberry.field(permission_classes=[AlwaysFailPermission]) def always_fail(self) -> str | None: return "Hey" @strawberry.field async def error(self, message: str) -> AsyncGenerator[str, None]: yield GraphQLError(message) # type: ignore @strawberry.field async def exception(self, message: str) -> str: raise ValueError(message) @strawberry.field async def some_error(self) -> str | None: raise ValueError("Some error") @strawberry.field def teapot(self, info: strawberry.Info[Any, None]) -> str: info.context["response"].status_code = 418 return "🫖" @strawberry.field def root_name(self) -> str: return type(self).__name__ @strawberry.field def value_from_context( self, info: strawberry.Info, key: str = "custom_value" ) -> str: return info.context[key] @strawberry.field def value_from_extensions(self, info: strawberry.Info, key: str) -> str: return info.input_extensions[key] @strawberry.field def returns_401(self, info: strawberry.Info) -> str: response = info.context["response"] if hasattr(response, "set_status"): response.set_status(401) else: response.status_code = 401 return "hey" @strawberry.field def set_header(self, info: strawberry.Info, name: str) -> str: response = info.context["response"] response.headers["X-Name"] = name return name @strawberry.field def character(self) -> Hero: return Hero(id=strawberry.ID("1")) @strawberry.field async def streamable_field(self) -> strawberry.Streamable[str]: for i in range(2): yield f"Hello {i}" await asyncio.sleep(0.1) @strawberry.type class Mutation: @strawberry.mutation def echo(self, string_to_echo: str) -> str: return string_to_echo @strawberry.mutation def hello(self) -> str: return "strawberry" @strawberry.mutation def read_text(self, text_file: Upload) -> str: return _read_file(text_file) @strawberry.mutation def read_files(self, files: list[Upload]) -> list[str]: return list(map(_read_file, files)) @strawberry.mutation def read_folder(self, folder: FolderInput) -> list[str]: return list(map(_read_file, folder.files)) @strawberry.mutation def match_text(self, text_file: Upload, pattern: str) -> str: text = text_file.read().decode() return pattern if pattern in text else "" @strawberry.mutation def update_context(self, info: strawberry.Info, key: str, value: str) -> bool: info.context[key] = value return True @strawberry.type class Subscription: active_infinity_subscriptions = 0 @strawberry.subscription async def echo(self, message: str, delay: float = 0) -> AsyncGenerator[str, None]: await asyncio.sleep(delay) yield message @strawberry.subscription async def request_ping(self, info: strawberry.Info) -> AsyncGenerator[bool, None]: ws = info.context["ws"] await ws.send_json(PingMessage({"type": "ping"})) yield True @strawberry.subscription async def infinity(self, message: str) -> AsyncGenerator[str, None]: Subscription.active_infinity_subscriptions += 1 try: while True: yield message await asyncio.sleep(1) finally: Subscription.active_infinity_subscriptions -= 1 @strawberry.subscription async def context(self, info: strawberry.Info) -> AsyncGenerator[str, None]: yield info.context["custom_value"] @strawberry.subscription async def error(self, message: str) -> AsyncGenerator[str, None]: yield GraphQLError(message) # type: ignore @strawberry.subscription async def exception(self, message: str) -> AsyncGenerator[str, None]: raise ValueError(message) # Without this yield, the method is not recognised as an async generator yield "Hi" @strawberry.subscription async def flavors(self) -> AsyncGenerator[Flavor, None]: yield Flavor.VANILLA yield Flavor.STRAWBERRY yield Flavor.CHOCOLATE @strawberry.subscription async def flavors_invalid(self) -> AsyncGenerator[Flavor, None]: yield Flavor.VANILLA yield "invalid type" # type: ignore yield Flavor.CHOCOLATE @strawberry.subscription async def debug(self, info: strawberry.Info) -> AsyncGenerator[DebugInfo, None]: active_result_handlers = [ task for task in info.context["get_tasks"]() if not task.done() ] connection_init_timeout_task = info.context["connectionInitTimeoutTask"] is_connection_init_timeout_task_done = ( connection_init_timeout_task.done() if connection_init_timeout_task else None ) yield DebugInfo( num_active_result_handlers=len(active_result_handlers), is_connection_init_timeout_task_done=is_connection_init_timeout_task_done, ) @strawberry.subscription async def listener( self, info: strawberry.Info, timeout: float | None = None, group: str | None = None, ) -> AsyncGenerator[str, None]: yield info.context["request"].channel_name async with info.context["request"].listen_to_channel( type="test.message", timeout=timeout, groups=[group] if group is not None else [], ) as cm: async for message in cm: yield message["text"] @strawberry.subscription async def listener_with_confirmation( self, info: strawberry.Info, timeout: float | None = None, group: str | None = None, ) -> AsyncGenerator[str | None, None]: async with info.context["request"].listen_to_channel( type="test.message", timeout=timeout, groups=[group] if group is not None else [], ) as cm: yield None yield info.context["request"].channel_name async for message in cm: yield message["text"] @strawberry.subscription async def connection_params( self, info: strawberry.Info ) -> AsyncGenerator[strawberry.scalars.JSON, None]: yield info.context["connection_params"] @strawberry.subscription async def long_finalizer( self, info: strawberry.Info, delay: float = 0 ) -> AsyncGenerator[str, None]: try: for _i in range(100): yield "hello" await asyncio.sleep(0.01) finally: await asyncio.sleep(delay) class Schema(strawberry.Schema): def process_errors( self, errors: list, execution_context: ExecutionContext | None = None ) -> None: import traceback traceback.print_stack() return super().process_errors(errors, execution_context) schema = Schema( query=Query, mutation=Mutation, subscription=Subscription, extensions=[MyExtension], config=StrawberryConfig( enable_experimental_incremental_execution=( version_info >= VersionInfo.from_str("3.3.0a0") ) ), ) strawberry-graphql-0.287.0/tests/websockets/000077500000000000000000000000001511033167500210455ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/websockets/__init__.py000066400000000000000000000000001511033167500231440ustar00rootroot00000000000000strawberry-graphql-0.287.0/tests/websockets/conftest.py000066400000000000000000000025441511033167500232510ustar00rootroot00000000000000import importlib from collections.abc import Generator from typing import Any import pytest from tests.http.clients.base import HttpClient from tests.views.schema import schema def _get_http_client_classes() -> Generator[Any, None, None]: for client, module, marks in [ ("AioHttpClient", "aiohttp", [pytest.mark.aiohttp]), ("AsgiHttpClient", "asgi", [pytest.mark.asgi]), ("ChannelsHttpClient", "channels", [pytest.mark.channels]), ("FastAPIHttpClient", "fastapi", [pytest.mark.fastapi]), ("LitestarHttpClient", "litestar", [pytest.mark.litestar]), ("QuartHttpClient", "quart", [pytest.mark.quart]), ]: try: client_class = getattr( importlib.import_module(f"tests.http.clients.{module}"), client ) except ImportError: client_class = None yield pytest.param( client_class, marks=[ *marks, pytest.mark.skipif( client_class is None, reason=f"Client {client} not found" ), ], ) @pytest.fixture(params=_get_http_client_classes()) def http_client_class(request: Any) -> type[HttpClient]: return request.param @pytest.fixture def http_client(http_client_class: type[HttpClient]) -> HttpClient: return http_client_class(schema) strawberry-graphql-0.287.0/tests/websockets/test_graphql_transport_ws.py000066400000000000000000001113011511033167500267360ustar00rootroot00000000000000from __future__ import annotations import asyncio import contextlib import json import time from collections.abc import AsyncGenerator from datetime import timedelta from typing import TYPE_CHECKING from unittest.mock import AsyncMock, Mock, patch import pytest import pytest_asyncio from pytest_mock import MockerFixture from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL from strawberry.subscriptions.protocols.graphql_transport_ws.types import ( CompleteMessage, ConnectionAckMessage, ConnectionInitMessage, ErrorMessage, NextMessage, PingMessage, PongMessage, SubscribeMessage, ) from tests.http.clients.base import DebuggableGraphQLTransportWSHandler from tests.views.schema import MyExtension, Schema, Subscription, schema if TYPE_CHECKING: from tests.http.clients.base import HttpClient, WebSocketClient @pytest_asyncio.fixture async def ws_raw(http_client: HttpClient) -> AsyncGenerator[WebSocketClient, None]: async with http_client.ws_connect( "/graphql", protocols=[GRAPHQL_TRANSPORT_WS_PROTOCOL] ) as ws: yield ws await ws.close() assert ws.closed @pytest_asyncio.fixture async def ws(ws_raw: WebSocketClient) -> WebSocketClient: await ws_raw.send_message({"type": "connection_init"}) connection_ack_message: ConnectionAckMessage = await ws_raw.receive_json() assert connection_ack_message == {"type": "connection_ack"} return ws_raw def assert_next( next_message: NextMessage, id: str, data: dict[str, object], extensions: dict[str, object] | None = None, ): """ Assert that the NextMessage payload contains the provided data. If extensions is provided, it will also assert that the extensions are present """ assert next_message["type"] == "next" assert next_message["id"] == id assert set(next_message["payload"].keys()) <= {"data", "errors", "extensions"} assert "data" in next_message["payload"] assert next_message["payload"]["data"] == data if extensions is not None: assert "extensions" in next_message["payload"] assert next_message["payload"]["extensions"] == extensions async def test_unknown_message_type(ws_raw: WebSocketClient): ws = ws_raw await ws.send_json({"type": "NOT_A_MESSAGE_TYPE"}) await ws.receive(timeout=2) assert ws.closed assert ws.close_code == 4400 assert ws.close_reason == "Unknown message type: NOT_A_MESSAGE_TYPE" async def test_missing_message_type(ws_raw: WebSocketClient): ws = ws_raw await ws.send_json({"notType": None}) await ws.receive(timeout=2) assert ws.closed assert ws.close_code == 4400 assert ws.close_reason == "Failed to parse message" async def test_parsing_an_invalid_message(ws: WebSocketClient): await ws.send_json({"type": "subscribe", "notPayload": None}) await ws.receive(timeout=2) assert ws.closed assert ws.close_code == 4400 assert ws.close_reason == "Failed to parse message" async def test_non_text_ws_messages_result_in_socket_closure(ws_raw: WebSocketClient): ws = ws_raw await ws.send_bytes( json.dumps(ConnectionInitMessage({"type": "connection_init"})).encode() ) await ws.receive(timeout=2) assert ws.closed assert ws.close_code == 4400 assert ws.close_reason == "WebSocket message type must be text" async def test_non_json_ws_messages_result_in_socket_closure(ws_raw: WebSocketClient): ws = ws_raw await ws.send_text("not valid json") await ws.receive(timeout=2) assert ws.closed assert ws.close_code == 4400 assert ws.close_reason == "WebSocket message must be valid JSON" async def test_ws_message_frame_types_cannot_be_mixed(ws_raw: WebSocketClient): ws = ws_raw await ws.send_message({"type": "connection_init"}) ack_message: ConnectionAckMessage = await ws.receive_json() assert ack_message == {"type": "connection_ack"} await ws.send_bytes( json.dumps( SubscribeMessage( { "id": "sub1", "type": "subscribe", "payload": { "query": "subscription { debug { isConnectionInitTimeoutTaskDone } }" }, } ) ).encode() ) await ws.receive(timeout=2) assert ws.closed assert ws.close_code == 4400 assert ws.close_reason == "WebSocket message type must be text" async def test_connection_init_timeout(http_client_class: type[HttpClient]): test_client = http_client_class( schema, connection_init_wait_timeout=timedelta(seconds=0) ) async with test_client.ws_connect( "/graphql", protocols=[GRAPHQL_TRANSPORT_WS_PROTOCOL] ) as ws: await ws.receive(timeout=2) assert ws.closed assert ws.close_code == 4408 assert ws.close_reason == "Connection initialisation timeout" @pytest.mark.flaky async def test_connection_init_timeout_cancellation( ws_raw: WebSocketClient, ): # Verify that the timeout task is cancelled after the connection Init # message is received ws = ws_raw await ws.send_message({"type": "connection_init"}) connection_ack_message: ConnectionAckMessage = await ws.receive_json() assert connection_ack_message == {"type": "connection_ack"} await ws.send_message( { "id": "sub1", "type": "subscribe", "payload": { "query": "subscription { debug { isConnectionInitTimeoutTaskDone } }" }, } ) next_message: NextMessage = await ws.receive_json() assert_next( next_message, "sub1", {"debug": {"isConnectionInitTimeoutTaskDone": True}} ) @pytest.mark.xfail(reason="This test is flaky") async def test_close_twice(mocker: MockerFixture, http_client_class: type[HttpClient]): test_client = http_client_class( schema, connection_init_wait_timeout=timedelta(seconds=0.25) ) async with test_client.ws_connect( "/graphql", protocols=[GRAPHQL_TRANSPORT_WS_PROTOCOL] ) as ws: transport_close = mocker.patch.object(ws, "close") # We set payload is set to "invalid value" to force a invalid payload error # which will close the connection await ws.send_json({"type": "connection_init", "payload": "invalid value"}) # Yield control so that ._close can be called await asyncio.sleep(0) for t in asyncio.all_tasks(): if ( t.get_coro().__qualname__ == "BaseGraphQLTransportWSHandler.handle_connection_init_timeout" ): # The init timeout task should be cancelled with pytest.raises(asyncio.CancelledError): await t await ws.receive(timeout=0.5) assert ws.closed assert ws.close_code == 4400 assert ws.close_reason == "Invalid connection init payload" transport_close.assert_not_called() async def test_too_many_initialisation_requests(ws: WebSocketClient): await ws.send_message({"type": "connection_init"}) await ws.receive(timeout=2) assert ws.closed assert ws.close_code == 4429 assert ws.close_reason == "Too many initialisation requests" async def test_connections_are_accepted_by_default(ws_raw: WebSocketClient): await ws_raw.send_message({"type": "connection_init"}) connection_ack_message: ConnectionAckMessage = await ws_raw.receive_json() assert connection_ack_message == {"type": "connection_ack"} await ws_raw.close() assert ws_raw.closed @pytest.mark.parametrize("payload", [None, {"token": "secret"}]) async def test_setting_a_connection_ack_payload(ws_raw: WebSocketClient, payload): await ws_raw.send_message( { "type": "connection_init", "payload": {"test-accept": True, "ack-payload": payload}, } ) connection_ack_message: ConnectionAckMessage = await ws_raw.receive_json() assert connection_ack_message == {"type": "connection_ack", "payload": payload} await ws_raw.close() assert ws_raw.closed async def test_connection_ack_payload_may_be_unset(ws_raw: WebSocketClient): await ws_raw.send_message( { "type": "connection_init", "payload": {"test-accept": True}, } ) connection_ack_message: ConnectionAckMessage = await ws_raw.receive_json() assert connection_ack_message == {"type": "connection_ack"} await ws_raw.close() assert ws_raw.closed async def test_rejecting_connection_closes_socket_with_expected_code_and_message( ws_raw: WebSocketClient, ): await ws_raw.send_message( {"type": "connection_init", "payload": {"test-reject": True}} ) await ws_raw.receive(timeout=2) assert ws_raw.closed assert ws_raw.close_code == 4403 assert ws_raw.close_reason == "Forbidden" async def test_context_can_be_modified_from_within_on_ws_connect( ws_raw: WebSocketClient, ): await ws_raw.send_message( { "type": "connection_init", "payload": {"test-modify": True}, } ) connection_ack_message: ConnectionAckMessage = await ws_raw.receive_json() assert connection_ack_message == {"type": "connection_ack"} await ws_raw.send_message( { "type": "subscribe", "id": "demo", "payload": { "query": "subscription { connectionParams }", }, } ) next_message: NextMessage = await ws_raw.receive_json() assert next_message["type"] == "next" assert next_message["id"] == "demo" assert "data" in next_message["payload"] assert next_message["payload"]["data"] == { "connectionParams": {"test-modify": True, "modified": True} } await ws_raw.close() assert ws_raw.closed async def test_ping_pong(ws: WebSocketClient): await ws.send_message({"type": "ping"}) pong_message: PongMessage = await ws.receive_json() assert pong_message == {"type": "pong"} async def test_can_send_payload_with_additional_things(ws_raw: WebSocketClient): ws = ws_raw # send init await ws.send_message({"type": "connection_init"}) await ws.receive(timeout=2) await ws.send_message( { "type": "subscribe", "payload": { "query": 'subscription { echo(message: "Hi") }', "extensions": { "some": "other thing", }, }, "id": "1", } ) next_message: NextMessage = await ws.receive_json(timeout=2) assert next_message == { "type": "next", "id": "1", "payload": {"data": {"echo": "Hi"}, "extensions": {"example": "example"}}, } async def test_server_sent_ping(ws: WebSocketClient): await ws.send_message( { "id": "sub1", "type": "subscribe", "payload": {"query": "subscription { requestPing }"}, } ) ping_message: PingMessage = await ws.receive_json() assert ping_message == {"type": "ping"} await ws.send_message({"type": "pong"}) next_message: NextMessage = await ws.receive_json() assert_next(next_message, "sub1", {"requestPing": True}) complete_message: CompleteMessage = await ws.receive_json() assert complete_message == {"id": "sub1", "type": "complete"} async def test_unauthorized_subscriptions(ws_raw: WebSocketClient): ws = ws_raw await ws.send_message( { "id": "sub1", "type": "subscribe", "payload": {"query": 'subscription { echo(message: "Hi") }'}, } ) await ws.receive(timeout=2) assert ws.closed assert ws.close_code == 4401 assert ws.close_reason == "Unauthorized" async def test_duplicated_operation_ids(ws: WebSocketClient): await ws.send_message( { "id": "sub1", "type": "subscribe", "payload": {"query": 'subscription { echo(message: "Hi", delay: 5) }'}, } ) await ws.send_message( { "id": "sub1", "type": "subscribe", "payload": {"query": 'subscription { echo(message: "Hi", delay: 5) }'}, } ) await ws.receive(timeout=2) assert ws.closed assert ws.close_code == 4409 assert ws.close_reason == "Subscriber for sub1 already exists" async def test_reused_operation_ids(ws: WebSocketClient): """Test that an operation id can be re-used after it has been previously used for a completed operation. """ # Use sub1 as an id for an operation await ws.send_message( { "id": "sub1", "type": "subscribe", "payload": {"query": 'subscription { echo(message: "Hi") }'}, } ) next_message1: NextMessage = await ws.receive_json() assert_next(next_message1, "sub1", {"echo": "Hi"}) complete_message: CompleteMessage = await ws.receive_json() assert complete_message == {"id": "sub1", "type": "complete"} # operation is now complete. Create a new operation using # the same ID await ws.send_message( { "id": "sub1", "type": "subscribe", "payload": {"query": 'subscription { echo(message: "Hi") }'}, } ) next_message2: NextMessage = await ws.receive_json() assert_next(next_message2, "sub1", {"echo": "Hi"}) async def test_simple_subscription(ws: WebSocketClient): await ws.send_message( { "id": "sub1", "type": "subscribe", "payload": {"query": 'subscription { echo(message: "Hi") }'}, } ) next_message: NextMessage = await ws.receive_json() assert_next(next_message, "sub1", {"echo": "Hi"}) await ws.send_message({"id": "sub1", "type": "complete"}) @pytest.mark.parametrize( ("extra_payload", "expected_message"), [ ({}, "Hi1"), ({"operationName": None}, "Hi1"), ({"operationName": "Subscription1"}, "Hi1"), ({"operationName": "Subscription2"}, "Hi2"), ], ) async def test_operation_selection( ws: WebSocketClient, extra_payload, expected_message ): await ws.send_json( { "type": "subscribe", "id": "sub1", "payload": { "query": """ subscription Subscription1 { echo(message: "Hi1") } subscription Subscription2 { echo(message: "Hi2") } """, **extra_payload, }, } ) next_message: NextMessage = await ws.receive_json() assert_next(next_message, "sub1", {"echo": expected_message}) await ws.send_message({"id": "sub1", "type": "complete"}) @pytest.mark.parametrize( ("operation_name"), ["", "Subscription2"], ) async def test_invalid_operation_selection(ws: WebSocketClient, operation_name): await ws.send_message( { "type": "subscribe", "id": "sub1", "payload": { "query": """ subscription Subscription1 { echo(message: "Hi1") } """, "operationName": f"{operation_name}", }, } ) await ws.receive(timeout=2) assert ws.closed assert ws.close_code == 4400 assert ws.close_reason == f'Unknown operation named "{operation_name}".' async def test_operation_selection_without_operations(ws: WebSocketClient): await ws.send_message( { "type": "subscribe", "id": "sub1", "payload": { "query": """ fragment Fragment1 on Query { __typename } """, }, } ) await ws.receive(timeout=2) assert ws.closed assert ws.close_code == 4400 assert ws.close_reason == "Can't get GraphQL operation type" async def test_subscription_syntax_error(ws: WebSocketClient): await ws.send_message( { "id": "sub1", "type": "subscribe", "payload": {"query": "subscription { INVALID_SYNTAX "}, } ) await ws.receive(timeout=2) assert ws.closed assert ws.close_code == 4400 assert ws.close_reason == "Syntax Error: Expected Name, found ." async def test_subscription_field_errors(ws: WebSocketClient): process_errors = Mock() with patch.object(Schema, "process_errors", process_errors): await ws.send_message( { "id": "sub1", "type": "subscribe", "payload": { "query": "subscription { notASubscriptionField }", }, } ) error_message: ErrorMessage = await ws.receive_json() assert error_message["type"] == "error" assert error_message["id"] == "sub1" assert len(error_message["payload"]) == 1 assert "locations" in error_message["payload"][0] assert error_message["payload"][0]["locations"] == [{"line": 1, "column": 16}] assert "message" in error_message["payload"][0] assert ( error_message["payload"][0]["message"] == "Cannot query field 'notASubscriptionField' on type 'Subscription'." ) process_errors.assert_called_once() async def test_subscription_cancellation(ws: WebSocketClient): await ws.send_message( { "id": "sub1", "type": "subscribe", "payload": {"query": 'subscription { echo(message: "Hi", delay: 99) }'}, } ) await ws.send_message( { "id": "sub2", "type": "subscribe", "payload": { "query": "subscription { debug { numActiveResultHandlers } }", }, } ) next_message: NextMessage = await ws.receive_json() assert_next(next_message, "sub2", {"debug": {"numActiveResultHandlers": 2}}) complete_message: CompleteMessage = await ws.receive_json() assert complete_message == {"id": "sub2", "type": "complete"} await ws.send_message({"id": "sub1", "type": "complete"}) await ws.send_message( { "id": "sub3", "type": "subscribe", "payload": { "query": "subscription { debug { numActiveResultHandlers } }", }, } ) next_message: NextMessage = await ws.receive_json() assert_next(next_message, "sub3", {"debug": {"numActiveResultHandlers": 1}}) complete_message: CompleteMessage = await ws.receive_json() assert complete_message == {"id": "sub3", "type": "complete"} async def test_subscription_errors(ws: WebSocketClient): await ws.send_message( { "id": "sub1", "type": "subscribe", "payload": { "query": 'subscription { error(message: "TEST ERR") }', }, } ) next_message: NextMessage = await ws.receive_json() assert next_message["type"] == "next" assert next_message["id"] == "sub1" assert "errors" in next_message["payload"] payload_errors = next_message["payload"]["errors"] assert payload_errors is not None assert len(payload_errors) == 1 assert "path" in payload_errors[0] assert payload_errors[0]["path"] == ["error"] assert "message" in payload_errors[0] assert payload_errors[0]["message"] == "TEST ERR" async def test_operation_error_no_complete(ws: WebSocketClient): """Test that an "error" message is not followed by "complete".""" # Since we don't include the operation variables, # the subscription will fail immediately. # see https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md#error await ws.send_message( { "id": "sub1", "type": "subscribe", "payload": { "query": "subscription Foo($bar: String!){ exception(message: $bar) }", }, } ) error_message: ErrorMessage = await ws.receive_json() assert error_message["type"] == "error" assert error_message["id"] == "sub1" # after an "error" message, there should be nothing more # sent regarding "sub1", not even a "complete". await ws.send_message({"type": "ping"}) pong_message: PongMessage = await ws.receive_json(timeout=1) assert pong_message == {"type": "pong"} async def test_subscription_exceptions(ws: WebSocketClient): process_errors = Mock() with patch.object(Schema, "process_errors", process_errors): await ws.send_message( { "id": "sub1", "type": "subscribe", "payload": { "query": 'subscription { exception(message: "TEST EXC") }', }, } ) next_message: NextMessage = await ws.receive_json() assert next_message["type"] == "next" assert next_message["id"] == "sub1" assert "errors" in next_message["payload"] assert next_message["payload"]["errors"] == [{"message": "TEST EXC"}] process_errors.assert_called_once() async def test_single_result_query_operation(ws: WebSocketClient): await ws.send_message( { "id": "sub1", "type": "subscribe", "payload": {"query": "query { hello }"}, } ) next_message: NextMessage = await ws.receive_json() assert_next(next_message, "sub1", {"hello": "Hello world"}) complete_message: CompleteMessage = await ws.receive_json() assert complete_message == {"id": "sub1", "type": "complete"} async def test_single_result_query_operation_async(ws: WebSocketClient): """Test a single result query operation on an `async` method in the schema, including an artificial async delay. """ await ws.send_message( { "id": "sub1", "type": "subscribe", "payload": {"query": 'query { asyncHello(name: "Dolly", delay:0.01)}'}, } ) next_message: NextMessage = await ws.receive_json() assert_next(next_message, "sub1", {"asyncHello": "Hello Dolly"}) complete_message: CompleteMessage = await ws.receive_json() assert complete_message == {"id": "sub1", "type": "complete"} async def test_single_result_query_operation_overlapped(ws: WebSocketClient): """Test that two single result queries can be in flight at the same time, just like regular queries. Start two queries with separate ids. The first query has a delay, so we expect the message to the second query to be delivered first. """ # first query await ws.send_message( { "id": "sub1", "type": "subscribe", "payload": {"query": 'query { asyncHello(name: "Dolly", delay:1)}'}, } ) # second query await ws.send_message( { "id": "sub2", "type": "subscribe", "payload": {"query": 'query { asyncHello(name: "Dolly", delay:0)}'}, } ) # we expect the message to the second query to arrive first next_message: NextMessage = await ws.receive_json() assert_next(next_message, "sub2", {"asyncHello": "Hello Dolly"}) complete_message: CompleteMessage = await ws.receive_json() assert complete_message == {"id": "sub2", "type": "complete"} async def test_single_result_mutation_operation(ws: WebSocketClient): await ws.send_message( { "id": "sub1", "type": "subscribe", "payload": {"query": "mutation { hello }"}, } ) next_message: NextMessage = await ws.receive_json() assert_next(next_message, "sub1", {"hello": "strawberry"}) complete_message: CompleteMessage = await ws.receive_json() assert complete_message == {"id": "sub1", "type": "complete"} @pytest.mark.parametrize( ("extra_payload", "expected_message"), [ ({}, "Hello Strawberry1"), ({"operationName": None}, "Hello Strawberry1"), ({"operationName": "Query1"}, "Hello Strawberry1"), ({"operationName": "Query2"}, "Hello Strawberry2"), ], ) async def test_single_result_operation_selection( ws: WebSocketClient, extra_payload, expected_message ): query = """ query Query1 { hello(name: "Strawberry1") } query Query2 { hello(name: "Strawberry2") } """ await ws.send_json( { "id": "sub1", "type": "subscribe", "payload": {"query": query, **extra_payload}, } ) next_message: NextMessage = await ws.receive_json() assert_next(next_message, "sub1", {"hello": expected_message}) complete_message: CompleteMessage = await ws.receive_json() assert complete_message == {"id": "sub1", "type": "complete"} @pytest.mark.parametrize( "operation_name", ["", "Query2"], ) async def test_single_result_invalid_operation_selection( ws: WebSocketClient, operation_name ): query = """ query Query1 { hello } """ await ws.send_message( { "id": "sub1", "type": "subscribe", "payload": {"query": query, "operationName": operation_name}, } ) await ws.receive(timeout=2) assert ws.closed assert ws.close_code == 4400 assert ws.close_reason == f'Unknown operation named "{operation_name}".' async def test_single_result_operation_selection_without_operations( ws: WebSocketClient, ): await ws.send_message( { "id": "sub1", "type": "subscribe", "payload": { "query": """ fragment Fragment1 on Query { __typename } """, }, } ) await ws.receive(timeout=2) assert ws.closed assert ws.close_code == 4400 assert ws.close_reason == "Can't get GraphQL operation type" async def test_single_result_execution_error(ws: WebSocketClient): process_errors = Mock() with patch.object(Schema, "process_errors", process_errors): await ws.send_message( { "id": "sub1", "type": "subscribe", "payload": { "query": "query { alwaysFail }", }, } ) next_message: NextMessage = await ws.receive_json() assert next_message["type"] == "next" assert next_message["id"] == "sub1" assert "errors" in next_message["payload"] payload_errors = next_message["payload"]["errors"] assert payload_errors is not None assert len(payload_errors) == 1 assert "path" in payload_errors[0] assert payload_errors[0]["path"] == ["alwaysFail"] assert "message" in payload_errors[0] assert payload_errors[0]["message"] == "You are not authorized" process_errors.assert_called_once() async def test_single_result_pre_execution_error(ws: WebSocketClient): """Test that single-result-operations which raise exceptions behave in the same way as streaming operations. """ process_errors = Mock() with patch.object(Schema, "process_errors", process_errors): await ws.send_message( { "id": "sub1", "type": "subscribe", "payload": { "query": "query { IDontExist }", }, } ) error_message: ErrorMessage = await ws.receive_json() assert error_message["type"] == "error" assert error_message["id"] == "sub1" assert len(error_message["payload"]) == 1 assert "message" in error_message["payload"][0] assert ( error_message["payload"][0]["message"] == "Cannot query field 'IDontExist' on type 'Query'." ) process_errors.assert_called_once() async def test_single_result_duplicate_ids_sub(ws: WebSocketClient): """Test that single-result-operations and streaming operations share the same ID namespace. Start a regular subscription, then issue a single-result operation with same ID and expect an error due to already existing ID """ # regular subscription await ws.send_message( { "id": "sub1", "type": "subscribe", "payload": {"query": 'subscription { echo(message: "Hi", delay: 5) }'}, } ) # single result subscription with duplicate id await ws.send_message( { "id": "sub1", "type": "subscribe", "payload": { "query": "query { hello }", }, } ) await ws.receive(timeout=2) assert ws.closed assert ws.close_code == 4409 assert ws.close_reason == "Subscriber for sub1 already exists" async def test_single_result_duplicate_ids_query(ws: WebSocketClient): """Test that single-result-operations don't allow duplicate IDs for two asynchronous queries. Issue one async query with delay, then another with same id. Expect error. """ # single result subscription 1 await ws.send_message( { "id": "sub1", "type": "subscribe", "payload": {"query": 'query { asyncHello(name: "Hi", delay: 5) }'}, } ) # single result subscription with duplicate id await ws.send_message( { "id": "sub1", "type": "subscribe", "payload": { "query": "query { hello }", }, } ) # We expect the remote to close the socket due to duplicate ID in use await ws.receive(timeout=2) assert ws.closed assert ws.close_code == 4409 assert ws.close_reason == "Subscriber for sub1 already exists" async def test_injects_connection_params(ws_raw: WebSocketClient): ws = ws_raw await ws.send_message( {"type": "connection_init", "payload": {"strawberry": "rocks"}} ) connection_ack_message: ConnectionAckMessage = await ws.receive_json() assert connection_ack_message == {"type": "connection_ack"} await ws.send_message( { "id": "sub1", "type": "subscribe", "payload": {"query": "subscription { connectionParams }"}, } ) next_message: NextMessage = await ws.receive_json() assert_next(next_message, "sub1", {"connectionParams": {"strawberry": "rocks"}}) await ws.send_message({"id": "sub1", "type": "complete"}) async def test_rejects_connection_params_not_dict(ws_raw: WebSocketClient): ws = ws_raw await ws.send_json({"type": "connection_init", "payload": "gonna fail"}) await ws.receive(timeout=2) assert ws.closed assert ws.close_code == 4400 assert ws.close_reason == "Invalid connection init payload" @pytest.mark.parametrize( "payload", [[], "invalid value", 1], ) async def test_rejects_connection_params_with_wrong_type( payload: object, ws_raw: WebSocketClient ): ws = ws_raw await ws.send_json({"type": "connection_init", "payload": payload}) await ws.receive(timeout=2) assert ws.closed assert ws.close_code == 4400 assert ws.close_reason == "Invalid connection init payload" # timings can sometimes fail currently. Until this test is rewritten when # generator based subscriptions are implemented, mark it as flaky @pytest.mark.xfail(reason="This test is flaky, see comment above") async def test_subsciption_cancel_finalization_delay(ws: WebSocketClient): # Test that when we cancel a subscription, the websocket isn't blocked # while some complex finalization takes place. delay = 0.1 await ws.send_message( { "id": "sub1", "type": "subscribe", "payload": {"query": f"subscription {{ longFinalizer(delay: {delay}) }}"}, } ) next_message: NextMessage = await ws.receive_json() assert_next(next_message, "sub1", {"longFinalizer": "hello"}) # now cancel the stubscription and send a new query. We expect the message # to the new query to arrive immediately, without waiting for the finalizer start = time.time() await ws.send_message({"id": "sub1", "type": "complete"}) await ws.send_message( { "id": "sub2", "type": "subscribe", "payload": {"query": "query { hello }"}, } ) while True: next_or_complete_message: ( NextMessage | CompleteMessage ) = await ws.receive_json() assert next_or_complete_message["type"] in ("next", "complete") if next_or_complete_message["id"] == "sub2": break end = time.time() elapsed = end - start assert elapsed < delay async def test_error_handler_for_timeout(http_client: HttpClient): """Test that the error handler is called when the timeout task encounters an error. """ with contextlib.suppress(ImportError): from tests.http.clients.channels import ChannelsHttpClient if isinstance(http_client, ChannelsHttpClient): pytest.skip("Can't patch on_init for this client") if not AsyncMock: pytest.skip("Don't have AsyncMock") ws = ws_raw handler = None errorhandler = AsyncMock() def on_init(_handler): nonlocal handler if handler: return handler = _handler # patch the object handler.handle_task_exception = errorhandler # cause an attribute error in the timeout task handler.connection_init_wait_timeout = None with patch.object(DebuggableGraphQLTransportWSHandler, "on_init", on_init): async with http_client.ws_connect( "/graphql", protocols=[GRAPHQL_TRANSPORT_WS_PROTOCOL] ) as ws: await asyncio.sleep(0.01) # wait for the timeout task to start await ws.send_message({"type": "connection_init"}) connection_ack_message: ConnectionAckMessage = await ws.receive_json() assert connection_ack_message == {"type": "connection_ack"} await ws.close() # the error hander should have been called assert handler errorhandler.assert_called_once() args = errorhandler.call_args assert isinstance(args[0][0], AttributeError) assert "total_seconds" in str(args[0][0]) async def test_subscription_errors_continue(ws: WebSocketClient): """Verify that an ExecutionResult with errors during subscription does not terminate the subscription. """ process_errors = Mock() with patch.object(Schema, "process_errors", process_errors): await ws.send_message( { "id": "sub1", "type": "subscribe", "payload": { "query": "subscription { flavorsInvalid }", }, } ) next_message1: NextMessage = await ws.receive_json() assert next_message1["type"] == "next" assert next_message1["id"] == "sub1" assert "data" in next_message1["payload"] assert next_message1["payload"]["data"] == {"flavorsInvalid": "VANILLA"} next_message2: NextMessage = await ws.receive_json() assert next_message2["type"] == "next" assert next_message2["id"] == "sub1" assert "data" in next_message2["payload"] assert next_message2["payload"]["data"] is None assert "errors" in next_message2["payload"] assert "cannot represent value" in str(next_message2["payload"]["errors"]) process_errors.assert_called_once() next_message3: NextMessage = await ws.receive_json() assert next_message3["type"] == "next" assert next_message3["id"] == "sub1" assert "data" in next_message3["payload"] assert next_message3["payload"]["data"] == {"flavorsInvalid": "CHOCOLATE"} complete_message: CompleteMessage = await ws.receive_json() assert complete_message["type"] == "complete" assert complete_message["id"] == "sub1" @patch.object(MyExtension, MyExtension.get_results.__name__, return_value={}) async def test_no_extensions_results_wont_send_extensions_in_payload( mock: Mock, ws: WebSocketClient ): await ws.send_message( { "id": "sub1", "type": "subscribe", "payload": {"query": 'subscription { echo(message: "Hi") }'}, } ) next_message: NextMessage = await ws.receive_json() mock.assert_called_once() assert_next(next_message, "sub1", {"echo": "Hi"}) assert "extensions" not in next_message["payload"] async def test_unexpected_client_disconnects_are_gracefully_handled( ws: WebSocketClient, ): process_errors = Mock() with patch.object(Schema, "process_errors", process_errors): await ws.send_message( { "id": "sub1", "type": "subscribe", "payload": {"query": 'subscription { infinity(message: "Hi") }'}, } ) await ws.receive(timeout=2) assert Subscription.active_infinity_subscriptions == 1 await ws.close() await asyncio.sleep(1) assert not process_errors.called assert Subscription.active_infinity_subscriptions == 0 strawberry-graphql-0.287.0/tests/websockets/test_graphql_ws.py000066400000000000000000000660571511033167500246430ustar00rootroot00000000000000from __future__ import annotations import asyncio import json from collections.abc import AsyncGenerator from typing import TYPE_CHECKING from unittest import mock import pytest import pytest_asyncio from strawberry.subscriptions import GRAPHQL_WS_PROTOCOL from strawberry.subscriptions.protocols.graphql_ws.types import ( CompleteMessage, ConnectionAckMessage, ConnectionErrorMessage, ConnectionInitMessage, ConnectionKeepAliveMessage, DataMessage, ErrorMessage, StartMessage, ) from tests.views.schema import MyExtension, Schema, Subscription, schema if TYPE_CHECKING: from tests.http.clients.base import HttpClient, WebSocketClient @pytest_asyncio.fixture async def ws_raw(http_client: HttpClient) -> AsyncGenerator[WebSocketClient, None]: async with http_client.ws_connect( "/graphql", protocols=[GRAPHQL_WS_PROTOCOL] ) as ws: yield ws await ws.close() assert ws.closed @pytest_asyncio.fixture async def ws(ws_raw: WebSocketClient) -> AsyncGenerator[WebSocketClient, None]: ws = ws_raw await ws.send_legacy_message({"type": "connection_init"}) response: ConnectionAckMessage = await ws.receive_json() assert response["type"] == "connection_ack" yield ws await ws.send_legacy_message({"type": "connection_terminate"}) # make sure the WebSocket is disconnected now await ws.receive(timeout=2) # receive close assert ws.closed async def test_simple_subscription(ws: WebSocketClient): await ws.send_legacy_message( { "type": "start", "id": "demo", "payload": { "query": 'subscription { echo(message: "Hi") }', }, } ) data_message: DataMessage = await ws.receive_json() assert data_message["type"] == "data" assert data_message["id"] == "demo" assert data_message["payload"]["data"] == {"echo": "Hi"} await ws.send_legacy_message({"type": "stop", "id": "demo"}) complete_message: CompleteMessage = await ws.receive_json() assert complete_message["type"] == "complete" assert complete_message["id"] == "demo" @pytest.mark.parametrize( ("extra_payload", "expected_message"), [ ({}, "Hi1"), ({"operationName": None}, "Hi1"), ({"operationName": "Subscription1"}, "Hi1"), ({"operationName": "Subscription2"}, "Hi2"), ], ) async def test_operation_selection( ws: WebSocketClient, extra_payload, expected_message ): await ws.send_json( { "type": "start", "id": "demo", "payload": { "query": """ subscription Subscription1 { echo(message: "Hi1") } subscription Subscription2 { echo(message: "Hi2") } """, **extra_payload, }, } ) data_message: DataMessage = await ws.receive_json() assert data_message["type"] == "data" assert data_message["id"] == "demo" assert data_message["payload"]["data"] == {"echo": expected_message} await ws.send_legacy_message({"type": "stop", "id": "demo"}) complete_message: CompleteMessage = await ws.receive_json() assert complete_message["type"] == "complete" assert complete_message["id"] == "demo" @pytest.mark.parametrize( ("operation_name"), ["", "Subscription2"], ) async def test_invalid_operation_selection(ws: WebSocketClient, operation_name): await ws.send_legacy_message( { "type": "start", "id": "demo", "payload": { "query": """ subscription Subscription1 { echo(message: "Hi1") } """, "operationName": operation_name, }, } ) error_message: ErrorMessage = await ws.receive_json() assert error_message["type"] == "error" assert error_message["id"] == "demo" assert error_message["payload"] == { "message": f'Unknown operation named "{operation_name}".' } async def test_operation_selection_without_operations(ws: WebSocketClient): await ws.send_legacy_message( { "type": "start", "id": "demo", "payload": { "query": """ fragment Fragment1 on Query { __typename } """, }, } ) error_message: ErrorMessage = await ws.receive_json() assert error_message["type"] == "error" assert error_message["id"] == "demo" assert error_message["payload"] == {"message": "Can't get GraphQL operation type"} async def test_connections_are_accepted_by_default(ws_raw: WebSocketClient): await ws_raw.send_legacy_message({"type": "connection_init"}) connection_ack_message: ConnectionAckMessage = await ws_raw.receive_json() assert connection_ack_message == {"type": "connection_ack"} await ws_raw.close() assert ws_raw.closed async def test_setting_a_connection_ack_payload(ws_raw: WebSocketClient): await ws_raw.send_legacy_message( { "type": "connection_init", "payload": {"test-accept": True, "ack-payload": {"token": "secret"}}, } ) connection_ack_message: ConnectionAckMessage = await ws_raw.receive_json() assert connection_ack_message == { "type": "connection_ack", "payload": {"token": "secret"}, } await ws_raw.close() assert ws_raw.closed async def test_connection_ack_payload_may_be_unset(ws_raw: WebSocketClient): await ws_raw.send_legacy_message( { "type": "connection_init", "payload": {"test-accept": True}, } ) connection_ack_message: ConnectionAckMessage = await ws_raw.receive_json() assert connection_ack_message == {"type": "connection_ack"} await ws_raw.close() assert ws_raw.closed async def test_a_connection_ack_payload_of_none_is_treated_as_unset( ws_raw: WebSocketClient, ): await ws_raw.send_legacy_message( { "type": "connection_init", "payload": {"test-accept": True, "ack-payload": None}, } ) connection_ack_message: ConnectionAckMessage = await ws_raw.receive_json() assert connection_ack_message == {"type": "connection_ack"} await ws_raw.close() assert ws_raw.closed async def test_rejecting_connection_results_in_error_message_and_socket_closure( ws_raw: WebSocketClient, ): await ws_raw.send_legacy_message( {"type": "connection_init", "payload": {"test-reject": True}} ) connection_error_message: ConnectionErrorMessage = await ws_raw.receive_json() assert connection_error_message == {"type": "connection_error", "payload": {}} await ws_raw.receive(timeout=2) assert ws_raw.closed assert ws_raw.close_code == 1011 assert not ws_raw.close_reason async def test_rejecting_connection_with_custom_connection_error_payload( ws_raw: WebSocketClient, ): await ws_raw.send_legacy_message( { "type": "connection_init", "payload": {"test-reject": True, "err-payload": {"custom": "error"}}, } ) connection_error_message: ConnectionErrorMessage = await ws_raw.receive_json() assert connection_error_message == { "type": "connection_error", "payload": {"custom": "error"}, } await ws_raw.receive(timeout=2) assert ws_raw.closed assert ws_raw.close_code == 1011 assert not ws_raw.close_reason async def test_context_can_be_modified_from_within_on_ws_connect( ws_raw: WebSocketClient, ): await ws_raw.send_legacy_message( { "type": "connection_init", "payload": {"test-modify": True}, } ) connection_ack_message: ConnectionAckMessage = await ws_raw.receive_json() assert connection_ack_message == {"type": "connection_ack"} await ws_raw.send_legacy_message( { "type": "start", "id": "demo", "payload": { "query": "subscription { connectionParams }", }, } ) data_message: DataMessage = await ws_raw.receive_json() assert data_message["type"] == "data" assert data_message["id"] == "demo" assert data_message["payload"]["data"] == { "connectionParams": {"test-modify": True, "modified": True} } await ws_raw.close() assert ws_raw.closed async def test_sends_keep_alive(http_client_class: type[HttpClient]): http_client = http_client_class(schema, keep_alive=True, keep_alive_interval=0.1) async with http_client.ws_connect( "/graphql", protocols=[GRAPHQL_WS_PROTOCOL] ) as ws: await ws.send_legacy_message({"type": "connection_init"}) await ws.send_legacy_message( { "type": "start", "id": "demo", "payload": { "query": 'subscription { echo(message: "Hi", delay: 0.15) }', }, } ) ack_message: ConnectionAckMessage = await ws.receive_json() assert ack_message["type"] == "connection_ack" # we can't be sure how many keep-alives exactly we # get but they should be more than one. keepalive_count = 0 while True: ka_or_data_message: ( ConnectionKeepAliveMessage | DataMessage ) = await ws.receive_json() if ka_or_data_message["type"] == "ka": keepalive_count += 1 else: break assert keepalive_count >= 1 assert ka_or_data_message["type"] == "data" assert ka_or_data_message["id"] == "demo" assert ka_or_data_message["payload"]["data"] == {"echo": "Hi"} complete_message: CompleteMessage = await ws.receive_json() assert complete_message["type"] == "complete" assert complete_message["id"] == "demo" await ws.send_legacy_message({"type": "connection_terminate"}) async def test_subscription_cancellation(ws: WebSocketClient): await ws.send_legacy_message( { "type": "start", "id": "demo", "payload": {"query": 'subscription { echo(message: "Hi", delay: 99) }'}, } ) await ws.send_legacy_message( { "type": "start", "id": "debug1", "payload": { "query": "subscription { debug { numActiveResultHandlers } }", }, } ) data_message: DataMessage = await ws.receive_json() assert data_message["type"] == "data" assert data_message["id"] == "debug1" assert data_message["payload"]["data"] == {"debug": {"numActiveResultHandlers": 2}} complete_message1 = await ws.receive_json() assert complete_message1["type"] == "complete" assert complete_message1["id"] == "debug1" await ws.send_legacy_message({"type": "stop", "id": "demo"}) complete_message2 = await ws.receive_json() assert complete_message2["type"] == "complete" assert complete_message2["id"] == "demo" await ws.send_legacy_message( { "type": "start", "id": "debug2", "payload": { "query": "subscription { debug { numActiveResultHandlers} }", }, } ) data_message2 = await ws.receive_json() assert data_message2["type"] == "data" assert data_message2["id"] == "debug2" assert data_message2["payload"]["data"] == {"debug": {"numActiveResultHandlers": 1}} complete_message3: CompleteMessage = await ws.receive_json() assert complete_message3["type"] == "complete" assert complete_message3["id"] == "debug2" async def test_subscription_errors(ws: WebSocketClient): await ws.send_legacy_message( { "type": "start", "id": "demo", "payload": {"query": 'subscription { error(message: "TEST ERR") }'}, } ) data_message: DataMessage = await ws.receive_json() assert data_message["type"] == "data" assert data_message["id"] == "demo" assert data_message["payload"]["data"] is None assert "errors" in data_message["payload"] assert data_message["payload"]["errors"] is not None assert len(data_message["payload"]["errors"]) == 1 assert "path" in data_message["payload"]["errors"][0] assert data_message["payload"]["errors"][0]["path"] == ["error"] assert "message" in data_message["payload"]["errors"][0] assert data_message["payload"]["errors"][0]["message"] == "TEST ERR" complete_message: CompleteMessage = await ws.receive_json() assert complete_message["type"] == "complete" assert complete_message["id"] == "demo" async def test_subscription_exceptions(ws: WebSocketClient): await ws.send_legacy_message( { "type": "start", "id": "demo", "payload": {"query": 'subscription { exception(message: "TEST EXC") }'}, } ) data_message: DataMessage = await ws.receive_json() assert data_message["type"] == "data" assert data_message["id"] == "demo" assert data_message["payload"]["data"] is None assert "errors" in data_message["payload"] assert data_message["payload"]["errors"] is not None assert data_message["payload"]["errors"] == [{"message": "TEST EXC"}] await ws.send_legacy_message({"type": "stop", "id": "demo"}) complete_message = await ws.receive_json() assert complete_message["type"] == "complete" assert complete_message["id"] == "demo" async def test_subscription_field_error(ws: WebSocketClient): await ws.send_legacy_message( { "type": "start", "id": "invalid-field", "payload": {"query": "subscription { notASubscriptionField }"}, } ) error_message: ErrorMessage = await ws.receive_json() assert error_message["type"] == "error" assert error_message["id"] == "invalid-field" assert error_message["payload"] == { "locations": [{"line": 1, "column": 16}], "message": ( "Cannot query field 'notASubscriptionField' on type 'Subscription'." ), } async def test_subscription_syntax_error(ws: WebSocketClient): await ws.send_legacy_message( { "type": "start", "id": "syntax-error", "payload": {"query": "subscription { example "}, } ) error_message: ErrorMessage = await ws.receive_json() assert error_message["type"] == "error" assert error_message["id"] == "syntax-error" assert error_message["payload"] == { "locations": [{"line": 1, "column": 24}], "message": "Syntax Error: Expected Name, found .", } async def test_non_text_ws_messages_result_in_socket_closure(ws_raw: WebSocketClient): ws = ws_raw await ws.send_bytes( json.dumps(ConnectionInitMessage({"type": "connection_init"})).encode() ) await ws.receive(timeout=2) assert ws.closed assert ws.close_code == 1002 assert ws.close_reason == "WebSocket message type must be text" async def test_non_json_ws_messages_are_ignored(ws_raw: WebSocketClient): ws = ws_raw await ws.send_text("NOT VALID JSON") await ws.send_legacy_message({"type": "connection_init"}) connection_ack_message: ConnectionAckMessage = await ws.receive_json() assert connection_ack_message["type"] == "connection_ack" await ws.send_text("NOT VALID JSON") await ws.send_legacy_message( { "type": "start", "id": "demo", "payload": { "query": 'subscription { echo(message: "Hi") }', }, } ) data_message = await ws.receive_json() assert data_message["type"] == "data" assert data_message["id"] == "demo" assert data_message["payload"]["data"] == {"echo": "Hi"} await ws.send_text("NOT VALID JSON") await ws.send_legacy_message({"type": "stop", "id": "demo"}) complete_message: CompleteMessage = await ws.receive_json() assert complete_message["type"] == "complete" assert complete_message["id"] == "demo" await ws.send_text("NOT VALID JSON") await ws.send_legacy_message({"type": "connection_terminate"}) await ws.receive(timeout=2) # receive close assert ws.closed async def test_ws_message_frame_types_cannot_be_mixed(ws_raw: WebSocketClient): ws = ws_raw await ws.send_legacy_message({"type": "connection_init"}) connection_ack_message: ConnectionAckMessage = await ws.receive_json() assert connection_ack_message["type"] == "connection_ack" await ws.send_bytes( json.dumps( StartMessage( { "type": "start", "id": "demo", "payload": { "query": 'subscription { echo(message: "Hi") }', }, } ) ).encode() ) await ws.receive(timeout=2) assert ws.closed assert ws.close_code == 1002 assert ws.close_reason == "WebSocket message type must be text" async def test_unknown_protocol_messages_are_ignored(ws_raw: WebSocketClient): ws = ws_raw await ws.send_json({"type": "NotAProtocolMessage"}) await ws.send_legacy_message({"type": "connection_init"}) await ws.send_json({"type": "NotAProtocolMessage"}) await ws.send_legacy_message( { "type": "start", "id": "demo", "payload": { "query": 'subscription { echo(message: "Hi") }', }, } ) connection_ack_message: ConnectionAckMessage = await ws.receive_json() assert connection_ack_message["type"] == "connection_ack" data_message = await ws.receive_json() assert data_message["type"] == "data" assert data_message["id"] == "demo" assert data_message["payload"]["data"] == {"echo": "Hi"} await ws.send_json({"type": "NotAProtocolMessage"}) await ws.send_legacy_message({"type": "stop", "id": "demo"}) complete_message: CompleteMessage = await ws.receive_json() assert complete_message["type"] == "complete" assert complete_message["id"] == "demo" await ws.send_json({"type": "NotAProtocolMessage"}) await ws.send_legacy_message({"type": "connection_terminate"}) # make sure the WebSocket is disconnected now await ws.receive(timeout=2) # receive close assert ws.closed async def test_custom_context(ws: WebSocketClient): await ws.send_legacy_message( { "type": "start", "id": "demo", "payload": { "query": "subscription { context }", }, } ) data_message: DataMessage = await ws.receive_json() assert data_message["type"] == "data" assert data_message["id"] == "demo" assert data_message["payload"]["data"] == {"context": "a value from context"} await ws.send_legacy_message({"type": "stop", "id": "demo"}) complete_message: CompleteMessage = await ws.receive_json() assert complete_message["type"] == "complete" assert complete_message["id"] == "demo" async def test_resolving_enums(ws: WebSocketClient): await ws.send_legacy_message( { "type": "start", "id": "demo", "payload": { "query": "subscription { flavors }", }, } ) data_message1: DataMessage = await ws.receive_json() assert data_message1["type"] == "data" assert data_message1["id"] == "demo" assert data_message1["payload"]["data"] == {"flavors": "VANILLA"} data_message2: DataMessage = await ws.receive_json() assert data_message2["type"] == "data" assert data_message2["id"] == "demo" assert data_message2["payload"]["data"] == {"flavors": "STRAWBERRY"} data_message3: DataMessage = await ws.receive_json() assert data_message3["type"] == "data" assert data_message3["id"] == "demo" assert data_message3["payload"]["data"] == {"flavors": "CHOCOLATE"} await ws.send_legacy_message({"type": "stop", "id": "demo"}) complete_message: CompleteMessage = await ws.receive_json() assert complete_message["type"] == "complete" assert complete_message["id"] == "demo" @pytest.mark.xfail(reason="flaky test") async def test_task_cancellation_separation(http_client: HttpClient): # Note Python 3.7 does not support Task.get_name/get_coro so we have to use # repr(Task) to check whether expected tasks are running. # This only works for aiohttp, where we are using the same event loop # on the client side and server. try: from tests.http.clients.aiohttp import AioHttpClient aio = http_client == AioHttpClient # type: ignore except ImportError: aio = False def get_result_handler_tasks(): return [ task for task in asyncio.all_tasks() if "BaseGraphQLWSHandler.handle_async_results" in repr(task) ] connection1 = http_client.ws_connect("/graphql", protocols=[GRAPHQL_WS_PROTOCOL]) connection2 = http_client.ws_connect("/graphql", protocols=[GRAPHQL_WS_PROTOCOL]) async with connection1 as ws1, connection2 as ws2: start_message: StartMessage = { "type": "start", "id": "demo", "payload": {"query": 'subscription { infinity(message: "Hi") }'}, } # 0 active result handler tasks if aio: assert len(get_result_handler_tasks()) == 0 await ws1.send_legacy_message({"type": "connection_init"}) await ws1.send_legacy_message(start_message) await ws1.receive_json() # ack await ws1.receive_json() # data # 1 active result handler tasks if aio: assert len(get_result_handler_tasks()) == 1 await ws2.send_legacy_message({"type": "connection_init"}) await ws2.send_legacy_message(start_message) await ws2.receive_json() await ws2.receive_json() # 2 active result handler tasks if aio: assert len(get_result_handler_tasks()) == 2 await ws1.send_legacy_message({"type": "stop", "id": "demo"}) await ws1.receive_json() # complete # 1 active result handler tasks if aio: assert len(get_result_handler_tasks()) == 1 await ws2.send_legacy_message({"type": "stop", "id": "demo"}) await ws2.receive_json() # complete # 0 active result handler tasks if aio: assert len(get_result_handler_tasks()) == 0 await ws1.send_legacy_message( { "type": "start", "id": "debug1", "payload": { "query": "subscription { debug { numActiveResultHandlers } }", }, } ) data_message: DataMessage = await ws1.receive_json() assert data_message["type"] == "data" assert data_message["id"] == "debug1" # The one active result handler is the one for this debug subscription assert data_message["payload"]["data"] == { "debug": {"numActiveResultHandlers": 1} } complete_message: CompleteMessage = await ws1.receive_json() assert complete_message["type"] == "complete" assert complete_message["id"] == "debug1" async def test_injects_connection_params(http_client: HttpClient): async with http_client.ws_connect( "/graphql", protocols=[GRAPHQL_WS_PROTOCOL] ) as ws: await ws.send_legacy_message( { "type": "connection_init", "payload": {"strawberry": "rocks"}, } ) await ws.send_legacy_message( { "type": "start", "id": "demo", "payload": { "query": "subscription { connectionParams }", }, } ) connection_ack_message: ConnectionAckMessage = await ws.receive_json() assert connection_ack_message["type"] == "connection_ack" data_message: DataMessage = await ws.receive_json() assert data_message["type"] == "data" assert data_message["id"] == "demo" assert data_message["payload"]["data"] == { "connectionParams": {"strawberry": "rocks"} } await ws.send_legacy_message({"type": "stop", "id": "demo"}) complete_message: CompleteMessage = await ws.receive_json() assert complete_message["type"] == "complete" assert complete_message["id"] == "demo" await ws.send_legacy_message({"type": "connection_terminate"}) # make sure the WebSocket is disconnected now await ws.receive(timeout=2) # receive close assert ws.closed async def test_rejects_connection_params(http_client: HttpClient): async with http_client.ws_connect( "/graphql", protocols=[GRAPHQL_WS_PROTOCOL] ) as ws: await ws.send_json( { "type": "connection_init", "id": "demo", "payload": "gonna fail", } ) connection_error_message: ConnectionErrorMessage = await ws.receive_json() assert connection_error_message["type"] == "connection_error" # make sure the WebSocket is disconnected now await ws.receive(timeout=2) # receive close assert ws.closed @mock.patch.object(MyExtension, MyExtension.get_results.__name__, return_value={}) async def test_no_extensions_results_wont_send_extensions_in_payload( mock: mock.MagicMock, http_client: HttpClient ): async with http_client.ws_connect( "/graphql", protocols=[GRAPHQL_WS_PROTOCOL] ) as ws: await ws.send_legacy_message({"type": "connection_init"}) await ws.send_legacy_message( { "type": "start", "id": "demo", "payload": { "query": 'subscription { echo(message: "Hi") }', }, } ) connection_ack_message = await ws.receive_json() assert connection_ack_message["type"] == "connection_ack" data_message: DataMessage = await ws.receive_json() mock.assert_called_once() assert data_message["type"] == "data" assert data_message["id"] == "demo" assert "extensions" not in data_message["payload"] await ws.send_legacy_message({"type": "stop", "id": "demo"}) await ws.receive_json() async def test_unexpected_client_disconnects_are_gracefully_handled( ws_raw: WebSocketClient, ): ws = ws_raw process_errors = mock.Mock() with mock.patch.object(Schema, "process_errors", process_errors): await ws.send_legacy_message({"type": "connection_init"}) connection_ack_message: ConnectionAckMessage = await ws.receive_json() assert connection_ack_message["type"] == "connection_ack" await ws.send_legacy_message( { "type": "start", "id": "sub1", "payload": { "query": 'subscription { infinity(message: "Hi") }', }, } ) await ws.receive_json() assert Subscription.active_infinity_subscriptions == 1 await ws.close() await asyncio.sleep(1) assert not process_errors.called assert Subscription.active_infinity_subscriptions == 0 strawberry-graphql-0.287.0/tests/websockets/test_websockets.py000066400000000000000000000101121511033167500246220ustar00rootroot00000000000000from strawberry.http.async_base_view import AsyncBaseHTTPView from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL from strawberry.subscriptions.protocols.graphql_transport_ws.types import ( ConnectionAckMessage, ) from tests.http.clients.base import HttpClient from tests.views.schema import schema async def test_turning_off_graphql_ws(http_client_class: type[HttpClient]): http_client = http_client_class( schema, subscription_protocols=[GRAPHQL_TRANSPORT_WS_PROTOCOL] ) async with http_client.ws_connect( "/graphql", protocols=[GRAPHQL_WS_PROTOCOL] ) as ws: await ws.receive(timeout=2) assert ws.closed assert ws.close_code == 4406 assert ws.close_reason == "Subprotocol not acceptable" async def test_turning_off_graphql_transport_ws(http_client_class: type[HttpClient]): http_client = http_client_class( schema, subscription_protocols=[GRAPHQL_WS_PROTOCOL] ) async with http_client.ws_connect( "/graphql", protocols=[GRAPHQL_TRANSPORT_WS_PROTOCOL] ) as ws: await ws.receive(timeout=2) assert ws.closed assert ws.close_code == 4406 assert ws.close_reason == "Subprotocol not acceptable" async def test_turning_off_all_subprotocols(http_client_class: type[HttpClient]): http_client = http_client_class(schema, subscription_protocols=[]) async with http_client.ws_connect( "/graphql", protocols=[GRAPHQL_TRANSPORT_WS_PROTOCOL] ) as ws: await ws.receive(timeout=2) assert ws.closed assert ws.close_code == 4406 assert ws.close_reason == "Subprotocol not acceptable" async with http_client.ws_connect( "/graphql", protocols=[GRAPHQL_WS_PROTOCOL] ) as ws: await ws.receive(timeout=2) assert ws.closed assert ws.close_code == 4406 assert ws.close_reason == "Subprotocol not acceptable" async def test_generally_unsupported_subprotocols_are_rejected(http_client: HttpClient): async with http_client.ws_connect( "/graphql", protocols=["imaginary-protocol"] ) as ws: await ws.receive(timeout=2) assert ws.closed assert ws.close_code == 4406 assert ws.close_reason == "Subprotocol not acceptable" async def test_clients_can_prefer_subprotocols(http_client_class: type[HttpClient]): http_client = http_client_class( schema, subscription_protocols=[GRAPHQL_WS_PROTOCOL, GRAPHQL_TRANSPORT_WS_PROTOCOL], ) async with http_client.ws_connect( "/graphql", protocols=[GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL] ) as ws: assert ws.accepted_subprotocol == GRAPHQL_TRANSPORT_WS_PROTOCOL await ws.close() assert ws.closed async with http_client.ws_connect( "/graphql", protocols=[GRAPHQL_WS_PROTOCOL, GRAPHQL_TRANSPORT_WS_PROTOCOL] ) as ws: assert ws.accepted_subprotocol == GRAPHQL_WS_PROTOCOL await ws.close() assert ws.closed async def test_handlers_use_the_views_encode_json_method( http_client: HttpClient, mocker ): spy = mocker.spy(AsyncBaseHTTPView, "encode_json") async with http_client.ws_connect( "/graphql", protocols=[GRAPHQL_TRANSPORT_WS_PROTOCOL] ) as ws: await ws.send_json({"type": "connection_init"}) connection_ack_message: ConnectionAckMessage = await ws.receive_json() assert connection_ack_message == {"type": "connection_ack"} await ws.close() assert ws.closed assert spy.call_count == 1 async def test_handlers_use_the_views_decode_json_method( http_client: HttpClient, mocker ): spy = mocker.spy(AsyncBaseHTTPView, "decode_json") async with http_client.ws_connect( "/graphql", protocols=[GRAPHQL_TRANSPORT_WS_PROTOCOL] ) as ws: await ws.send_message({"type": "connection_init"}) connection_ack_message: ConnectionAckMessage = await ws.receive_json() assert connection_ack_message == {"type": "connection_ack"} await ws.close() assert ws.closed assert spy.call_count == 1 strawberry-graphql-0.287.0/tests/websockets/views.py000066400000000000000000000025511511033167500225570ustar00rootroot00000000000000from strawberry import UNSET from strawberry.exceptions import ConnectionRejectionError from strawberry.http.async_base_view import AsyncBaseHTTPView from strawberry.http.typevars import ( Request, Response, SubResponse, WebSocketRequest, WebSocketResponse, ) from strawberry.types.unset import UnsetType class OnWSConnectMixin( AsyncBaseHTTPView[ Request, Response, SubResponse, WebSocketRequest, WebSocketResponse, dict[str, object], object, ] ): async def on_ws_connect( self, context: dict[str, object] ) -> UnsetType | None | dict[str, object]: connection_params = context["connection_params"] if isinstance(connection_params, dict): if connection_params.get("test-reject"): if "err-payload" in connection_params: raise ConnectionRejectionError(connection_params["err-payload"]) raise ConnectionRejectionError if connection_params.get("test-accept"): if "ack-payload" in connection_params: return connection_params["ack-payload"] return UNSET if connection_params.get("test-modify"): connection_params["modified"] = True return UNSET return await super().on_ws_connect(context)