From c4e334d48e5b01d119681ebe06dd196fa324d1d7 Mon Sep 17 00:00:00 2001 From: ABINFLYCATCH Date: Tue, 4 Feb 2025 11:19:51 +0530 Subject: [PATCH 01/28] initial structure structure --- flycatch_auth/__init__.py | 5 +++ flycatch_auth/auth.py | 10 ++++++ flycatch_auth/identity.py | 10 ++++++ flycatch_auth/jwt_auth.py | 28 +++++++++++++++ setup.py | 29 ++++++++++++++++ tests/__init__.py | 0 tests/test_auth.py | 61 +++++++++++++++++++++++++++++++++ tests/test_flask_integration.py | 45 ++++++++++++++++++++++++ 8 files changed, 188 insertions(+) create mode 100644 flycatch_auth/__init__.py create mode 100644 flycatch_auth/auth.py create mode 100644 flycatch_auth/identity.py create mode 100644 flycatch_auth/jwt_auth.py create mode 100644 setup.py create mode 100644 tests/__init__.py create mode 100644 tests/test_auth.py create mode 100644 tests/test_flask_integration.py diff --git a/flycatch_auth/__init__.py b/flycatch_auth/__init__.py new file mode 100644 index 0000000..53529c7 --- /dev/null +++ b/flycatch_auth/__init__.py @@ -0,0 +1,5 @@ +from .auth import Auth +from .jwt_auth import AuthCoreJwtConfig +from .identity import Identity, IdentityService + +__all__ = ["Auth", "AuthCoreJwtConfig", "Identity", "IdentityService"] diff --git a/flycatch_auth/auth.py b/flycatch_auth/auth.py new file mode 100644 index 0000000..f5f6a15 --- /dev/null +++ b/flycatch_auth/auth.py @@ -0,0 +1,10 @@ +class Auth: + def __init__(self, user_service, credential_checker): + self.user_service = user_service + self.credential_checker = credential_checker + + def authenticate(self, username, password): + user = self.user_service.load_user(username) + if user and self.credential_checker(password, user["password"]): + return user + return None diff --git a/flycatch_auth/identity.py b/flycatch_auth/identity.py new file mode 100644 index 0000000..f9fd20e --- /dev/null +++ b/flycatch_auth/identity.py @@ -0,0 +1,10 @@ +class Identity: + def __init__(self, user_id, username, grants): + self.id = user_id + self.username = username + self.grants = grants + +class IdentityService: + def load_user(self, username: str) -> Identity: + """Override this method to fetch user from DB""" + raise NotImplementedError("Must implement `load_user` method") diff --git a/flycatch_auth/jwt_auth.py b/flycatch_auth/jwt_auth.py new file mode 100644 index 0000000..8718540 --- /dev/null +++ b/flycatch_auth/jwt_auth.py @@ -0,0 +1,28 @@ +import jwt +import datetime + +class AuthCoreJwtConfig: + def __init__(self, enable=True, secret="secret", expiresIn="1h", refresh=True, prefix="/auth/jwt"): + self.enable = enable + self.secret = secret + self.expiresIn = expiresIn + self.refresh = refresh + self.prefix = prefix + +class JWTAuth: + def __init__(self, config: AuthCoreJwtConfig): + self.config = config + + def generate_token(self, user): + payload = { + "sub": user["id"], + "username": user["username"], + "exp": datetime.datetime.utcnow() + datetime.timedelta(hours=1) + } + return jwt.encode(payload, self.config.secret, algorithm="HS256") + + def verify_token(self, token): + try: + return jwt.decode(token, self.config.secret, algorithms=["HS256"]) + except jwt.ExpiredSignatureError: + return None diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..c150e4a --- /dev/null +++ b/setup.py @@ -0,0 +1,29 @@ +from setuptools import setup, find_packages + +setup( + name="flycatch_auth", + version="0.1.0", + description="A Python authentication package supporting Flask and FastAPI", + long_description=open("README.md", encoding="utf-8").read(), + long_description_content_type="text/markdown", + author="Your Name", + author_email="your.email@example.com", + url="https://github.com/yourusername/flycatch_auth", + packages=find_packages(), + install_requires=[ + "flask", + "fastapi", + "pyjwt", + "pydantic" + ], + classifiers=[ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + ], + python_requires=">=3.8", +) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_auth.py b/tests/test_auth.py new file mode 100644 index 0000000..442d057 --- /dev/null +++ b/tests/test_auth.py @@ -0,0 +1,61 @@ +import pytest +from auth_package.auth import AuthCoreJwtConfig +from auth_package.flask_auth import FlaskAuth +from flask import Flask, jsonify + +@pytest.fixture +def app(): + app = Flask(__name__) + app.config["TESTING"] = True + + auth = FlaskAuth() + auth.init_app( + app, + user_service=MockUserService(), + credential_checker=lambda input, user: input == user["password"], + jwt=AuthCoreJwtConfig( + enable=True, + secret="test-secret", + expiresIn="1h", + refresh=True, + prefix="/auth/jwt", + ), + ) + + @app.route("/protected") + @auth.verify() + @auth.has_grants(["read_user"]) + def protected(): + return jsonify({"message": "Access granted"}), 200 + + return app + +@pytest.fixture +def client(app): + return app.test_client() + +class MockUserService: + def load_user(self, username: str): + return { + "id": "1", + "username": "testuser", + "password": "password123", + "grants": ["read_user"], + } + +def test_jwt_token_generation(client): + response = client.post("/auth/jwt/login", json={"username": "testuser", "password": "password123"}) + assert response.status_code == 200 + assert "access_token" in response.json + +def test_protected_route_access(client): + login_response = client.post("/auth/jwt/login", json={"username": "testuser", "password": "password123"}) + token = login_response.json["access_token"] + + response = client.get("/protected", headers={"Authorization": f"Bearer {token}"}) + assert response.status_code == 200 + assert response.json["message"] == "Access granted" + +def test_protected_route_without_token(client): + response = client.get("/protected") + assert response.status_code == 401 diff --git a/tests/test_flask_integration.py b/tests/test_flask_integration.py new file mode 100644 index 0000000..ba61868 --- /dev/null +++ b/tests/test_flask_integration.py @@ -0,0 +1,45 @@ +import pytest +from flask import Flask, jsonify +from auth_package.flask_auth import FlaskAuth +from auth_package.auth import AuthCoreJwtConfig + +@pytest.fixture +def app(): + app = Flask(__name__) + auth = FlaskAuth() + + auth.init_app( + app, + user_service=MockUserService(), + credential_checker=lambda input, user: input == user["password"], + jwt=AuthCoreJwtConfig( + enable=True, + secret="test-secret", + expiresIn="1h", + refresh=True, + prefix="/auth/jwt", + ), + ) + + @app.route("/me") + @auth.verify() + def get_curr_user(): + return jsonify({"name": "Test User"}), 200 + + return app + +@pytest.fixture +def client(app): + return app.test_client() + +class MockUserService: + def load_user(self, username: str): + return {"id": "1", "username": "testuser", "password": "password123", "grants": ["read_user"]} + +def test_me_route_with_auth(client): + login_response = client.post("/auth/jwt/login", json={"username": "testuser", "password": "password123"}) + token = login_response.json["access_token"] + + response = client.get("/me", headers={"Authorization": f"Bearer {token}"}) + assert response.status_code == 200 + assert response.json["name"] == "Test User" From 2c78842742a57f26c1dd0afc6b8326a8ef9862e0 Mon Sep 17 00:00:00 2001 From: ABINFLYCATCH Date: Tue, 4 Feb 2025 12:01:03 +0530 Subject: [PATCH 02/28] documented the modules --- flycatch_auth/__init__.py | 1 + flycatch_auth/auth.py | 3 +++ flycatch_auth/identity.py | 1 + flycatch_auth/jwt_auth.py | 1 + tests/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 164 bytes .../test_auth.cpython-312-pytest-7.4.4.pyc | Bin 0 -> 6119 bytes ...lask_integration.cpython-312-pytest-7.4.4.pyc | Bin 0 -> 3829 bytes 7 files changed, 6 insertions(+) create mode 100644 tests/__pycache__/__init__.cpython-312.pyc create mode 100644 tests/__pycache__/test_auth.cpython-312-pytest-7.4.4.pyc create mode 100644 tests/__pycache__/test_flask_integration.cpython-312-pytest-7.4.4.pyc diff --git a/flycatch_auth/__init__.py b/flycatch_auth/__init__.py index 53529c7..3c296a1 100644 --- a/flycatch_auth/__init__.py +++ b/flycatch_auth/__init__.py @@ -2,4 +2,5 @@ from .jwt_auth import AuthCoreJwtConfig from .identity import Identity, IdentityService +# It will helps to call on top level import these methods __all__ = ["Auth", "AuthCoreJwtConfig", "Identity", "IdentityService"] diff --git a/flycatch_auth/auth.py b/flycatch_auth/auth.py index f5f6a15..e51fd20 100644 --- a/flycatch_auth/auth.py +++ b/flycatch_auth/auth.py @@ -1,9 +1,12 @@ +# module for handle authentication + class Auth: def __init__(self, user_service, credential_checker): self.user_service = user_service self.credential_checker = credential_checker def authenticate(self, username, password): + """Use to authenticate user""" user = self.user_service.load_user(username) if user and self.credential_checker(password, user["password"]): return user diff --git a/flycatch_auth/identity.py b/flycatch_auth/identity.py index f9fd20e..307435e 100644 --- a/flycatch_auth/identity.py +++ b/flycatch_auth/identity.py @@ -1,4 +1,5 @@ class Identity: + """manage user authentication and authorization""" def __init__(self, user_id, username, grants): self.id = user_id self.username = username diff --git a/flycatch_auth/jwt_auth.py b/flycatch_auth/jwt_auth.py index 8718540..8641256 100644 --- a/flycatch_auth/jwt_auth.py +++ b/flycatch_auth/jwt_auth.py @@ -10,6 +10,7 @@ def __init__(self, enable=True, secret="secret", expiresIn="1h", refresh=True, p self.prefix = prefix class JWTAuth: + """generate and verify jwt token""" def __init__(self, config: AuthCoreJwtConfig): self.config = config diff --git a/tests/__pycache__/__init__.cpython-312.pyc b/tests/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bb6907639b12a56ec88caf00256483a921cf34d8 GIT binary patch literal 164 zcmX@j%ge<81R`@5ri19mAOanHW&w&!XQ*V*Wb|9fP{ah}eFmxd<*c8PpPQ7bg9zQ^{GwFdw4B7^Z2gkd;*w(h`1s7c%#!$cy@JYL95%W6 cDWy57c15f}GZ}%n7{vI<%*e=C#0+Es03Wm|R{#J2 literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_auth.cpython-312-pytest-7.4.4.pyc b/tests/__pycache__/test_auth.cpython-312-pytest-7.4.4.pyc new file mode 100644 index 0000000000000000000000000000000000000000..733635fdf56488afc7ca63076279c259193dfbaa GIT binary patch literal 6119 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 UK1KTeccw1AJl`WrF9;F-8;a4;Bme*a literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_flask_integration.cpython-312-pytest-7.4.4.pyc b/tests/__pycache__/test_flask_integration.cpython-312-pytest-7.4.4.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8be66922e2f667a8d7d4a4f9a88c250d222052e7 GIT binary patch literal 3829 zcmb^zTWlN0aqoC{yyHXE!;<_q(>$6BD$$bsDiS$O-88CE6DZ=KHHG$Y<{hP@jy!hn zXjv=?(2W2m$VXD3fCAJl+&>p`fqweapGEsaRUcLt4s4+L$)5}vD4?&-?C~f{aF7=0 z;%;_#c6N4lW_D)&o=ztSv^W0n#|2F$;NSTYz#0hzjs*~O&m?5Mi))bpbQreiCM}^Ul;aV0 zuWyn-i)In6C9IMl8DTBaidhms`M$E=<$FKA-nEKAnxveLX!m;1MOPX)H?xP2M!mB@ zPtiE)TVj*EPxM5-*t;5R!^@ArzcUPA4UTOJmT&~dORp0o7xPT5nZCd5QR`1IWwKMP z)dMEgeVh8oDVyE>Np8`r*twFsQZ$3&Vh)IxdNacW3*-HX2(BKHe zsRs4}H3P@1PS#d-)wf);a@{g7Hb%`_ZSpPGE1Ir<5e6Z808aJ?0kp}z`UB}fVdKKw zMsBW6b^u4NHj;S2gl`wA9WZffv7vDha^>YfmziQ$&FikslrDj0->f;*_J34macY-f zw8)eijGUW#jHU3d2K+aiqRobZrDazG$8?S2qFr3FDT@Ky-B_+-?{h9RCf98i2K~-u z36|&xktwZMNd!$xc_vlOioL5{1<|t~zLdbTO%i*CzW(!TO;!DbCQ&m?W07|ZeP1~?W6jpJh~-ox94xofB5rfq9D)g#E3G2 zU~F50>9#^rnf7IV=VI@@gCyw8DD2L+0N_(7_pbsDTlAX1U_u_^3->dl=sMsLU@5$x zkK4lgG9_H0@R-OcTtfr4$UM>-x#0{C8w9_S95ZjIPHE*C^gu)$R< zaaAfvD=}T7FTpZX(f(+$eeW7=$1fAUYxFn_I17R$-;qFgCC`K@rs6BX{VW+h@~P<= zrcv9pxL@fK6CI1b0o*7?G^W5Ctp+~#Mu-NIZ@VSF+>HgHb84>FWzaZsz-uOZOI%l@ zz_d-a#*W-=JZN>US)HHR8<|o^Epe^nb(x~tSi&$Wo>h0jEm_Jiepxr&&`DX$4=9~M z`Sw2$t{e_thD9DKR>?N_!-8=4<>v%@egr4k=LZj^p1F6j`9=F29fQ6 zNz^O_UmwQbaxa5KYXowfrCM53{2lo{1cm#F9=BLCR>o&?ptU3`*^--5Q^pY8QkshY z=e=xG<{5wg7Aw_MN`jTfJtZ;9G1frP%P?gOrw3Z(={JZ!+Q%Pb?t#{fMWKxLQ=;b` zXnT}Z0nYE`$nuM5rHJb{(8&>6ZPmA*YM(uo#H z_6>AOL}!iO5xEwU)|fTkOm=mW`mc48ia14mS0@_iB+*hbpZHaGke$uq=(O%n`hf{S%_w@7ooHoe!-ccD&s0e4HLvR1 zn5KXh!8C%ijRB}}Y#MML=YbTE=jk!L1s1b#;4pHZ(*20v8Qq8Y2V_$S&g^4wE@W^{ zPcfh zPxG|I2Dr(9U8ox-hh~{YnUg3&sG6z%$(jDiOgIToIM+Y9&`)-{e=^-qcD8?V-p8;6 z6~kT&lgc<(6;vj?;xLw-V3F6BbROux4Zr_40FX>ZkFCxBX1?>%^uu!J7fT!8bw8Wm z9IdqTTdAXuQl~ajr|u4Hq;l^)j`WsRKor)i~VI|H>H(vVY&%`ky0|lEn ixQ(Oj(?nE5yda$D;$ie}MHCi Date: Tue, 4 Feb 2025 14:12:17 +0530 Subject: [PATCH 03/28] test passed for auth with return access token --- .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 374 bytes .../__pycache__/auth.cpython-312.pyc | Bin 0 -> 2164 bytes .../__pycache__/fastapi_auth.cpython-312.pyc | Bin 0 -> 1348 bytes .../__pycache__/flask_auth.cpython-312.pyc | Bin 0 -> 1390 bytes .../__pycache__/identity.cpython-312.pyc | Bin 0 -> 1091 bytes .../__pycache__/jwt_auth.cpython-312.pyc | Bin 0 -> 1958 bytes flycatch_auth/auth.py | 35 +++++++++++- flycatch_auth/fastapi_auth.py | 18 ++++++ flycatch_auth/flask_auth.py | 16 ++++++ flycatch_auth/jwt_auth.py | 30 +++++----- .../test_auth.cpython-312-pytest-7.4.4.pyc | Bin 6119 -> 5524 bytes tests/test_auth.py | 52 ++++++++++-------- 12 files changed, 113 insertions(+), 38 deletions(-) create mode 100644 flycatch_auth/__pycache__/__init__.cpython-312.pyc create mode 100644 flycatch_auth/__pycache__/auth.cpython-312.pyc create mode 100644 flycatch_auth/__pycache__/fastapi_auth.cpython-312.pyc create mode 100644 flycatch_auth/__pycache__/flask_auth.cpython-312.pyc create mode 100644 flycatch_auth/__pycache__/identity.cpython-312.pyc create mode 100644 flycatch_auth/__pycache__/jwt_auth.cpython-312.pyc create mode 100644 flycatch_auth/fastapi_auth.py create mode 100644 flycatch_auth/flask_auth.py diff --git a/flycatch_auth/__pycache__/__init__.cpython-312.pyc b/flycatch_auth/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a97baea5f3e71933f78761c064824ff853ad7fdc GIT binary patch literal 374 zcmZ8cyH3L}6!lGO1yr?zTo!Q%{ z>q6c0tca*`u|QD^o&_J_Dhdrvv8WGCc++auTf1gLzHa+FFacw!5RAQ$C9aUwWn49~ zh$o`UQk_+C+o;=L*LTIXaFpRoM-!1w|EG-cnrp@m@48iZESr2LiW9Yq-S)%NA;PB% W0CO^&lY@5>fBR(n3?2ftgV7Hg&}BdX literal 0 HcmV?d00001 diff --git a/flycatch_auth/__pycache__/auth.cpython-312.pyc b/flycatch_auth/__pycache__/auth.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e2b764f7c86de85cb4c2c0ae7665261a0d4600d2 GIT binary patch literal 2164 zcmZuyO-vg{6rR~1vw*<{TP#r2ET~CYlGs$Gzlu^(B>`FqRiZyChiq57YcLp_&MqKZ zj>G|}j?_x&p@Jj55pn6U#~v%?Vhbv}t5mAgQ*RNbQk7HRn_U~DGSa?zZ)WGso9}yX z=2>4~4}tdOuRpGgM+o@~jaHLGrFjFCePR=v=Ew$Jrj!U|j@ZH;u|!I8YV;jkp^;Y(qF(CZ+-q@3S_2kW3^5)U}PxjC6*WP|`nw)C@ zq#rk8iT`7Y>B%wO1chVTE+!ZV8x~3n6OQ0DK(I4u!vC&BRcvD~*|aQW=@Q&3wurqh zm2Th}F9E8Vd0S(Sx5e_>M_+sjpszc5KWbW*vKvoCL0q!Vm6;AOQV% zvK}9=#m5gPkLFL}Z*k-s$>II^@8?7K@0}#ysN8QPCl2pd6I0dbR3oldM^e>Tx~ilD zB08Z)I5`nmci#XN6_TMg-BTXXGD0Pk#y@vhHd71$(J}=V)SQNj>3XxDELfcmky*@_ zW*Ni}bf0+b*~j3x#U~qhOs}jN(&BCD^2&B5@Re89QOC+MfzZW8KgusOW1FAA@6SNI z=4LjubeSK9i8@`4PxGqbOg)8gg4DT=Zjr06!Ze8l`cwbIXh zF?^NarGu9PF9XBydkkZvU~lD6j~d3;TV^hp;a5Gwga~r_Rn%TbfvXm^M4oQO6FfWUq@#XLI_z5P3E$u=4wsd)ft^>vC%YLGxhbj(6n5O zfd<^Xo29$922&6k>kyiE@i)lap>Dyk{vp@VmGvH9=B-ew%JlsF-1}byd^w5QEoFbg zgN}%juESK3LRYvixWJuto!{(mnFLfJH#2kkZ=tgZ99$NHtA%VGTpbx1$cYW^G+3zd zLa1}AVIVWpwY#QMP^AxinM}-mIMGQKg1mjoAlf>9{W`)X^jHr(^aj?`aF-PZ=;^r2 z+63r=!)CC>az}FE;dd(vxT*$%hrE?Ue(VJcJXqpF>5D6gV<h&=&kcBlhKgqYZ4(UyjnN_2sUzJuhlS ziRZ1JI@WtVKc&YZPa~|s{;Q)K#;$#O|MiQsp4~tB(uVQ1-Nzs(OAPc-X3}2bt$=2g z8d;_pyfBY=c%ISHQQk)Cco@?H#>ejh=>!IAxH;h%j{1nXv9jF zi4bSdy943{x-OZMbRATo zycwsctgLrI(@F<9U~wWluOG6Zo>UbT5;0E_Ue8{nOlgYOC_QCWBkz#uGyBq@<8g5g s1N1MJ=MfHc#@OQmu(x;M;yrlt9-RNP^x{{S*6hc|5SwNXfiSJ}FVuiUhyVZp literal 0 HcmV?d00001 diff --git a/flycatch_auth/__pycache__/flask_auth.cpython-312.pyc b/flycatch_auth/__pycache__/flask_auth.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d09d6b87876b289e12b5033c77452fbc1c884f7c GIT binary patch literal 1390 zcmZuwO=ufe5T3U`mh4J#u!NP+)LufVwUo$*HYJ5%Xh|AUr~^$)Ks{`h-6w0M)voh) z6(ga7poS=i2671M(Sblxa%oTPtv#0ZBI7`uCD21py@iOK`5?laV+6G&Gh%bq%a!1kC zKS|W3C?oi_HV?um(+GUesdX&6NQq5^@i7#Jz@f@)s7f~5MmOZPs&r&siDo}?&7k=~ zJFHtN9(qX;WnAM5vLWPe`j0iY+NHbDr`0iCVua+GTaTo|~nI~I=?EWvH= zg^uYOR-Ic-E+VC{)zHHd6YfAfAU@0 zm-h>c1Lf=N7%#z^=8DR+PK&7AfE#QG12%vf6*~K+)RQh_tGaKU;|S0UeW8WEcAX3C zmiFO>Yl&n?0O#iI9l5Zz>J) zdWafS_CwT@8m}EXLe-WIW-;{~%!m2Z(pGHMnZ~qdYM8wQH*r23oS=K`7Q<>})}uU) zn=hQM89Kh#&3wpB!G#tJjsHxIdhTVM-NpaMPrVN2k^S3;zZ7Lwe>Mk4 zOM@eEh;^(N{t(Y>al?q_45Q`SZI|SnVO(pQZgL~2;euvUED`{-{IunN}iRT I1G3one?ON;AOHXW literal 0 HcmV?d00001 diff --git a/flycatch_auth/__pycache__/identity.cpython-312.pyc b/flycatch_auth/__pycache__/identity.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..01a5f0f9fee21cfa71c4ee31d15112921c063804 GIT binary patch literal 1091 zcmZ8fJ8#rL5Z<+Yk38<85Gf=gkrrq$BzGMZqCg~3gg6SiYc`g5FK6LLSnmbh>52$l zkpk*;2s#iFe}pCyO>hf8AYi0OshF|P24$q3nVs30otbaGHX5@8=-!tP`|l_rKk!nJ zHC56LP)>+Jc8DMoBIsu$_jGfc0XDO=(V=Bm+#&}`$eU5R4B~`H(xrlQ4MDr6fY`1z zG;J%X?+O`ePY*{MffG9Wk_}?1m^09Qw7ZV>qL4YE$V`#)Mw!Jes>Cm_E)!1L0(L^i zbWAvCIgJgt6}6DD3AbqmAd`*t4O^lZydX5BinQ%B{zG%6JPau)Am~j!=%o+7kZlW z(PA-`R$;>a;_JYU$l0~kW9v=rY=wbnoGq^%TW{x5aFLE(!6euP=gBPRK_mt~>hqkx z960{eQ{!AjF6U|vf<6&74?^KaDEua4II$Q_6Y|}O^|Ccp}A?QjxL^RfjUNYGbUDv20w)OqpS{dZo2i>6(FnxC!it{A}Jly|wkNxqVvS z&c@lXtEz#i(XSSOC1$KujswE27Lok}XbG>#u%H-+Ga=`OQQ4vIZ=VyCAMgChWFCZ1 wJ5WE)GK;&C@Us3OUy_Gv6`o|U-vH&@q?BG56}tY50J~_Kbcg;X$g{+M0UK%mG5`Po literal 0 HcmV?d00001 diff --git a/flycatch_auth/__pycache__/jwt_auth.cpython-312.pyc b/flycatch_auth/__pycache__/jwt_auth.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..abf7f7276235bc78de49c7579796373747743ce8 GIT binary patch literal 1958 zcmZuxO>7%Q6rTNaY{!X{tkcwmM1dH{QsqZTP!0%aLuivUxg>zC5GqJVI%lGIZRX+}k1ivfUqiaE#n&R^e34Vc zznC(YsF`ZW)s>?G5Gn|xIH#jHuXAfCF0eh~qAmbRMNyGx_r<)Ep2!hAU2rCHR?hyD zd=fjsO8_^~{+Z|3#n0QlDf%L4o%?+QFoj&>7wtz7h!Txdfx>R!ruhn_vx3T;&PBoG zz*id?PiUDljr2B+TXoTCJWHmL_GXmB66lUH%CJ117@77H22gdr$-S-m?%Sd#l~{yy zn_uOf4sUIy)03JrqfDl&se6*{+iu&c-)B{DWY1m}oQNlT%qQJtA=Bqso3rlsPO~bM zg?Xd|Nx3P3+9U73IF^*6v5 z(9Beh7*0A{lJZV+Tlxiw?G_Pshx!h=gUG2J$v0D*Q;JZOdBG%xOB6+>_hvB^kp> zhy`}0>UUFJZ)xx}J)e$XtL#qzZlWhX6zShw*m&naer`{WRpr>8e7-85-;qbY_3w>e zs*Yd!MgDQ&*XuuB-~>A8OOM-oZ{gb??6wc>N1~Oh8&~i2?L?II$^CF-JwzBYo+CoT|-5&rP!WP63w^* zS`n_H0;fo&t`9LWlEo&u$Qjrc$P;Xk$6k0}kw`z7$qNiv<%5UmlFMYN2pzy%>J@oFr-1px>xMwdsTn!H&A}$d9Lq6*oV%2cWGdvN%acr}q z{B-DLbWgt6C4Aol@I#mboP$OU9n;dXSXF6IRkJxgZ!$TgsvqYyv*sbJNf6dB$*TbO zk;@2|*Mza$1t!?t0Bh)wm!$r~&Y;wLbj~kDYthWF2({dv^%jv)CNX}wyB+)As-56% eG6pu*x9na3xCX~PLcvEU{IA&0MGp~xyX=287_tZe literal 0 HcmV?d00001 diff --git a/flycatch_auth/auth.py b/flycatch_auth/auth.py index e51fd20..ad33221 100644 --- a/flycatch_auth/auth.py +++ b/flycatch_auth/auth.py @@ -1,9 +1,20 @@ -# module for handle authentication +from .flask_auth import FlaskAuth +# from .fastapi_auth import FastAPIAuth -class Auth: - def __init__(self, user_service, credential_checker): + +class Auth(FlaskAuth): + def __init__(self, user_service, credential_checker, jwt=None): self.user_service = user_service self.credential_checker = credential_checker + self.jwt = jwt + + # Initialize FlaskAuth and FastAPIAuth + FlaskAuth.__init__(self, user_service, credential_checker, jwt) + # FastAPIAuth.__init__(self, user_service, credrintrint(user["password"]) (user["password"]) ential_checker, jwt) + + def init_app(self, app): + """Initialize Auth with Flask""" + app.auth = self def authenticate(self, username, password): """Use to authenticate user""" @@ -11,3 +22,21 @@ def authenticate(self, username, password): if user and self.credential_checker(password, user["password"]): return user return None + + def login(self, username, password): + """Authenticate user and return JWT token""" + user = self.authenticate(username, password) + if user: + token = self.jwt.generate_token(user) + return {"access_token": token} + return {"error": "Invalid credentials"} + + def verify(self): + """Verify token for Flask or FastAPI""" + def wrapper(func): + if hasattr(self, 'flask_verify'): + return self.flask_verify(func) # Flask verification + elif hasattr(self, 'fastapi_verify'): + return self.fastapi_verify(func) # FastAPI verification + return func + return wrapper diff --git a/flycatch_auth/fastapi_auth.py b/flycatch_auth/fastapi_auth.py new file mode 100644 index 0000000..80f02fa --- /dev/null +++ b/flycatch_auth/fastapi_auth.py @@ -0,0 +1,18 @@ +# This file contains the FastAPIAuth class which is used to verify JWT tokens in FastAPI + +from fastapi import Depends, HTTPException, Security +from fastapi.security import HTTPBearer +from starlette.requests import Request + +security = HTTPBearer() + +class FastAPIAuth: + def __init__(self, user_service, credential_checker, jwt): + self.user_service = user_service + self.credential_checker = credential_checker + self.jwt = jwt + + async def fastapi_verify(self, request: Request, token: str = Security(security)): + """Verify JWT token in FastAPI""" + if not token.credentials or not self.jwt.verify_token(token.credentials): + raise HTTPException(status_code=401, detail="Unauthorized") diff --git a/flycatch_auth/flask_auth.py b/flycatch_auth/flask_auth.py new file mode 100644 index 0000000..2e3db90 --- /dev/null +++ b/flycatch_auth/flask_auth.py @@ -0,0 +1,16 @@ +from flask import request, jsonify + +class FlaskAuth: + def __init__(self, user_service, credential_checker, jwt): + self.user_service = user_service + self.credential_checker = credential_checker + self.jwt = jwt + + def flask_verify(self, func): + """Decorator to verify JWT token in Flask""" + def wrapper(*args, **kwargs): + token = request.headers.get("Authorization") + if not token or not self.jwt.verify_token(token.replace("Bearer ", "")): + return jsonify({"error": "Unauthorized"}), 401 + return func(*args, **kwargs) + return wrapper diff --git a/flycatch_auth/jwt_auth.py b/flycatch_auth/jwt_auth.py index 8641256..bdc2e2d 100644 --- a/flycatch_auth/jwt_auth.py +++ b/flycatch_auth/jwt_auth.py @@ -2,28 +2,32 @@ import datetime class AuthCoreJwtConfig: - def __init__(self, enable=True, secret="secret", expiresIn="1h", refresh=True, prefix="/auth/jwt"): + def __init__(self, enable, secret, expiresIn, refresh, prefix): self.enable = enable self.secret = secret self.expiresIn = expiresIn self.refresh = refresh self.prefix = prefix -class JWTAuth: - """generate and verify jwt token""" - def __init__(self, config: AuthCoreJwtConfig): - self.config = config - - def generate_token(self, user): + def generate_token(self, user_data): + """Generate JWT token""" payload = { - "sub": user["id"], - "username": user["username"], - "exp": datetime.datetime.utcnow() + datetime.timedelta(hours=1) + "sub": user_data["id"], + "exp": datetime.datetime.utcnow() + datetime.timedelta(hours=int(self.expiresIn.replace("h", ""))), } - return jwt.encode(payload, self.config.secret, algorithm="HS256") + token = jwt.encode(payload, self.secret, algorithm="HS256") + + if isinstance(token, bytes): + token = token.decode("utf-8") + return token + def verify_token(self, token): + """Verify JWT token""" try: - return jwt.decode(token, self.config.secret, algorithms=["HS256"]) + jwt.decode(token, self.secret, algorithms=["HS256"]) + return True except jwt.ExpiredSignatureError: - return None + return False + except jwt.InvalidTokenError: + return False diff --git a/tests/__pycache__/test_auth.cpython-312-pytest-7.4.4.pyc b/tests/__pycache__/test_auth.cpython-312-pytest-7.4.4.pyc index 733635fdf56488afc7ca63076279c259193dfbaa..17504c28883583ab9382becbe396187c0c345bee 100644 GIT binary patch delta 2359 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 UK1KTeccw1AJl`WrF9;F-8;a4;Bme*a diff --git a/tests/test_auth.py b/tests/test_auth.py index 442d057..f235d01 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -1,18 +1,29 @@ import pytest -from auth_package.auth import AuthCoreJwtConfig -from auth_package.flask_auth import FlaskAuth from flask import Flask, jsonify +from flycatch_auth import Auth, AuthCoreJwtConfig + + +class MockUserService: + def load_user(self, username: str): + return { + "id": "1", + "username": "testuser", + "password": "password123", + "grants": ["read_user"], + } + + +print("credential_checker.......") + @pytest.fixture def app(): app = Flask(__name__) app.config["TESTING"] = True - auth = FlaskAuth() - auth.init_app( - app, + auth = Auth( user_service=MockUserService(), - credential_checker=lambda input, user: input == user["password"], + credential_checker=lambda input, user: input == user, jwt=AuthCoreJwtConfig( enable=True, secret="test-secret", @@ -22,40 +33,37 @@ def app(): ), ) + auth.init_app(app) + + @app.route("/auth/jwt/login", methods=["POST"]) + def login(): + return auth.login("testuser", "password123") + @app.route("/protected") @auth.verify() - @auth.has_grants(["read_user"]) def protected(): return jsonify({"message": "Access granted"}), 200 return app + @pytest.fixture def client(app): return app.test_client() -class MockUserService: - def load_user(self, username: str): - return { - "id": "1", - "username": "testuser", - "password": "password123", - "grants": ["read_user"], - } def test_jwt_token_generation(client): - response = client.post("/auth/jwt/login", json={"username": "testuser", "password": "password123"}) + response = client.post("/auth/jwt/login") + print(response.json) assert response.status_code == 200 assert "access_token" in response.json + def test_protected_route_access(client): - login_response = client.post("/auth/jwt/login", json={"username": "testuser", "password": "password123"}) + login_response = client.post("/auth/jwt/login") token = login_response.json["access_token"] - response = client.get("/protected", headers={"Authorization": f"Bearer {token}"}) + response = client.get( + "/protected", headers={"Authorization": f"Bearer {token}"}) assert response.status_code == 200 assert response.json["message"] == "Access granted" - -def test_protected_route_without_token(client): - response = client.get("/protected") - assert response.status_code == 401 From 44127618fc42c48a3a55c22def1b37244d587d82 Mon Sep 17 00:00:00 2001 From: ABINFLYCATCH Date: Tue, 4 Feb 2025 17:49:04 +0530 Subject: [PATCH 04/28] added poetry deps on project and tested passed for jwt access token --- .../__pycache__/jwt_auth.cpython-312.pyc | Bin 1958 -> 3053 bytes flycatch_auth/jwt_auth.py | 24 ++++++++++ poetry.lock | 7 +++ pyproject.toml | 22 ++++++++++ .../test_auth.cpython-312-pytest-7.4.4.pyc | Bin 5524 -> 8781 bytes tests/test_auth.py | 41 ++++++++++++++---- 6 files changed, 86 insertions(+), 8 deletions(-) create mode 100644 poetry.lock create mode 100644 pyproject.toml diff --git a/flycatch_auth/__pycache__/jwt_auth.cpython-312.pyc b/flycatch_auth/__pycache__/jwt_auth.cpython-312.pyc index abf7f7276235bc78de49c7579796373747743ce8..082b3e88af4b29a12b433c8fdb871f35c516b363 100644 GIT binary patch delta 982 zcmZvaO=uHA6o7YjcQ%`tY^q7yq)pnbt==%J=B!lwuxz4XSZU) z)>iDnqh$myo%81YBhJ72#3>w(D)7_$p>)Z`V!;HSNl=8iPGKrA%W*fajJyqTojO7_y(C;nT z_|jB*c%+JKf37&UNb=_V!Y+L)=%`A&#T*LJ2jYvdi-_Z%awem@G_=`}(Y{-2->ro- zKF*9yo}R*qt^)R4|LvO%(G+MICq^~lmFPvSFqMX%@hiOP_Z$jNncm6WE#Be^L3$NO zXi`vm!=Ba>%ob1HB zs=0VuFIAHHl9AVRb*^DD-B3?EY68#y^f9E6Eo!;4L89zH+5yEFumh9<8!wT3$t0a{ zl>j6GDxe1dG00IsFQ5-_jKL5YRJKTv33l7Tc~tu8EwS@NQ*JO8;~~Qu_gxNcDJN_4 z;ID3&)>-aWzzH*%2l;T}Y6`uk3vw(99S5h+lAq}iTI%E(q0vKhe zazp_cT1ue74&d(^Fram=&SRXcD*@bEk0}hQjN>~|2Gi-k delta 319 zcmaDWzKoymG%qg~0}!ZRTA2QXc_LpDquj(od6r5RP1cPI+!+~JH@{%q!>Fjqbc?yT zH0c&|YDK{G?&OB^kL@laDf|GHOoNW=Uk^ncT~=WHKMeepxY~ z!9ZnPaAn0IZknt`vOwV?#mTQYj5sx!inu{MmC34{wycssX7S{7PG6Z~5g@ZkS&d@QI0)RrV7f z7pv%JDGpXih-#3$CNsn(5-?3gdXv|2OQ`Du`HVnZED9t(Ff%eT-e%ys&A|ViMV3+W K69bR{n*ji{6Gf2# diff --git a/flycatch_auth/jwt_auth.py b/flycatch_auth/jwt_auth.py index bdc2e2d..1036bcd 100644 --- a/flycatch_auth/jwt_auth.py +++ b/flycatch_auth/jwt_auth.py @@ -1,6 +1,7 @@ import jwt import datetime + class AuthCoreJwtConfig: def __init__(self, enable, secret, expiresIn, refresh, prefix): self.enable = enable @@ -21,6 +22,19 @@ def generate_token(self, user_data): token = token.decode("utf-8") return token + def generate_refresh_token(self, user_data): + """Generate JWT refresh token""" + print("inside generate_refresh_token") + print(user_data) + payload = { + "sub": user_data["id"], + "exp": datetime.datetime.utcnow() + datetime.timedelta(days=7), + } + token = jwt.encode(payload, self.secret, algorithm="HS256") + + if isinstance(token, bytes): + token = token.decode("utf-8") + return token def verify_token(self, token): """Verify JWT token""" @@ -31,3 +45,13 @@ def verify_token(self, token): return False except jwt.InvalidTokenError: return False + + def verify_refresh_token(self, token): + """Verify JWT refresh token""" + try: + jwt.decode(token, self.secret, algorithms=["HS256"]) + return True + except jwt.ExpiredSignatureError: + return False + except jwt.InvalidTokenError: + return False diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..3af4d0e --- /dev/null +++ b/poetry.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +package = [] + +[metadata] +lock-version = "2.0" +python-versions = "^3.12" +content-hash = "34e39677d8527182346093002688d17a5d2fc204b9eb3e094b2e6ac519028228" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b81850b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,22 @@ +[tool.poetry] +name = "flycatch_auth" +version = "0.1.0" +description = "Authentication package for Flask and FastAPI with JWT support" +authors = ["Your Name "] +license = "MIT" +packages = [{ include = "flycatch_auth" }] + +[tool.poetry.dependencies] +python = "^3.8" +flask = "^2.1" +fastapi = "^0.78" +pyjwt = "^2.4" +pytest = "^7.0" # For testing + +[tool.poetry.dev-dependencies] +pytest-cov = "^3.0" # For coverage reports +black = "^22.3.0" # For formatting + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/__pycache__/test_auth.cpython-312-pytest-7.4.4.pyc b/tests/__pycache__/test_auth.cpython-312-pytest-7.4.4.pyc index 17504c28883583ab9382becbe396187c0c345bee..883639f7479d2fbf352beadf87def32d1306528c 100644 GIT binary patch literal 8781 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 o>GCYH7e0-~u1_*Wj9b!S?(-R`b4 zy9q6`VTmN#sA11Xsb=XWznbwxt zxbPSE7H!2n&`Ua-FBU37(Kaoc16@3BMcAUSUTcPljW;fnkBeUkpOnhrlU)%$3<;nN z!*g>J;B9)E%(1+p9W$uGE#Ve04iKFmu~d*OKR~AyPbN2!&VnE$i-p5BhNfIQem97- zp=3V4WgwGFB{TNm7T0zI=xw~bEsT7Fu2`ia}$Rax+ukXYITiXy10c_kd9*+;#Zu2PW5f~x+Bldwj8Kj>6n>-Qw zsvJmFk;RlA+Q9ZH!!*Vg6m1)tyAgrdg9wf@X-J(l2$QY276d>Le1Z9)xuF$b>$1=a zxwxh@aao>K8(SOilv;@v@ciy@=%m1CB;0$q3_&B_S^e)_;ta0DJDkH$K|)J{BiNs@ za#O6?;2mL{8Uikjb4z$n7>1|dXFX~wTWs>PcqGP_eJa}v@AH0@df4Y$J8N%N*cV!i zji^dgJj2g|(~`&V8j|BlQ+Fn^8=B6RHJNn>g6zC1dF7JVJ@7Ybf*o&aQw_zDz`4)y zIy^fD#@_}M{{F7frFvhN4eAWh{UcTf+{fx?w}W}Dq9)yf87t(bOsrE5s+Aq7>)xRA z6+S=HgXpz(!xy?*5%M{^Q1RrgbQXS|NjsNi-_^Y-L-}CNtQ~Buews$urTX@cBq$z+ z$G!k4ldC~({^;D%<(AHsU{`t1H9>xT(|p%l*JYt`mA%r~$%aEQW`y>5t_Yjga@^?V zm|LQ3DBMmtHWqFR@`hk|9PVB69+#${itLT>I_hQT!re`hBSPF{M{qp%Bu)8W(hgF^ zP1LE#)~IhST3$mn{El}mL6V!Gb8RvUYCcL>il#fYy{a8TDMnn6!25Wj%s86 z`ZJuHR-X|v!t8f#cPlwUOENr4NrX^G)`3rwf|JpeMNwVXb*aA-NPbw%qfk6dN3 z#xFzIaP>$@>by>vfICR&eG{NtIKRI-pdJcdoL)=2OVtaX!TK&p7h z(?<$L%XE82t$MIxEOdTvqk`!zX<#~M6f@?aRfH-2);8K!c}nRjAt%VI{#6oRB|U$O zUaH&}A;<`!; Date: Tue, 4 Feb 2025 18:06:12 +0530 Subject: [PATCH 05/28] test passed for refresh token --- .../__pycache__/jwt_auth.cpython-312.pyc | Bin 3053 -> 3020 bytes .../test_auth.cpython-312-pytest-7.4.4.pyc | Bin 8781 -> 9870 bytes tests/test_auth.py | 21 +++++++++++++++++- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/flycatch_auth/__pycache__/jwt_auth.cpython-312.pyc b/flycatch_auth/__pycache__/jwt_auth.cpython-312.pyc index 082b3e88af4b29a12b433c8fdb871f35c516b363..48e973d8c6d12d2e03e195a40af8bcab27dab306 100644 GIT binary patch delta 100 zcmaDWeny<{G%qg~0}xcPE=vE&wvkVnl}(S4fnhqsj{#KmtG%qg~0}yzzEJ`jAlB9ha6Ci*N8zbvBJ;d+Nh@Pd%x6&|C>f~*Rg s-?E1=GHPyi;#|haXgv8PR~0KCtLP_%$r;??yk?A&pBR8d5g*VV0B7MPE&u=k diff --git a/tests/__pycache__/test_auth.cpython-312-pytest-7.4.4.pyc b/tests/__pycache__/test_auth.cpython-312-pytest-7.4.4.pyc index 883639f7479d2fbf352beadf87def32d1306528c..731e1c3be2e507a215761f96871905a60d5913ec 100644 GIT binary patch delta 1795 zcmb7_TW}Lq7=X{-k|s^Ex%C1q&Gv$1TcDQG0@4e&YO6yPD?&SJCS*75hLF^=8!nx6 zS~Nq;12ScOkOu_x!H(qtrsLp?W7QXC(3#DInPS92M_zmjgAP7={!L?xGd{QvU;cCc z`#Jxa{BYodgRZ+yryY^+fc2Bni>^6WHMKOi=@mgj5*2rixM=helGqHAxCI)_5E|iS zOH9zi6~4JE4$f0r1gsgfP_#fJUa2gDMtqWu&@b5uD{lSLLR&Nnp&3^9Ff$x`o{c$m ziudU^nQauS|HcHHB>O0Jqr8-8mW@)9D^pgAI!UKdjEBTdet`snj|B1Plz)A#r;z)y*1*YgeNu{Y6 z2Msk^OCcs{;^r=eU;T~w%>04%wz^3s&EV!|qn8Y06{gvs!!|X^t|_xD&8{iaW|q0O z3HustIcP=4AZ{a3aXZ08#X#a@IIc*-%8?0L^0=J9+lemE^$%_9=oG2EJsclRVnrJ} zq2?KNDs>D$4*|A80M}tv#zG>g3DIOKAt@h{`xDDC3kDs4PCyGm-hWcYO7u)J0YJ6g(DGJRafG^ zK(xm*A9Hvf4zTss%dYwY46gOdrhxB@U8!7z}VDd)b!?jdzKmvU!URV`@@3P z)n2tpR2D|oy0R_mQpca(e8M`xY)y@6$KW7gYvtuND z^UYbld45NZZ=Yptilyu@YIf#)yS{8Ud_9JvM~{_Vr}pU%o3x9|Go)|4yL1eA03`F2;0oZ7_7o3k z5EKBIR`q-RQ`^KwKeE_nhR+Tge8c>k^ZnDqIj-Xg?dE!)Fv!wb0IBetUT2@(0FEj= zV!@1-w?yKKoY3$gvi0)fclLIU>d>F^Ur}%AU%0P0VMauosl`Y2kmmw-lI*;?_}DYY zx`&8}-vF2c_o9BOa`V2!us#Af3K#~w4S0uO&>U;rPehTL@NNu_pJ@|Sp}SiEal5FB zG>%-V5_)CuSRK$PEJp!j00j^O90$Y!Cjbco(TNY~p{klDGk+4c#{usGFhB)pfRz4m z)$80e5sxhXS@pSP$7)OPeDG>BX;}Wd8S|lb5sm(+u3;(!CSkxa0#b(A#Q!OMwc|WD zj+KN)o+?}nHnaa{D`pw*!M{nVtBZjxT!yS8i{Au7l(RBlb}XgDH6@{9Mc-22PL1l} z`rScz_t-{|w?>uIS_;eNU=?%1MdN8eM*q5g!@vb1!r-XTU4fw}>LEfI^uF^U60=Br oh}wQa_NT0svOh)y1wW$8X6nw?J*GP;p3EWbE`U_PdP76Y->NpDZU6uP delta 848 zcmb7?O=uHA6vt`8L*;=zk3;=!W}(rZDGf(JnqoR=5^-kigaH~%*?@Be1z!RW_9 z<%2BS2_G@>>FmX(lF~|Fm3EzJ7058rEJdP8YXYYE0!6o`Iw8E>?XnihT2&-5XH6}= zZ+5%1#v+Mr(QKS<&Ccn0xzR0Hqm+;p5w?jdl6YTC$!w`kl=EpPys;osUrkKcPSua0^^&^gOQ^P}%N?NVEs~J+J9G_~m&b_i$OYG0d`2 zc;;P|(mafo54V&o&XcH~LhMKM!RNNU!P6LyAVv{mi1UaEjxiR&@F?tQ_t7gb-0q)= zp@3!BLBv(WB;p!k3K2(KMzQhOjjx08 zKqqXpk4ptU87xPAFRVj#?+5U>y6YP_{nvGvi_5N1qL7~pr55ucrYCu|Ip_%N&@SMH zONh%H+*75a|LLRNzA0syk;$t~)f=yP`P+fli6+=0!a$_pU0|QI#KW`YpMf})y=LQd z!I;k*nH-yfWH3S};Zd+JVB<^7hNN*bUtoImBUIiKUy|XKuqF5%?AUpShxi+Fc;A~M prF5N;BDpKC6ZI2O*Gc$?6^?hg9Rce2E)v1HDe!1>1ZKLr{{UlexJ&>5 diff --git a/tests/test_auth.py b/tests/test_auth.py index 391cbd7..494ef18 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -1,7 +1,7 @@ import pytest from flask import Flask, request, jsonify from flycatch_auth import Auth, AuthCoreJwtConfig - +import jwt class MockUserService: def load_user(self, username: str): @@ -43,6 +43,25 @@ def login(): return jsonify({"access_token": access_token, "refresh_token": refresh_token}), 200 else: return jsonify({"message": "Invalid credentials"}), 401 + @app.route("/auth/jwt/refresh", methods=["POST"]) + def refresh(): + refresh_token = request.json.get("refresh_token") + if not refresh_token or not auth.jwt.verify_refresh_token(refresh_token): + return jsonify({"message": "Invalid refresh token"}), 401 + + # Assuming user ID is stored in the token payload + decoded_token = jwt.decode(refresh_token, auth.jwt.secret, algorithms=["HS256"]) + user_id = decoded_token.get("sub") + + # Load user using MockUserService or actual UserService + user = auth.user_service.load_user(user_id) + + if user: + new_access_token = auth.jwt.generate_token(user) + return jsonify({"access_token": new_access_token}), 200 + else: + return jsonify({"message": "User not found"}), 404 + @app.route("/protected") @auth.verify() From ae4223f7c64240d76ed494a01595f53772f806c5 Mon Sep 17 00:00:00 2001 From: ABINFLYCATCH Date: Wed, 5 Feb 2025 10:25:35 +0530 Subject: [PATCH 06/28] updated the test for flask_integration test --- flycatch_auth/__init__.py | 2 +- .../__pycache__/__init__.cpython-312.pyc | Bin 374 -> 385 bytes .../__pycache__/auth.cpython-312.pyc | Bin 2164 -> 2164 bytes flycatch_auth/fastapi_auth.py | 3 +- .../test_auth.cpython-312-pytest-7.4.4.pyc | Bin 9870 -> 9940 bytes ...k_integration.cpython-312-pytest-7.4.4.pyc | Bin 3829 -> 5130 bytes tests/test_auth.py | 2 + tests/test_flask_integration.py | 40 +++++++++++++----- 8 files changed, 34 insertions(+), 13 deletions(-) diff --git a/flycatch_auth/__init__.py b/flycatch_auth/__init__.py index 3c296a1..aeef13d 100644 --- a/flycatch_auth/__init__.py +++ b/flycatch_auth/__init__.py @@ -3,4 +3,4 @@ from .identity import Identity, IdentityService # It will helps to call on top level import these methods -__all__ = ["Auth", "AuthCoreJwtConfig", "Identity", "IdentityService"] +__all__ = ["Auth", "FlaskAuth","AuthCoreJwtConfig", "Identity", "IdentityService"] diff --git a/flycatch_auth/__pycache__/__init__.cpython-312.pyc b/flycatch_auth/__pycache__/__init__.cpython-312.pyc index a97baea5f3e71933f78761c064824ff853ad7fdc..af9d2299c7311ba242f3ab883ad7b5230b6cd3b5 100644 GIT binary patch delta 58 zcmeyy)X2XxID;7* J*^79AvHH?6nwOW00SI2MUYP!IBJX52mLg^#cjCqsjC_-$809$K85`M)cz{9xMtuwt diff --git a/flycatch_auth/__pycache__/auth.cpython-312.pyc b/flycatch_auth/__pycache__/auth.cpython-312.pyc index e2b764f7c86de85cb4c2c0ae7665261a0d4600d2..42179e35cb64618af429c9379631515f86479a4b 100644 GIT binary patch delta 20 acmew&@I`?8G%qg~0}yn+U9^!qlLG)nE(S;d delta 20 acmew&@I`?8G%qg~0}w=>Sh$fplLG)k`vvj< diff --git a/flycatch_auth/fastapi_auth.py b/flycatch_auth/fastapi_auth.py index 80f02fa..060c5e3 100644 --- a/flycatch_auth/fastapi_auth.py +++ b/flycatch_auth/fastapi_auth.py @@ -11,7 +11,8 @@ def __init__(self, user_service, credential_checker, jwt): self.user_service = user_service self.credential_checker = credential_checker self.jwt = jwt - + print("HI......... , FastAPIAuth") + async def fastapi_verify(self, request: Request, token: str = Security(security)): """Verify JWT token in FastAPI""" if not token.credentials or not self.jwt.verify_token(token.credentials): diff --git a/tests/__pycache__/test_auth.cpython-312-pytest-7.4.4.pyc b/tests/__pycache__/test_auth.cpython-312-pytest-7.4.4.pyc index 731e1c3be2e507a215761f96871905a60d5913ec..bcc3d7f5017c8d6db90399af013422c8a390d364 100644 GIT binary patch delta 926 zcmd6m%WD%+6vpS=d1fY)Oqw)}Op``J)X=mw1|NtUAG=bit`vkI+(|nLPINLRT4uoD z!kr86LXd(Y;wB2p_#X&T(1nYxL~x}$p{5&g<-JXU)s^l%a1Y<%eD|Cm^SHaTxun0< zb(N!W=Ii5?H!t<48Z3mbG!Ni(Skk8e(&S`p0~U!IKa?K`9Cx4dfcaTq=#MmTKG)z$ zWhS|ZFpmW+uA^sC9}Rs0_|Twt|BL`(IpBTd^UVVs7YM%4e(uR*S3_a*_9(tk2OMFp zI_i(6VAa#ei^McJnAAZg*K%26S>Qtsq>xo-#{ YJ8DohP$Vc1CL(ZbI}?MdJ4FZl1QCA1@&Et; delta 429 zcmccO+vm%BnwOW00SH{!7p3=a6uZ^_DW^3`ypF{bd>uutBvs6Dw~kZUrZ+=9tx zB{-Q<1SUU}3+892V60^aVvbto6hRU-3m|E(WY84yn;b6kjFEe@tEes`e;LsA{y<#J z$jHFZz_7VXOqEq26(~?-3L?xvMB3zC(%O?>NvkmCPkyK@%UC*DS0;$H0Vox|xkctM z8)MC659I(hur82V9XYt(TdV~|nRz9e+(m&PlWIT&$PftuLlCnGM3jJt(#d=(hK$LZ z9aORz88auZQ8i{vpZq{If(_)rqK3(KYB5%3jQpRO8JGktj1n*rsvJHWsaqKS?Y*u9~DaYSo49#HQLV*4!mUY5DQn zrERe!KsN#`rw7MC3%RI@8a>I7dg;kW9}DzCl?9|M49Gxx$*D4ApoLDIdCMg!MTT3z zJ@kQ`eV>_mvorH%=Ix(DAwPlU{m1Xq9bQ8Ih6A^Wjn2jrbmoXgG|nIijy?ITkmNa% z6%!uLS&xBeGNzNNm7ItJ*Qw@6Q9%oF1c#EKi$f{YS{%hN8C2MWlF3;*Y+_~dx&Fem z?KAQXfl7O~y?=jmNrLUoE&x@s+P(Kd=~1P2MXO!bm&2)=oZ5f|FqFrZ6$Q1WD7Ifw zvU#m&U_YoR*NUpqSP@K%(rzH25d=L5kTdnU0QGC`0x(0?dCrsO9=!RS;L|}yye;Mp zzyz~LA4kY-VUAevbeh$)+hSv`LM-IZngxf&X|9WE+zhdB1J)e70`x3Niw&wJ?;`F6 z?Q`LdFo}C_Yh~i@#gQ_Z3~gJ6p(bm-G`Hk;56`TS;`GDLUO*RgIm6E3uk&^3 zwNUFoxpNi1Ou#og#f<~7bW7&GZk7_JjTKo-3=n3 zrcF~#>!rw<+;!E+XwjCj%*;K|GMp zf5EEv49^BvdtdvV{A+oo_tB4pgb8L9$7~V9`KIq89OURFIDK>w2xFY1edN@N>~;u@ zq$G=!vbfu8RW-UBelLr}F@Bs|M6!SU7 z+~gz=G>|Q4av4j3Eo=|X7cB@Q*L4b21EgC4X6bu~ehR_608D&wu;~X*4jRRI1egQN zc>riYfB2X3FXGkFCxP&ju5Z=54t%-)#FOCbV8PW$bhUfm>Ym=!NZ<2-6!Jg!`h!yS zd+WW#Va>RG0XWHZ@;v>2OEGaKIofbo2?eHG@g zxhdfSp;)m&3}o?GNg5eAL|{WyfQf`r$Im5ag?$0FVm@Iz;9luBxw|JW8mzuRC#7b_*D9dXSL_oeQV2-MYWzqaYE16HCA8c!yw=6V8IFx43*#ozgLx}9yh z{{OJ9yrXp^%(^~p51ce#S!8F(uvR2^h@wW7fvUu9B6oa@IUtWW-0A*YF%!K+2xDOy0-m>peXWyf-pe+{irez1(-pBTT zP?*MwUQwyNTY*b}0k2b_$$Yi|ve-dbR<0H^hLy>gw&YwB>|Vv3$QKQbp(<%3e-%o0 zEGlfDVyY<}IoKVi*}KD4^+QVY)Q%{rJk6?>0#^b<&9PT@3^9x^6=Erho*SoUSu{H6Xl~?pz4enbB zzOfv9<3Y!AFkb!eX}Ifd=}zhXTMO|=pFY0y_~Y99Kd8md&6eunvFi8N!~-j@=(i@u zSN_Z2U&Odm`$cTobtZZljk8yA+^^rUHh$Nj`VE?a+RxXV!mA^jqn{v+1OF6xF3mk72-ju8%eVV(^*!ScaA7F=_+9JRTR#sU-Wm%W P_j+Ridwwr>nCa%<^9EHv delta 1860 zcmZ`(OKcNI7@pae_uleh3p{?yaiBf0U-Gr7b+)vk+J|Llk+gAK479kj`PLwYSEeTV-=@f zv&-{2=qrI_f73mgg9O5?9;c=}{Z{W&LJWTE-srX#^k5}MrE8W4TqP1kwg@f&S0hLG z(QQJggTMn7J+gcHr-)&=qo*5WL>)ZKfJX;K{}$(gqaddiO5OMZ9|kl1 zL~wxeGg|;N=L){uC_2vRI!52ptuNZOM$`3$rek4;os`W!K9WCKpSAMk>U^o_mQLo$ z6L^4}7RLG$RNn^QG;azwCztn3EaxZMU=@(-!W`KxzngjR6V>M+W&vVWT5K?)ldJM} zm?6K(8_zO9PR0zTFPmnqIBS_E7K5!_vt6^;X!yqAdTHi83h1bXPuV34OH41~degOh z>6C?8rFfrbwnC9s3Xf0&haH;DW--et-V_n$>&#Ko2KUv2#^>_IeEZGYTHnQ2+XwFn z^7+H(4tHejc6!sT^w=NkCvL}wJE?V@RJPMU*y+o3QX`KvNmo}CASNFHO_Wv!$}%k1jnkDF;jQT6U0aMabtFHQJQMnpwmC_<(ZqUrQ>MneQVh$ zFUjQ~`GuT_N7<>WY0lPBvr5zL$4&F2X0aM3-{&0{!xoSi$TByuA*dyj zl~`=iV(lMH$3~uft&X2S66-eBoE28pH4DX%j1-T%46brE{9cV_v&DOSg;i?JY6(bd ziJstztbbdQCpp(iLLV7KaZf5kq%&2SyC#HwsK?dG?>$509ZlE%8^KC~d>{=8IPEGV zV+@nJq)0VLZNu%KJqj1HFN@LSA>#^vhT>_(Py+ROF&hRu*=5-d*3${E^pl1@G#
aN(M<%P>P16cv29Y>ZwkZ1(%`0sWiAgrg|D&h*49bi@KPl zXphoYxB#Vw`q4U1>tc@o59W==&O_2X$Yly=j655neC4#8uhysS8s0{6;_Z~=xS;cb z#-xoduxHYb7fU6}aZI;9W7Y5gd2Ebvc$l<|{ZJ-98hL3wa~~o*69*K$hw?r@Z4sRK zG@H|dA@F;GYfNl8je5PLLg`p|k!v7NOZF zmS&36*7h=c^v&Sq2%eta=d^)5cyeN8d(LfQD|}7Z{DwopfUTL9$MS38e7%k>s%0bM z9HV4~hY%jS3%~;SM86BNcfhtgAb$_W&X1fMdC2ucW0eP7Vuf)lI{+t#atMdJa?1TJ Rac~+w1dOiA65J7t_P=ZPm+k-n diff --git a/tests/test_auth.py b/tests/test_auth.py index 494ef18..22a9e39 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -43,6 +43,7 @@ def login(): return jsonify({"access_token": access_token, "refresh_token": refresh_token}), 200 else: return jsonify({"message": "Invalid credentials"}), 401 + @app.route("/auth/jwt/refresh", methods=["POST"]) def refresh(): refresh_token = request.json.get("refresh_token") @@ -77,6 +78,7 @@ def client(app): def test_jwt_token_generation(client): + print("inside test_jwt_token_generation") response = client.post( "/auth/jwt/login", json={"username": "testuser", "password": "password123"}) assert response.status_code == 200 diff --git a/tests/test_flask_integration.py b/tests/test_flask_integration.py index ba61868..3c2cda3 100644 --- a/tests/test_flask_integration.py +++ b/tests/test_flask_integration.py @@ -1,17 +1,17 @@ import pytest -from flask import Flask, jsonify -from auth_package.flask_auth import FlaskAuth -from auth_package.auth import AuthCoreJwtConfig +from flask import Flask, jsonify, request +from flycatch_auth import Auth, AuthCoreJwtConfig + +class MockUserService: + def load_user(self, username: str): + return {"id": "1", "username": "testuser", "password": "password123", "grants": ["read_user"]} @pytest.fixture def app(): app = Flask(__name__) - auth = FlaskAuth() - - auth.init_app( - app, + auth = Auth( user_service=MockUserService(), - credential_checker=lambda input, user: input == user["password"], + credential_checker=lambda input, user: input == user, jwt=AuthCoreJwtConfig( enable=True, secret="test-secret", @@ -20,6 +20,25 @@ def app(): prefix="/auth/jwt", ), ) + + auth.init_app(app) + + + @app.route("/auth/jwt/login", methods=["POST"]) + def login(): + data = request.json + username = data.get("username") + password = data.get("password") + + user = auth.authenticate(username, password) + if not user: + return jsonify({"message": "Invalid credentials"}), 401 + + access_token = auth.jwt.generate_token(user) + refresh_token = auth.jwt.generate_refresh_token(user) + + return jsonify({"access_token": access_token, "refresh_token": refresh_token}), 200 + @app.route("/me") @auth.verify() @@ -32,12 +51,11 @@ def get_curr_user(): def client(app): return app.test_client() -class MockUserService: - def load_user(self, username: str): - return {"id": "1", "username": "testuser", "password": "password123", "grants": ["read_user"]} + def test_me_route_with_auth(client): login_response = client.post("/auth/jwt/login", json={"username": "testuser", "password": "password123"}) + assert login_response.status_code == 200 token = login_response.json["access_token"] response = client.get("/me", headers={"Authorization": f"Bearer {token}"}) From a2fac18ca257cb9279b0dca295a4324066ecb002 Mon Sep 17 00:00:00 2001 From: ABINFLYCATCH Date: Wed, 5 Feb 2025 15:05:54 +0530 Subject: [PATCH 07/28] added fastapi integration also --- .../__pycache__/fastapi_auth.cpython-312.pyc | Bin 1348 -> 1413 bytes pyproject.toml | 2 +- setup.py | 2 +- ...i_integration.cpython-312-pytest-7.4.4.pyc | Bin 0 -> 3121 bytes tests/test_fastapi_integration.py | 38 ++++++++++++++++++ 5 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 tests/__pycache__/test_fastapi_integration.cpython-312-pytest-7.4.4.pyc create mode 100644 tests/test_fastapi_integration.py diff --git a/flycatch_auth/__pycache__/fastapi_auth.cpython-312.pyc b/flycatch_auth/__pycache__/fastapi_auth.cpython-312.pyc index 37758cb1dc292ef26b4c720d8f476a49bdad8f41..d17e3e8fc3dd2fcd4b8960c25fac3856631954a1 100644 GIT binary patch delta 307 zcmX@Y)ymCxnwOW00SI_sElTHN-pD7#$i)ogg79aT$wrL&7A5Q;c@RipT+Ik!F)~y# zXfpX#NqTtd!GMB}f?Hy7iDQ7LV`)i-Cd=duMs>DZtOZ4xd6WAXr>iLewKg!^kkRO1 zxxyiNL&*e0%ijtN~OxWmDFflu=)ht_5%CT&IuexTYSVGtn#B#J?pv+2!A|a4c0T96nBS4H|O(4<2@QI0qRr@2)@Mxn`5 rSwd9gVXBH0K`glTUmP~M`6;D2sdh!GlX+P+__-K87(X$9NU$ORpddWh delta 221 zcmZqWKElO!nwOW00SJ6{Elj`7w2@DWk&6k)Wd`EUdXtS9_1P;KG#ULgnJ3R=RG)mD zaT=rK7kOX~5f1TMkgpdih5AHgqCQ!fq(md%BVKBYcqW_N)?-bT zqLy%Jg>~5iR}j(k&`; zmzjtz20m*F-*Rav~`0f!&8R~WzQyUg9Uh`=f0v3D50 z*9lYZsc0T$dR6nZEu&(zb<2>YAB67um>`*H3C0^vow#- z;E44_v_vRqSHa6p)3^Ob#6V5thmD11TV{iBU{dF?FY(C2kemi1L>a&)>E}!DzwuH2 zc>l;_zd84(mv>%Y+j-5uU8?OAYW-qq4|pK3DVectbfs;}oNd=buN5F(ubi8s-V7qI~U1yvh0|lr=OaMTi zJbJ^vojoKmKJ;Z9K2?iHWgR4h9 z(1=Jr5ym)6ksu~DUv>~v_8)&;*=ndQr!Jl>;uxX7GEQaioY z$*P;SSlfso&L(R(R{|!@0YjEuYx>t|8!^uvdI8M9 z)Bha)dw30AA{4yj!6`iLrnW*p)HkT5FLx%FT$c$^R$0P42xYnsb7z%F1Jf-IFy*H} zhCu1%IEKZcPMe3Vh;_b}B!kmzE<78At`mq03u9bIP|5)Se7 zJ^Tz%ipcUhfKBpQ?!eEi&5IxBi+88y?o2&?_r!&}&%M$w9qO0L{YQ@Urw{i_vk&s- z5;gaTS;%a@uscP}?Ax!t`RY5b-ANtk&z<=7xnG|9oY2&{J)M|GcQwH7=O;H`j4$eV z7|^mo@!Y?tU6Nd#6~eI$a`b^km9?Y3HeFS|`BOmtD*OlO@mYT6{q;Nf<2%Ojf7gos zQd;kWEts^mQJa8>!=s4>J-VMPaFNlFv5|06NdMX}dG?ul&(L)Gy=J5>NaOK0V>{%$s@#Jl0EjX)n`F`Bcr!XeO~_L=qP2vC$otm_*`R@H)9h zdm8+Wo(>%{yUZ=v<$nM{w&+9c{CSIZo_zddvwg-AxiW}vh*!-im(U%-pt41%XSlh9HbkCcqFzzU0JtpZ;F#WL08&;+GX6JbN< zN2JT#bIrDD-VT=sY^Vx>4V|02VZG^aCJR8eueAIi@*6^$aov!IZLt=%0#C8pt3h}L zx&*d2nYD#eWhg;TE-8DU>3)avkjs)?4SC&(Y^ari(@^agstINdKZ{1@QCED3)1#B8 zMki0klUUR@MA0T_!IQood5UA(JG2}}_+lt@; zrz%|>AoSJPd*xx~+4zNXf&U1oSii)N0qp9O(tG%L`RT+b Date: Wed, 5 Feb 2025 15:20:39 +0530 Subject: [PATCH 08/28] readme just added --- README.md | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 744c051..91cd323 100644 --- a/README.md +++ b/README.md @@ -1 +1,89 @@ -# auth-core-flask \ No newline at end of file +# Flycatch Auth + +Flycatch Auth is a Python authentication package that provides JWT-based authentication with grant-based access control for both Flask and FastAPI applications. + +## Features + +- \*\*JWT authentication + +- \*\*Grant-based access control + +- \*\*Works with Flask and FastAPI + +- \*\*Simple integration with existing user services + +## Installation + +Install flycatch_auth using pip: + +```bash +pip install flycatch-auth +``` + +## Usage + +```python + +from flask import Flask, request, jsonify +from flycatch_auth import Auth, AuthCoreJwtConfig + +class MockUserService: + def load_user(self, username: str): + return { + "id": "1", + "username": "testuser", + "password": "password123", + "grants": ["read_user"], + } + +app = Flask(__name__) + +auth = Auth( + user_service=MockUserService(), + credential_checker=lambda input, user: input == user["password"], + jwt=AuthCoreJwtConfig( + enable=True, + secret="your-secret-key", + expiresIn="1h", + refresh=True, + prefix="/auth/jwt", + ), +) + +auth.init_app(app) + +@app.route("/auth/jwt/login", methods=["POST"]) +def login(): + data = request.json + username = data.get("username") + password = data.get("password") + user = auth.authenticate(username, password) + if not user: + return jsonify({"message": "Invalid credentials"}), 401 + + access_token = auth.jwt.generate_token(user) + refresh_token = auth.jwt.generate_refresh_token(user) + return jsonify({"access_token": access_token, "refresh_token": refresh_token}), 200 + +@app.route("/protected") +@auth.verify() +def protected(): + return jsonify({"message": "Access granted"}), 200 + +if __name__ == "__main__": + app.run(debug=True) +``` + +## Configuration Options + +### JWT Configuration + +```python +jwt_config = { + "enabled": True, + "secret": "your-jwt-secret", + "expiresIn": "8h", # Expiry time for JWT + "refreshToken": True, # Enable refresh tokens + "prefix": "/auth/jwt", # Prefix for JWT-related routes +} +``` From f6f4974cd23d645b47f6b24032bb8a0b84709cf1 Mon Sep 17 00:00:00 2001 From: ABINFLYCATCH Date: Wed, 5 Feb 2025 15:22:42 +0530 Subject: [PATCH 09/28] change the esc line in readme --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 91cd323..af59e60 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,13 @@ Flycatch Auth is a Python authentication package that provides JWT-based authent ## Features -- \*\*JWT authentication +- **JWT authentication -- \*\*Grant-based access control +- **Grant-based access control -- \*\*Works with Flask and FastAPI +- **Works with Flask and FastAPI -- \*\*Simple integration with existing user services +- **Simple integration with existing user services ## Installation From 576b7b3d612f7b3b187314efc8efb0ee355090d7 Mon Sep 17 00:00:00 2001 From: ABINFLYCATCH Date: Wed, 5 Feb 2025 17:02:06 +0530 Subject: [PATCH 10/28] updated version of flask and python also readme file --- .gitignore | 2 + README.md | 27 ++- poetry.lock | 607 ++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 3 +- 4 files changed, 629 insertions(+), 10 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..43ae0e2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.py[cod] diff --git a/README.md b/README.md index af59e60..242c77a 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,13 @@ Flycatch Auth is a Python authentication package that provides JWT-based authent ## Features -- **JWT authentication +- **JWT authentication**:Supports access and refresh tokens with customizable expiry times. -- **Grant-based access control +- **Grant-based access control** -- **Works with Flask and FastAPI +- **Works with Flask and FastAPI** -- **Simple integration with existing user services +- **Simple integration with existing user services** ## Installation @@ -73,6 +73,7 @@ def protected(): if __name__ == "__main__": app.run(debug=True) ``` +This snippet demonstrates how to configure and initialize Auth using MockUserService and JWT-based authentication ## Configuration Options @@ -87,3 +88,21 @@ jwt_config = { "prefix": "/auth/jwt", # Prefix for JWT-related routes } ``` + +### Password Checker + +```python +credential_checker=lambda input, user: input == user["password"], +``` + +## License + +This project is licensed under the GPL-3.0 License. + +--- + +For more details and advanced use cases, visit the [GitHub repository](#) or contact the project maintainers. + +``` + +``` diff --git a/poetry.lock b/poetry.lock index 3af4d0e..6ed686e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,7 +1,604 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. -package = [] +# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. + +[[package]] +name = "anyio" +version = "4.8.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a"}, + {file = "anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a"}, +] + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" +typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} + +[package.extras] +doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] +trio = ["trio (>=0.26.1)"] + +[[package]] +name = "black" +version = "22.12.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, + {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, + {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, + {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, + {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, + {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, + {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, + {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, + {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, + {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, + {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, + {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.9.0" +platformdirs = ">=2" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "blinker" +version = "1.9.0" +description = "Fast, simple object-to-object and broadcast signaling" +optional = false +python-versions = ">=3.9" +groups = ["main"] +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 = "click" +version = "8.1.8" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +files = [ + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[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"] +markers = "platform_system == \"Windows\" or sys_platform == \"win32\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.6.10" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78"}, + {file = "coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c"}, + {file = "coverage-7.6.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a"}, + {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165"}, + {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988"}, + {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5"}, + {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3"}, + {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5"}, + {file = "coverage-7.6.10-cp310-cp310-win32.whl", hash = "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244"}, + {file = "coverage-7.6.10-cp310-cp310-win_amd64.whl", hash = "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e"}, + {file = "coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3"}, + {file = "coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43"}, + {file = "coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132"}, + {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f"}, + {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994"}, + {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99"}, + {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd"}, + {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377"}, + {file = "coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8"}, + {file = "coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609"}, + {file = "coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853"}, + {file = "coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078"}, + {file = "coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0"}, + {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50"}, + {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022"}, + {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b"}, + {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0"}, + {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852"}, + {file = "coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359"}, + {file = "coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247"}, + {file = "coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9"}, + {file = "coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b"}, + {file = "coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690"}, + {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18"}, + {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c"}, + {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd"}, + {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e"}, + {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694"}, + {file = "coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6"}, + {file = "coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e"}, + {file = "coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe"}, + {file = "coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273"}, + {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8"}, + {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098"}, + {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb"}, + {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0"}, + {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf"}, + {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2"}, + {file = "coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312"}, + {file = "coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d"}, + {file = "coverage-7.6.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:656c82b8a0ead8bba147de9a89bda95064874c91a3ed43a00e687f23cc19d53a"}, + {file = "coverage-7.6.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccc2b70a7ed475c68ceb548bf69cec1e27305c1c2606a5eb7c3afff56a1b3b27"}, + {file = "coverage-7.6.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5e37dc41d57ceba70956fa2fc5b63c26dba863c946ace9705f8eca99daecdc4"}, + {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0aa9692b4fdd83a4647eeb7db46410ea1322b5ed94cd1715ef09d1d5922ba87f"}, + {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa744da1820678b475e4ba3dfd994c321c5b13381d1041fe9c608620e6676e25"}, + {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0b1818063dc9e9d838c09e3a473c1422f517889436dd980f5d721899e66f315"}, + {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:59af35558ba08b758aec4d56182b222976330ef8d2feacbb93964f576a7e7a90"}, + {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7ed2f37cfce1ce101e6dffdfd1c99e729dd2ffc291d02d3e2d0af8b53d13840d"}, + {file = "coverage-7.6.10-cp39-cp39-win32.whl", hash = "sha256:4bcc276261505d82f0ad426870c3b12cb177752834a633e737ec5ee79bbdff18"}, + {file = "coverage-7.6.10-cp39-cp39-win_amd64.whl", hash = "sha256:457574f4599d2b00f7f637a0700a6422243b3565509457b2dbd3f50703e11f59"}, + {file = "coverage-7.6.10-pp39.pp310-none-any.whl", hash = "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f"}, + {file = "coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23"}, +] + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "fastapi" +version = "0.78.0" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +optional = false +python-versions = ">=3.6.1" +groups = ["main"] +files = [ + {file = "fastapi-0.78.0-py3-none-any.whl", hash = "sha256:15fcabd5c78c266fa7ae7d8de9b384bfc2375ee0503463a6febbe3bab69d6f65"}, + {file = "fastapi-0.78.0.tar.gz", hash = "sha256:3233d4a789ba018578658e2af1a4bb5e38bdd122ff722b313666a9b2c6786a83"}, +] + +[package.dependencies] +pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" +starlette = "0.19.1" + +[package.extras] +all = ["email_validator (>=1.1.1,<2.0.0)", "itsdangerous (>=1.1.0,<3.0.0)", "jinja2 (>=2.11.2,<4.0.0)", "orjson (>=3.2.1,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "pyyaml (>=5.3.1,<7.0.0)", "requests (>=2.24.0,<3.0.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)", "uvicorn[standard] (>=0.12.0,<0.18.0)"] +dev = ["autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "pre-commit (>=2.17.0,<3.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "uvicorn[standard] (>=0.12.0,<0.18.0)"] +doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer (>=0.4.1,<0.5.0)"] +test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==22.3.0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "email_validator (>=1.1.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "orjson (>=3.2.1,<4.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "requests (>=2.24.0,<3.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "types-dataclasses (==0.6.5)", "types-orjson (==3.6.2)", "types-ujson (==4.2.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"] + +[[package]] +name = "flask" +version = "2.3.3" +description = "A simple framework for building complex web applications." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "flask-2.3.3-py3-none-any.whl", hash = "sha256:f69fcd559dc907ed196ab9df0e48471709175e696d6e698dd4dbe940f96ce66b"}, + {file = "flask-2.3.3.tar.gz", hash = "sha256:09c347a92aa7ff4a8e7f3206795f30d826654baf38b873d0744cd571ca609efc"}, +] + +[package.dependencies] +blinker = ">=1.6.2" +click = ">=8.1.3" +itsdangerous = ">=2.1.2" +Jinja2 = ">=3.1.2" +Werkzeug = ">=2.3.7" + +[package.extras] +async = ["asgiref (>=3.2)"] +dotenv = ["python-dotenv"] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +groups = ["main"] +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 = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +description = "Safely pass data to untrusted environments and back." +optional = false +python-versions = ">=3.8" +groups = ["main"] +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 = "jinja2" +version = "3.1.5" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, + {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "markupsafe" +version = "3.0.2" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +groups = ["dev"] +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "packaging" +version = "24.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] + +[[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 = "platformdirs" +version = "4.3.6" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pydantic" +version = "1.10.21" +description = "Data validation and settings management using python type hints" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "pydantic-1.10.21-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:245e486e0fec53ec2366df9cf1cba36e0bbf066af7cd9c974bbbd9ba10e1e586"}, + {file = "pydantic-1.10.21-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6c54f8d4c151c1de784c5b93dfbb872067e3414619e10e21e695f7bb84d1d1fd"}, + {file = "pydantic-1.10.21-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b64708009cfabd9c2211295144ff455ec7ceb4c4fb45a07a804309598f36187"}, + {file = "pydantic-1.10.21-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a148410fa0e971ba333358d11a6dea7b48e063de127c2b09ece9d1c1137dde4"}, + {file = "pydantic-1.10.21-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:36ceadef055af06e7756eb4b871cdc9e5a27bdc06a45c820cd94b443de019bbf"}, + {file = "pydantic-1.10.21-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c0501e1d12df6ab1211b8cad52d2f7b2cd81f8e8e776d39aa5e71e2998d0379f"}, + {file = "pydantic-1.10.21-cp310-cp310-win_amd64.whl", hash = "sha256:c261127c275d7bce50b26b26c7d8427dcb5c4803e840e913f8d9df3f99dca55f"}, + {file = "pydantic-1.10.21-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8b6350b68566bb6b164fb06a3772e878887f3c857c46c0c534788081cb48adf4"}, + {file = "pydantic-1.10.21-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:935b19fdcde236f4fbf691959fa5c3e2b6951fff132964e869e57c70f2ad1ba3"}, + {file = "pydantic-1.10.21-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b6a04efdcd25486b27f24c1648d5adc1633ad8b4506d0e96e5367f075ed2e0b"}, + {file = "pydantic-1.10.21-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1ba253eb5af8d89864073e6ce8e6c8dec5f49920cff61f38f5c3383e38b1c9f"}, + {file = "pydantic-1.10.21-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:57f0101e6c97b411f287a0b7cf5ebc4e5d3b18254bf926f45a11615d29475793"}, + {file = "pydantic-1.10.21-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e85834f0370d737c77a386ce505c21b06bfe7086c1c568b70e15a568d9670d"}, + {file = "pydantic-1.10.21-cp311-cp311-win_amd64.whl", hash = "sha256:6a497bc66b3374b7d105763d1d3de76d949287bf28969bff4656206ab8a53aa9"}, + {file = "pydantic-1.10.21-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2ed4a5f13cf160d64aa331ab9017af81f3481cd9fd0e49f1d707b57fe1b9f3ae"}, + {file = "pydantic-1.10.21-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3b7693bb6ed3fbe250e222f9415abb73111bb09b73ab90d2d4d53f6390e0ccc1"}, + {file = "pydantic-1.10.21-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:185d5f1dff1fead51766da9b2de4f3dc3b8fca39e59383c273f34a6ae254e3e2"}, + {file = "pydantic-1.10.21-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38e6d35cf7cd1727822c79e324fa0677e1a08c88a34f56695101f5ad4d5e20e5"}, + {file = "pydantic-1.10.21-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:1d7c332685eafacb64a1a7645b409a166eb7537f23142d26895746f628a3149b"}, + {file = "pydantic-1.10.21-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c9b782db6f993a36092480eeaab8ba0609f786041b01f39c7c52252bda6d85f"}, + {file = "pydantic-1.10.21-cp312-cp312-win_amd64.whl", hash = "sha256:7ce64d23d4e71d9698492479505674c5c5b92cda02b07c91dfc13633b2eef805"}, + {file = "pydantic-1.10.21-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0067935d35044950be781933ab91b9a708eaff124bf860fa2f70aeb1c4be7212"}, + {file = "pydantic-1.10.21-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5e8148c2ce4894ce7e5a4925d9d3fdce429fb0e821b5a8783573f3611933a251"}, + {file = "pydantic-1.10.21-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4973232c98b9b44c78b1233693e5e1938add5af18042f031737e1214455f9b8"}, + {file = "pydantic-1.10.21-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:662bf5ce3c9b1cef32a32a2f4debe00d2f4839fefbebe1d6956e681122a9c839"}, + {file = "pydantic-1.10.21-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:98737c3ab5a2f8a85f2326eebcd214510f898881a290a7939a45ec294743c875"}, + {file = "pydantic-1.10.21-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0bb58bbe65a43483d49f66b6c8474424d551a3fbe8a7796c42da314bac712738"}, + {file = "pydantic-1.10.21-cp313-cp313-win_amd64.whl", hash = "sha256:e622314542fb48542c09c7bd1ac51d71c5632dd3c92dc82ede6da233f55f4848"}, + {file = "pydantic-1.10.21-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d356aa5b18ef5a24d8081f5c5beb67c0a2a6ff2a953ee38d65a2aa96526b274f"}, + {file = "pydantic-1.10.21-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08caa8c0468172d27c669abfe9e7d96a8b1655ec0833753e117061febaaadef5"}, + {file = "pydantic-1.10.21-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c677aa39ec737fec932feb68e4a2abe142682f2885558402602cd9746a1c92e8"}, + {file = "pydantic-1.10.21-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:79577cc045d3442c4e845df53df9f9202546e2ba54954c057d253fc17cd16cb1"}, + {file = "pydantic-1.10.21-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:b6b73ab347284719f818acb14f7cd80696c6fdf1bd34feee1955d7a72d2e64ce"}, + {file = "pydantic-1.10.21-cp37-cp37m-win_amd64.whl", hash = "sha256:46cffa24891b06269e12f7e1ec50b73f0c9ab4ce71c2caa4ccf1fb36845e1ff7"}, + {file = "pydantic-1.10.21-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:298d6f765e3c9825dfa78f24c1efd29af91c3ab1b763e1fd26ae4d9e1749e5c8"}, + {file = "pydantic-1.10.21-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f2f4a2305f15eff68f874766d982114ac89468f1c2c0b97640e719cf1a078374"}, + {file = "pydantic-1.10.21-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35b263b60c519354afb3a60107d20470dd5250b3ce54c08753f6975c406d949b"}, + {file = "pydantic-1.10.21-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e23a97a6c2f2db88995496db9387cd1727acdacc85835ba8619dce826c0b11a6"}, + {file = "pydantic-1.10.21-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:3c96fed246ccc1acb2df032ff642459e4ae18b315ecbab4d95c95cfa292e8517"}, + {file = "pydantic-1.10.21-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:b92893ebefc0151474f682e7debb6ab38552ce56a90e39a8834734c81f37c8a9"}, + {file = "pydantic-1.10.21-cp38-cp38-win_amd64.whl", hash = "sha256:b8460bc256bf0de821839aea6794bb38a4c0fbd48f949ea51093f6edce0be459"}, + {file = "pydantic-1.10.21-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5d387940f0f1a0adb3c44481aa379122d06df8486cc8f652a7b3b0caf08435f7"}, + {file = "pydantic-1.10.21-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:266ecfc384861d7b0b9c214788ddff75a2ea123aa756bcca6b2a1175edeca0fe"}, + {file = "pydantic-1.10.21-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61da798c05a06a362a2f8c5e3ff0341743e2818d0f530eaac0d6898f1b187f1f"}, + {file = "pydantic-1.10.21-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a621742da75ce272d64ea57bd7651ee2a115fa67c0f11d66d9dcfc18c2f1b106"}, + {file = "pydantic-1.10.21-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9e3e4000cd54ef455694b8be9111ea20f66a686fc155feda1ecacf2322b115da"}, + {file = "pydantic-1.10.21-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f198c8206640f4c0ef5a76b779241efb1380a300d88b1bce9bfe95a6362e674d"}, + {file = "pydantic-1.10.21-cp39-cp39-win_amd64.whl", hash = "sha256:e7f0cda108b36a30c8fc882e4fc5b7eec8ef584aa43aa43694c6a7b274fb2b56"}, + {file = "pydantic-1.10.21-py3-none-any.whl", hash = "sha256:db70c920cba9d05c69ad4a9e7f8e9e83011abb2c6490e561de9ae24aee44925c"}, + {file = "pydantic-1.10.21.tar.gz", hash = "sha256:64b48e2b609a6c22178a56c408ee1215a7206077ecb8a193e2fda31858b2362a"}, +] + +[package.dependencies] +typing-extensions = ">=4.2.0" + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] + +[[package]] +name = "pyjwt" +version = "2.10.1" +description = "JSON Web Token implementation in Python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"}, + {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"}, +] + +[package.extras] +crypto = ["cryptography (>=3.4.0)"] +dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] +docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] + +[[package]] +name = "pytest" +version = "7.4.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +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\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "3.0.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, + {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, +] + +[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 = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +groups = ["main"] +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 = "starlette" +version = "0.19.1" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "starlette-0.19.1-py3-none-any.whl", hash = "sha256:5a60c5c2d051f3a8eb546136aa0c9399773a689595e099e0877704d5888279bf"}, + {file = "starlette-0.19.1.tar.gz", hash = "sha256:c6d21096774ecb9639acad41b86b7706e52ba3bf1dc13ea4ed9ad593d47e24c7"}, +] + +[package.dependencies] +anyio = ">=3.4.0,<5" + +[package.extras] +full = ["itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests"] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[[package]] +name = "werkzeug" +version = "3.1.3" +description = "The comprehensive WSGI web application library." +optional = false +python-versions = ">=3.9" +groups = ["main"] +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)"] [metadata] -lock-version = "2.0" -python-versions = "^3.12" -content-hash = "34e39677d8527182346093002688d17a5d2fc204b9eb3e094b2e6ac519028228" +lock-version = "2.1" +python-versions = "^3.11" +content-hash = "889d81284d7efb9c83ff27783c283b351de75bc818352dfef32fb2e85ef766ec" diff --git a/pyproject.toml b/pyproject.toml index 8e91731..0d2bcce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,11 +4,12 @@ version = "0.1.0" description = "Authentication package for Flask and FastAPI with JWT support" authors = ["Your Name "] license = "MIT" +readme = "README.md" packages = [{ include = "flycatch_auth" }] [tool.poetry.dependencies] python = "^3.11" -flask = "^2.1" +flask = "3.1.0" fastapi = "^0.78" pyjwt = "^2.4" pytest = "^7.0" # For testing From f23f3862d3c68fd6f5f83d397e631c744cd1a63f Mon Sep 17 00:00:00 2001 From: ABINFLYCATCH Date: Wed, 5 Feb 2025 17:08:35 +0530 Subject: [PATCH 11/28] Remove cached Python files from repository --- .../__pycache__/__init__.cpython-312.pyc | Bin 385 -> 0 bytes flycatch_auth/__pycache__/auth.cpython-312.pyc | Bin 2164 -> 0 bytes .../__pycache__/fastapi_auth.cpython-312.pyc | Bin 1413 -> 0 bytes .../__pycache__/flask_auth.cpython-312.pyc | Bin 1390 -> 0 bytes .../__pycache__/identity.cpython-312.pyc | Bin 1091 -> 0 bytes .../__pycache__/jwt_auth.cpython-312.pyc | Bin 3020 -> 0 bytes tests/__pycache__/__init__.cpython-312.pyc | Bin 164 -> 0 bytes .../test_auth.cpython-312-pytest-7.4.4.pyc | Bin 9940 -> 0 bytes ...tapi_integration.cpython-312-pytest-7.4.4.pyc | Bin 3121 -> 0 bytes ...lask_integration.cpython-312-pytest-7.4.4.pyc | Bin 5130 -> 0 bytes 10 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 flycatch_auth/__pycache__/__init__.cpython-312.pyc delete mode 100644 flycatch_auth/__pycache__/auth.cpython-312.pyc delete mode 100644 flycatch_auth/__pycache__/fastapi_auth.cpython-312.pyc delete mode 100644 flycatch_auth/__pycache__/flask_auth.cpython-312.pyc delete mode 100644 flycatch_auth/__pycache__/identity.cpython-312.pyc delete mode 100644 flycatch_auth/__pycache__/jwt_auth.cpython-312.pyc delete mode 100644 tests/__pycache__/__init__.cpython-312.pyc delete mode 100644 tests/__pycache__/test_auth.cpython-312-pytest-7.4.4.pyc delete mode 100644 tests/__pycache__/test_fastapi_integration.cpython-312-pytest-7.4.4.pyc delete mode 100644 tests/__pycache__/test_flask_integration.cpython-312-pytest-7.4.4.pyc diff --git a/flycatch_auth/__pycache__/__init__.cpython-312.pyc b/flycatch_auth/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index af9d2299c7311ba242f3ab883ad7b5230b6cd3b5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 385 zcmZ8cy-LJD5T4DyN;qup6(q%(#!>`15LB?aF3l2>&1Jn!B71v=ls<+1bv}!4uy9yt zV<%pBAUGSMf?Iq)J2M|M&tbR^=Dn@wUr4XL8NeENdxGK-a>$8*iX?;>Z3(Ng6T8>8 za4I)(D=+Z~%prHyB=I{p^47bXc0T1CO3bd1&|GqD{Z58dUY1088g8v4zPiZOyrBATM z7T8_KWmB=Z5M7q4tc=@6E`D8~6`R0GhD%Qhv&O$FrM%{f(&M{s7o1j2z7p(AjxgKY bz8oWb*o2S`j5l!f0`aE@2Q%{E%MeT7kLzg? diff --git a/flycatch_auth/__pycache__/auth.cpython-312.pyc b/flycatch_auth/__pycache__/auth.cpython-312.pyc deleted file mode 100644 index 42179e35cb64618af429c9379631515f86479a4b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2164 zcmZuyO-Lk16t3!@vDMKToxxTk*&fMmYtL_;`VnM%pud3_StM7ZS z>X*L09s+Ie?;n;^5kme%qt)b4Y2E^5pV-8vd9qHIC?x`!CAP3jY|){|V({ORP?B`{ zFAA1U{q9G3(_Nd{^j56U4ex}|Xx;&3pEzWR+GI(vyTBV=5^ZspEJ?NmQnqD~5jz61 zt3-9hm(bgSC3X0NkO^+ze9YZdXoVQ<>52drhQnJ$1go_@@w!^$t4d3|>?;I32^H}; z+>4SDc}5p>*^h3z4m04}%2|#-Xfelj3SQ348`g?rtvSpWSGPTeG5LyN*Yi-Lg;n*1zcda6G?q(6#Yi83M zCDscb%YBGby0tk% zD6IEIb7R9rmzu`NH7q2)or&{kVB z9sO$Oig3I_kHZvsB34AE!pc_OcdW!@u;~bl9EX4~7+EAGK^M!zpSh0a6*W8@CIfKj zXqYFyvSGR|kZCXIA}8PP&KFJFz)7A5x)flc;7Gw-cUThk@Fg_c=@Fn4?Q~xrKy=fc zLkU>uz5@bKiY3lsT0N$n#U|^q$)k5qVnFzBy|Jec&XTw5$=gSBKiWUNUw`}2X>zs! zkbc~VCH{{orYFaA0~C&FyO>}gY*;8QOgMr!0Kv|r3IDqmRk4k|WYdb2ql<8>*dq42 zRKA60yaK3Z7Ho|<-X<$(AARvDfWGDw{HSSJj_VqMB?GiYR!+9#0)k9L=4?-{QzOlEeFR-_3>aKR8LgQGM7*jvqd(B_?ap$wpkQjihU_OijrI zM07%paB?EB?!E>psw7Kox~n{)6@*GCr#^RBE?Wu!(XvGr)SQNj>3TDt%v+rfky*@_ zW(CCebf0+b**$RF;*$;R(Q9jlw0K**y0V=KeC2g@)UmQcAarrTkMax6*y1Pf`?CEPwS%fK-F9>Z8K+M9XQqlWR-rkM|B_*Ks^A%dKK6}8t<;A976n~?SmAoHzx^W6=3f=~iT5Yn1l0piJ!EstU4cJcNwxEj;(rheStKH0K z)S2g|s>KjaIU^g$~U7nD^eyd++z8 zA4{b?;3_@;=5}5McqWx>X{pe=iNYrM;FA#Q#3pscR_dy)5_zVCT3xsGL{`IG-LMS; z3Q+w~8g82k%z&@0gRehQ`iJ&VJ7*bxNX1fxag{b{o~*8dT6!@+S4Ax? z6CgG+FP~(O6KI2_%ymxAUaMxSa;`G)wxx+;i&N&{z8iQ{ls!g$8pVMdI^GiXRwxtd z@@g#fCJUk%{WVU*8Y|#ytSCtd36~{QaJ`s+&kY$wyfg@$uY9X!lY5`EweQqbV(FbzpHmjp+LcegSga&jSAr-* zG58t>LKHuXWLq07=uqEML;;%^x47dqd@6E2jol!eL+}bm@bUyXQM{Yn?Iiay*#S#s zBgoisNk)-y*&_s(9YZdLGlcqqowo*I`L|8_^N*1H4u7q|uOp*-BbA>${ zqUrCyD8e+kPJX!f^OeOHpz5<^Yvv%~+tYt0ynl;~o3rx5NNZ=ZTS+7PSsY8xtB2Xh znnW29Vj+!VT1_XCCuy=V8J=YIj+72FeQjRqS3Jqiph5ly_*oK`5?laV+6G&Gh%bq%a!1kC zKS|W3C?oi_HV?um(+GUesdX&6NQq5^@i7#Jz@f@)s7f~5MmOZPs&r&siDo}?&7k=~ zJFHtN9(qX;WnAM5vLWPe`j0iY+NHbDr`0iCVua+GTaTo|~nI~I=?EWvH= zg^uYOR-Ic-E+VC{)zHHd6YfAfAU@0 zm-h>c1Lf=N7%#z^=8DR+PK&7AfE#QG12%vf6*~K+)RQh_tGaKU;|S0UeW8WEcAX3C zmiFO>Yl&n?0O#iI9l5Zz>J) zdWafS_CwT@8m}EXLe-WIW-;{~%!m2Z(pGHMnZ~qdYM8wQH*r23oS=K`7Q<>})}uU) zn=hQM89Kh#&3wpB!G#tJjsHxIdhTVM-NpaMPrVN2k^S3;zZ7Lwe>Mk4 zOM@eEh;^(N{t(Y>al?q_45Q`SZI|SnVO(pQZgL~2;euvUED`{-{IunN}iRT I1G3one?ON;AOHXW diff --git a/flycatch_auth/__pycache__/identity.cpython-312.pyc b/flycatch_auth/__pycache__/identity.cpython-312.pyc deleted file mode 100644 index 01a5f0f9fee21cfa71c4ee31d15112921c063804..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1091 zcmZ8fJ8#rL5Z<+Yk38<85Gf=gkrrq$BzGMZqCg~3gg6SiYc`g5FK6LLSnmbh>52$l zkpk*;2s#iFe}pCyO>hf8AYi0OshF|P24$q3nVs30otbaGHX5@8=-!tP`|l_rKk!nJ zHC56LP)>+Jc8DMoBIsu$_jGfc0XDO=(V=Bm+#&}`$eU5R4B~`H(xrlQ4MDr6fY`1z zG;J%X?+O`ePY*{MffG9Wk_}?1m^09Qw7ZV>qL4YE$V`#)Mw!Jes>Cm_E)!1L0(L^i zbWAvCIgJgt6}6DD3AbqmAd`*t4O^lZydX5BinQ%B{zG%6JPau)Am~j!=%o+7kZlW z(PA-`R$;>a;_JYU$l0~kW9v=rY=wbnoGq^%TW{x5aFLE(!6euP=gBPRK_mt~>hqkx z960{eQ{!AjF6U|vf<6&74?^KaDEua4II$Q_6Y|}O^|Ccp}A?QjxL^RfjUNYGbUDv20w)OqpS{dZo2i>6(FnxC!it{A}Jly|wkNxqVvS z&c@lXtEz#i(XSSOC1$KujswE27Lok}XbG>#u%H-+Ga=`OQQ4vIZ=VyCAMgChWFCZ1 wJ5WE)GK;&C@Us3OUy_Gv6`o|U-vH&@q?BG56}tY50J~_Kbcg;X$g{+M0UK%mG5`Po diff --git a/flycatch_auth/__pycache__/jwt_auth.cpython-312.pyc b/flycatch_auth/__pycache__/jwt_auth.cpython-312.pyc deleted file mode 100644 index 48e973d8c6d12d2e03e195a40af8bcab27dab306..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3020 zcmdT`O>7%Q6rSDnI=17)N!MxOHmO1rN=%g>D*7XYTH4Z|q(7GiP<^Pi^=y)LoOPI8 z*RkbF1@#al5bY^Nszl*X6s3nAI3RK2Rs<5Y5|l8_0f}CED=`v~6Ys6P>z{<)IPfHY z^X9$T`Fr1c;~!dD0t6&4e>wTLmyqAE3Lfa;m$npS z=sCwFIT95vMrH2R4E1VC<6cusFwNqAZEjX)nmKClCZ;Gg zg8{l!tnzTFGW!FMqvdggd{?|KS}o4E$yP_Jwn&*|Tea_K)cc@msdAp{lBGEIf@Fmq z-N^#hUXYTVjzqW=7jGn%0>)zW&$-W%_=($kv3MV)eCiUS9^JeAHc zb5Z1ynYqd(pdXjdUOI5_(4xToYHBjgbZcs6Q_ffugGZu4?xQf3rOyCmGgiV#XSpAH zR7+VZmvqCjNr~j0O{ocuOF$%Kc_K|Uo3uQjo4R3IssUd4YR=M3?xh;~7d;Gbo%=JU z#^SJ$%A01@Tq>-Rd?e04(&3$X($in&C z=kIkchokeS)tw<7A zXb?A9+(b&&*1pPUDs4-1tUR_PF?k!rra4BbK^kZta@;_3H!139?k0jW64God&3Dz& zyiz*LbkFwy-*l?=x?HHZ%dBCz>@zF}kPaTI=1hk9#xT8D7s4*|1-LxRkl(w})Q!-C z(2MXAf?bmLqSlAdkAMZSfwVif-$s7m1>~*UFh<&(1Gqt+`dS{h4zBoy)_pDW`iigX zSEc*KoPH#%jEohPvHys>_hDNL#9zbH?U;yf=ypY!3X{SE(J6ug$w_^+55P9e>?ceT z$YqlA)I1d1l=JGiESZxoldKSx^KI{HOrOYA3!@!#D0~68@R{SM7v*w{?Ra90dB-XH zk*1gQNkg?VOgqI`n(_8gb%={VL{+q5X!1&!gK8xu3qDO|y8_R5vAwP$POYyGdl zhow;fC|Vw`Ke8SSeLD8h*saVR`uX%{(+{%`^`C=pttp*URv!C2(w(kl-Ko?IZV<<89KnI{>~534q%dr2kb=yJ~`3R|d)nJpv~mKg|w6 z!wv$}g(Y0M_PTV-@B7#A)Qx)$r#y^s1mI%ShyR-lbw-QFc_1F2Nz+UU^YF59<(B01vjw(-6R{LEca}*2IP*7t{_0SPmYwr0Ojv+ zbW|!c*RELh97n-gop;msW_edf*)ee7KN_7bg9zQ^{GwFdw4B7^Z2gkd;*w(h`1s7c%#!$cy@JYL95%W6 cDWy57c15f}GZ}%n7{vI<%*e=C#0+Es03Wm|R{#J2 diff --git a/tests/__pycache__/test_auth.cpython-312-pytest-7.4.4.pyc b/tests/__pycache__/test_auth.cpython-312-pytest-7.4.4.pyc deleted file mode 100644 index bcc3d7f5017c8d6db90399af013422c8a390d364..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9940 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 P_j+Ridwwr>nCa%<^9EHv From 69a563305b87be7174e5b4bbb592fc862b344296 Mon Sep 17 00:00:00 2001 From: ABINFLYCATCH Date: Thu, 6 Feb 2025 09:46:14 +0530 Subject: [PATCH 12/28] changes poetry and versions are update and pytest to dev depentens --- flycatch_auth/auth.py | 5 +- flycatch_auth/fastapi_auth.py | 1 - flycatch_auth/identity.py | 33 ++- poetry.lock | 433 ++++++++++++++-------------------- pyproject.toml | 31 ++- setup.py | 29 --- 6 files changed, 221 insertions(+), 311 deletions(-) delete mode 100644 setup.py diff --git a/flycatch_auth/auth.py b/flycatch_auth/auth.py index ad33221..217fbe7 100644 --- a/flycatch_auth/auth.py +++ b/flycatch_auth/auth.py @@ -1,4 +1,5 @@ -from .flask_auth import FlaskAuth +from typing import Optional +from .flask_auth import FlaskAuth, Identity # from .fastapi_auth import FastAPIAuth @@ -16,7 +17,7 @@ def init_app(self, app): """Initialize Auth with Flask""" app.auth = self - def authenticate(self, username, password): + def authenticate(self, username: str, password: str)-> Optional[Identity]: """Use to authenticate user""" user = self.user_service.load_user(username) if user and self.credential_checker(password, user["password"]): diff --git a/flycatch_auth/fastapi_auth.py b/flycatch_auth/fastapi_auth.py index 060c5e3..a3aec09 100644 --- a/flycatch_auth/fastapi_auth.py +++ b/flycatch_auth/fastapi_auth.py @@ -11,7 +11,6 @@ def __init__(self, user_service, credential_checker, jwt): self.user_service = user_service self.credential_checker = credential_checker self.jwt = jwt - print("HI......... , FastAPIAuth") async def fastapi_verify(self, request: Request, token: str = Security(security)): """Verify JWT token in FastAPI""" diff --git a/flycatch_auth/identity.py b/flycatch_auth/identity.py index 307435e..37b19ae 100644 --- a/flycatch_auth/identity.py +++ b/flycatch_auth/identity.py @@ -1,11 +1,30 @@ -class Identity: - """manage user authentication and authorization""" - def __init__(self, user_id, username, grants): +from abc import ABC, abstractmethod +from typing import List, Optional + +class Identity(ABC): + """Manage user authentication and authorization.""" + def __init__(self, user_id: str, username: str, grants: List[str]): self.id = user_id self.username = username self.grants = grants -class IdentityService: - def load_user(self, username: str) -> Identity: - """Override this method to fetch user from DB""" - raise NotImplementedError("Must implement `load_user` method") + @abstractmethod + def has_grant(self, grant: str) -> bool: + """Check if the identity has a specific grant.""" + pass + +class IdentityService(ABC): + @abstractmethod + def load_user(self, username: str) -> Optional[Identity]: + """Override this method to fetch a user from DB.""" + pass + + @abstractmethod + def create_user(self, username: str, password: str) -> Identity: + """Override this method to create a new user in DB.""" + pass + + @abstractmethod + def validate_password(self, password: str, hashed_password: str) -> bool: + """Override this method to validate password.""" + pass diff --git a/poetry.lock b/poetry.lock index 6ed686e..39a8b7f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,17 @@ # This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. +[[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"] +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 = "anyio" version = "4.8.0" @@ -22,40 +34,6 @@ doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] trio = ["trio (>=0.26.1)"] -[[package]] -name = "black" -version = "22.12.0" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, - {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, - {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, - {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, - {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, - {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, - {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, - {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, - {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, - {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, - {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, - {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -pathspec = ">=0.9.0" -platformdirs = ">=2" - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - [[package]] name = "blinker" version = "1.9.0" @@ -74,7 +52,7 @@ version = "8.1.8" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, @@ -90,127 +68,51 @@ 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"] -markers = "platform_system == \"Windows\" or sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] - -[[package]] -name = "coverage" -version = "7.6.10" -description = "Code coverage measurement for Python" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78"}, - {file = "coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c"}, - {file = "coverage-7.6.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a"}, - {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165"}, - {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988"}, - {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5"}, - {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3"}, - {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5"}, - {file = "coverage-7.6.10-cp310-cp310-win32.whl", hash = "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244"}, - {file = "coverage-7.6.10-cp310-cp310-win_amd64.whl", hash = "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e"}, - {file = "coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3"}, - {file = "coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43"}, - {file = "coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132"}, - {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f"}, - {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994"}, - {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99"}, - {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd"}, - {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377"}, - {file = "coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8"}, - {file = "coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609"}, - {file = "coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853"}, - {file = "coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078"}, - {file = "coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0"}, - {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50"}, - {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022"}, - {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b"}, - {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0"}, - {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852"}, - {file = "coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359"}, - {file = "coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247"}, - {file = "coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9"}, - {file = "coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b"}, - {file = "coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690"}, - {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18"}, - {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c"}, - {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd"}, - {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e"}, - {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694"}, - {file = "coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6"}, - {file = "coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e"}, - {file = "coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe"}, - {file = "coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273"}, - {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8"}, - {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098"}, - {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb"}, - {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0"}, - {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf"}, - {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2"}, - {file = "coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312"}, - {file = "coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d"}, - {file = "coverage-7.6.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:656c82b8a0ead8bba147de9a89bda95064874c91a3ed43a00e687f23cc19d53a"}, - {file = "coverage-7.6.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccc2b70a7ed475c68ceb548bf69cec1e27305c1c2606a5eb7c3afff56a1b3b27"}, - {file = "coverage-7.6.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5e37dc41d57ceba70956fa2fc5b63c26dba863c946ace9705f8eca99daecdc4"}, - {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0aa9692b4fdd83a4647eeb7db46410ea1322b5ed94cd1715ef09d1d5922ba87f"}, - {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa744da1820678b475e4ba3dfd994c321c5b13381d1041fe9c608620e6676e25"}, - {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0b1818063dc9e9d838c09e3a473c1422f517889436dd980f5d721899e66f315"}, - {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:59af35558ba08b758aec4d56182b222976330ef8d2feacbb93964f576a7e7a90"}, - {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7ed2f37cfce1ce101e6dffdfd1c99e729dd2ffc291d02d3e2d0af8b53d13840d"}, - {file = "coverage-7.6.10-cp39-cp39-win32.whl", hash = "sha256:4bcc276261505d82f0ad426870c3b12cb177752834a633e737ec5ee79bbdff18"}, - {file = "coverage-7.6.10-cp39-cp39-win_amd64.whl", hash = "sha256:457574f4599d2b00f7f637a0700a6422243b3565509457b2dbd3f50703e11f59"}, - {file = "coverage-7.6.10-pp39.pp310-none-any.whl", hash = "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f"}, - {file = "coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23"}, -] - -[package.extras] -toml = ["tomli"] +markers = {main = "platform_system == \"Windows\"", dev = "sys_platform == \"win32\""} [[package]] name = "fastapi" -version = "0.78.0" +version = "0.115.8" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false -python-versions = ">=3.6.1" +python-versions = ">=3.8" groups = ["main"] files = [ - {file = "fastapi-0.78.0-py3-none-any.whl", hash = "sha256:15fcabd5c78c266fa7ae7d8de9b384bfc2375ee0503463a6febbe3bab69d6f65"}, - {file = "fastapi-0.78.0.tar.gz", hash = "sha256:3233d4a789ba018578658e2af1a4bb5e38bdd122ff722b313666a9b2c6786a83"}, + {file = "fastapi-0.115.8-py3-none-any.whl", hash = "sha256:753a96dd7e036b34eeef8babdfcfe3f28ff79648f86551eb36bfc1b0bf4a8cbf"}, + {file = "fastapi-0.115.8.tar.gz", hash = "sha256:0ce9111231720190473e222cdf0f07f7206ad7e53ea02beb1d2dc36e2f0741e9"}, ] [package.dependencies] -pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" -starlette = "0.19.1" +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.46.0" +typing-extensions = ">=4.8.0" [package.extras] -all = ["email_validator (>=1.1.1,<2.0.0)", "itsdangerous (>=1.1.0,<3.0.0)", "jinja2 (>=2.11.2,<4.0.0)", "orjson (>=3.2.1,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "pyyaml (>=5.3.1,<7.0.0)", "requests (>=2.24.0,<3.0.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)", "uvicorn[standard] (>=0.12.0,<0.18.0)"] -dev = ["autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "pre-commit (>=2.17.0,<3.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "uvicorn[standard] (>=0.12.0,<0.18.0)"] -doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer (>=0.4.1,<0.5.0)"] -test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==22.3.0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "email_validator (>=1.1.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "orjson (>=3.2.1,<4.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "requests (>=2.24.0,<3.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "types-dataclasses (==0.6.5)", "types-orjson (==3.6.2)", "types-ujson (==4.2.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"] +all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.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.5)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] [[package]] name = "flask" -version = "2.3.3" +version = "3.1.0" description = "A simple framework for building complex web applications." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "flask-2.3.3-py3-none-any.whl", hash = "sha256:f69fcd559dc907ed196ab9df0e48471709175e696d6e698dd4dbe940f96ce66b"}, - {file = "flask-2.3.3.tar.gz", hash = "sha256:09c347a92aa7ff4a8e7f3206795f30d826654baf38b873d0744cd571ca609efc"}, + {file = "flask-3.1.0-py3-none-any.whl", hash = "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136"}, + {file = "flask-3.1.0.tar.gz", hash = "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac"}, ] [package.dependencies] -blinker = ">=1.6.2" +blinker = ">=1.9" click = ">=8.1.3" -itsdangerous = ">=2.1.2" +itsdangerous = ">=2.2" Jinja2 = ">=3.1.2" -Werkzeug = ">=2.3.7" +Werkzeug = ">=3.1" [package.extras] async = ["asgiref (>=3.2)"] @@ -237,7 +139,7 @@ version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" -groups = ["main", "dev"] +groups = ["dev"] files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, @@ -344,66 +246,25 @@ files = [ {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, ] -[[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.5" -groups = ["dev"] -files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] - [[package]] name = "packaging" version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" -groups = ["main", "dev"] +groups = ["dev"] files = [ {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] -[[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 = "platformdirs" -version = "4.3.6" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, - {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, -] - -[package.extras] -docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] -type = ["mypy (>=1.11.2)"] - [[package]] name = "pluggy" version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" -groups = ["main", "dev"] +groups = ["dev"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -415,70 +276,137 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pydantic" -version = "1.10.21" -description = "Data validation and settings management using python type hints" +version = "2.10.6" +description = "Data validation using Python type hints" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" groups = ["main"] files = [ - {file = "pydantic-1.10.21-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:245e486e0fec53ec2366df9cf1cba36e0bbf066af7cd9c974bbbd9ba10e1e586"}, - {file = "pydantic-1.10.21-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6c54f8d4c151c1de784c5b93dfbb872067e3414619e10e21e695f7bb84d1d1fd"}, - {file = "pydantic-1.10.21-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b64708009cfabd9c2211295144ff455ec7ceb4c4fb45a07a804309598f36187"}, - {file = "pydantic-1.10.21-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a148410fa0e971ba333358d11a6dea7b48e063de127c2b09ece9d1c1137dde4"}, - {file = "pydantic-1.10.21-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:36ceadef055af06e7756eb4b871cdc9e5a27bdc06a45c820cd94b443de019bbf"}, - {file = "pydantic-1.10.21-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c0501e1d12df6ab1211b8cad52d2f7b2cd81f8e8e776d39aa5e71e2998d0379f"}, - {file = "pydantic-1.10.21-cp310-cp310-win_amd64.whl", hash = "sha256:c261127c275d7bce50b26b26c7d8427dcb5c4803e840e913f8d9df3f99dca55f"}, - {file = "pydantic-1.10.21-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8b6350b68566bb6b164fb06a3772e878887f3c857c46c0c534788081cb48adf4"}, - {file = "pydantic-1.10.21-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:935b19fdcde236f4fbf691959fa5c3e2b6951fff132964e869e57c70f2ad1ba3"}, - {file = "pydantic-1.10.21-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b6a04efdcd25486b27f24c1648d5adc1633ad8b4506d0e96e5367f075ed2e0b"}, - {file = "pydantic-1.10.21-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1ba253eb5af8d89864073e6ce8e6c8dec5f49920cff61f38f5c3383e38b1c9f"}, - {file = "pydantic-1.10.21-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:57f0101e6c97b411f287a0b7cf5ebc4e5d3b18254bf926f45a11615d29475793"}, - {file = "pydantic-1.10.21-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e85834f0370d737c77a386ce505c21b06bfe7086c1c568b70e15a568d9670d"}, - {file = "pydantic-1.10.21-cp311-cp311-win_amd64.whl", hash = "sha256:6a497bc66b3374b7d105763d1d3de76d949287bf28969bff4656206ab8a53aa9"}, - {file = "pydantic-1.10.21-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2ed4a5f13cf160d64aa331ab9017af81f3481cd9fd0e49f1d707b57fe1b9f3ae"}, - {file = "pydantic-1.10.21-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3b7693bb6ed3fbe250e222f9415abb73111bb09b73ab90d2d4d53f6390e0ccc1"}, - {file = "pydantic-1.10.21-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:185d5f1dff1fead51766da9b2de4f3dc3b8fca39e59383c273f34a6ae254e3e2"}, - {file = "pydantic-1.10.21-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38e6d35cf7cd1727822c79e324fa0677e1a08c88a34f56695101f5ad4d5e20e5"}, - {file = "pydantic-1.10.21-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:1d7c332685eafacb64a1a7645b409a166eb7537f23142d26895746f628a3149b"}, - {file = "pydantic-1.10.21-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c9b782db6f993a36092480eeaab8ba0609f786041b01f39c7c52252bda6d85f"}, - {file = "pydantic-1.10.21-cp312-cp312-win_amd64.whl", hash = "sha256:7ce64d23d4e71d9698492479505674c5c5b92cda02b07c91dfc13633b2eef805"}, - {file = "pydantic-1.10.21-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0067935d35044950be781933ab91b9a708eaff124bf860fa2f70aeb1c4be7212"}, - {file = "pydantic-1.10.21-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5e8148c2ce4894ce7e5a4925d9d3fdce429fb0e821b5a8783573f3611933a251"}, - {file = "pydantic-1.10.21-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4973232c98b9b44c78b1233693e5e1938add5af18042f031737e1214455f9b8"}, - {file = "pydantic-1.10.21-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:662bf5ce3c9b1cef32a32a2f4debe00d2f4839fefbebe1d6956e681122a9c839"}, - {file = "pydantic-1.10.21-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:98737c3ab5a2f8a85f2326eebcd214510f898881a290a7939a45ec294743c875"}, - {file = "pydantic-1.10.21-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0bb58bbe65a43483d49f66b6c8474424d551a3fbe8a7796c42da314bac712738"}, - {file = "pydantic-1.10.21-cp313-cp313-win_amd64.whl", hash = "sha256:e622314542fb48542c09c7bd1ac51d71c5632dd3c92dc82ede6da233f55f4848"}, - {file = "pydantic-1.10.21-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d356aa5b18ef5a24d8081f5c5beb67c0a2a6ff2a953ee38d65a2aa96526b274f"}, - {file = "pydantic-1.10.21-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08caa8c0468172d27c669abfe9e7d96a8b1655ec0833753e117061febaaadef5"}, - {file = "pydantic-1.10.21-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c677aa39ec737fec932feb68e4a2abe142682f2885558402602cd9746a1c92e8"}, - {file = "pydantic-1.10.21-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:79577cc045d3442c4e845df53df9f9202546e2ba54954c057d253fc17cd16cb1"}, - {file = "pydantic-1.10.21-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:b6b73ab347284719f818acb14f7cd80696c6fdf1bd34feee1955d7a72d2e64ce"}, - {file = "pydantic-1.10.21-cp37-cp37m-win_amd64.whl", hash = "sha256:46cffa24891b06269e12f7e1ec50b73f0c9ab4ce71c2caa4ccf1fb36845e1ff7"}, - {file = "pydantic-1.10.21-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:298d6f765e3c9825dfa78f24c1efd29af91c3ab1b763e1fd26ae4d9e1749e5c8"}, - {file = "pydantic-1.10.21-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f2f4a2305f15eff68f874766d982114ac89468f1c2c0b97640e719cf1a078374"}, - {file = "pydantic-1.10.21-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35b263b60c519354afb3a60107d20470dd5250b3ce54c08753f6975c406d949b"}, - {file = "pydantic-1.10.21-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e23a97a6c2f2db88995496db9387cd1727acdacc85835ba8619dce826c0b11a6"}, - {file = "pydantic-1.10.21-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:3c96fed246ccc1acb2df032ff642459e4ae18b315ecbab4d95c95cfa292e8517"}, - {file = "pydantic-1.10.21-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:b92893ebefc0151474f682e7debb6ab38552ce56a90e39a8834734c81f37c8a9"}, - {file = "pydantic-1.10.21-cp38-cp38-win_amd64.whl", hash = "sha256:b8460bc256bf0de821839aea6794bb38a4c0fbd48f949ea51093f6edce0be459"}, - {file = "pydantic-1.10.21-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5d387940f0f1a0adb3c44481aa379122d06df8486cc8f652a7b3b0caf08435f7"}, - {file = "pydantic-1.10.21-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:266ecfc384861d7b0b9c214788ddff75a2ea123aa756bcca6b2a1175edeca0fe"}, - {file = "pydantic-1.10.21-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61da798c05a06a362a2f8c5e3ff0341743e2818d0f530eaac0d6898f1b187f1f"}, - {file = "pydantic-1.10.21-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a621742da75ce272d64ea57bd7651ee2a115fa67c0f11d66d9dcfc18c2f1b106"}, - {file = "pydantic-1.10.21-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9e3e4000cd54ef455694b8be9111ea20f66a686fc155feda1ecacf2322b115da"}, - {file = "pydantic-1.10.21-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f198c8206640f4c0ef5a76b779241efb1380a300d88b1bce9bfe95a6362e674d"}, - {file = "pydantic-1.10.21-cp39-cp39-win_amd64.whl", hash = "sha256:e7f0cda108b36a30c8fc882e4fc5b7eec8ef584aa43aa43694c6a7b274fb2b56"}, - {file = "pydantic-1.10.21-py3-none-any.whl", hash = "sha256:db70c920cba9d05c69ad4a9e7f8e9e83011abb2c6490e561de9ae24aee44925c"}, - {file = "pydantic-1.10.21.tar.gz", hash = "sha256:64b48e2b609a6c22178a56c408ee1215a7206077ecb8a193e2fda31858b2362a"}, + {file = "pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584"}, + {file = "pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236"}, ] [package.dependencies] -typing-extensions = ">=4.2.0" +annotated-types = ">=0.6.0" +pydantic-core = "2.27.2" +typing-extensions = ">=4.12.2" [package.extras] -dotenv = ["python-dotenv (>=0.10.4)"] -email = ["email-validator (>=1.0.3)"] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata"] + +[[package]] +name = "pydantic-core" +version = "2.27.2" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, + {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"}, + {file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"}, + {file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"}, + {file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"}, + {file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"}, + {file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"}, + {file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"}, + {file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"}, + {file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"}, + {file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"}, + {file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"}, + {file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"}, + {file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"}, + {file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"}, + {file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"}, + {file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"}, + {file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pyjwt" @@ -500,43 +428,24 @@ tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] [[package]] name = "pytest" -version = "7.4.4" +version = "8.3.4" description = "pytest: simple powerful testing with Python" optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] +python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, - {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, + {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, + {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, ] [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} iniconfig = "*" packaging = "*" -pluggy = ">=0.12,<2.0" - -[package.extras] -testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-cov" -version = "3.0.0" -description = "Pytest plugin for measuring coverage." -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, - {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, -] - -[package.dependencies] -coverage = {version = ">=5.2.1", extras = ["toml"]} -pytest = ">=4.6" +pluggy = ">=1.5,<2" [package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "sniffio" @@ -552,21 +461,21 @@ files = [ [[package]] name = "starlette" -version = "0.19.1" +version = "0.45.3" description = "The little ASGI library that shines." optional = false -python-versions = ">=3.6" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "starlette-0.19.1-py3-none-any.whl", hash = "sha256:5a60c5c2d051f3a8eb546136aa0c9399773a689595e099e0877704d5888279bf"}, - {file = "starlette-0.19.1.tar.gz", hash = "sha256:c6d21096774ecb9639acad41b86b7706e52ba3bf1dc13ea4ed9ad593d47e24c7"}, + {file = "starlette-0.45.3-py3-none-any.whl", hash = "sha256:dfb6d332576f136ec740296c7e8bb8c8a7125044e7c6da30744718880cdd059d"}, + {file = "starlette-0.45.3.tar.gz", hash = "sha256:2cbcba2a75806f8a41c722141486f37c28e30a0921c5f6fe4346cb0dcee1302f"}, ] [package.dependencies] -anyio = ">=3.4.0,<5" +anyio = ">=3.6.2,<5" [package.extras] -full = ["itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests"] +full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] [[package]] name = "typing-extensions" @@ -600,5 +509,5 @@ watchdog = ["watchdog (>=2.3)"] [metadata] lock-version = "2.1" -python-versions = "^3.11" -content-hash = "889d81284d7efb9c83ff27783c283b351de75bc818352dfef32fb2e85ef766ec" +python-versions = ">=3.11" +content-hash = "22b6e8acad34d08d460e3e33266b43c2bc6bd46d32c82390b65ab54f2f195eff" diff --git a/pyproject.toml b/pyproject.toml index 0d2bcce..223b52f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,23 +1,34 @@ [tool.poetry] name = "flycatch_auth" version = "0.1.0" -description = "Authentication package for Flask and FastAPI with JWT support" +description = "A Python authentication package supporting Flask and FastAPI" authors = ["Your Name "] license = "MIT" readme = "README.md" +repository = "https://github.com/yourusername/flycatch_auth" packages = [{ include = "flycatch_auth" }] +keywords = ["authentication", "jwt", "flask", "fastapi"] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12" +] [tool.poetry.dependencies] -python = "^3.11" -flask = "3.1.0" -fastapi = "^0.78" -pyjwt = "^2.4" -pytest = "^7.0" # For testing +python = ">=3.11" +flask = "^3.1.0" +fastapi = "^0.115.8" +pyjwt = "^2.10.1" +pydantic = "^2.10.6" + + +[tool.poetry.group.dev.dependencies] +pytest = "^8.3.4" -[tool.poetry.dev-dependencies] -pytest-cov = "^3.0" # For coverage reports -black = "^22.3.0" # For formatting [build-system] -requires = ["poetry-core>=1.0.0"] +requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" diff --git a/setup.py b/setup.py deleted file mode 100644 index a1e87dd..0000000 --- a/setup.py +++ /dev/null @@ -1,29 +0,0 @@ -from setuptools import setup, find_packages - -setup( - name="flycatch_auth", - version="0.1.0", - description="A Python authentication package supporting Flask and FastAPI", - long_description=open("README.md", encoding="utf-8").read(), - long_description_content_type="text/markdown", - author="Your Name", - author_email="your.email@example.com", - url="https://github.com/yourusername/flycatch_auth", - packages=find_packages(), - install_requires=[ - "flask", - "fastapi", - "pyjwt", - "pydantic" - ], - classifiers=[ - "Development Status :: 3 - Alpha", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - ], - python_requires=">=3.12", -) From 7438fc4dc0995434ee3d3212add7737f059cebc6 Mon Sep 17 00:00:00 2001 From: ABINFLYCATCH Date: Thu, 6 Feb 2025 13:36:45 +0530 Subject: [PATCH 13/28] jwt routed added --- README.md | 24 +++++++++++---------- flycatch_auth/auth.py | 30 +++++++++++++++++++++----- flycatch_auth/fastapi_auth.py | 21 ++++++++++++++++--- flycatch_auth/flask_auth.py | 37 +++++++++++++++++++++++++++++++-- flycatch_auth/identity.py | 22 +++++++++++++------- flycatch_auth/jwt_auth.py | 24 +++++++++++++++++++-- routes/__init__.py | 3 +++ routes/jwt_routes.py | 32 ++++++++++++++++++++++++++++ tests/test_flask_integration.py | 33 +++++++++++++++++------------ 9 files changed, 182 insertions(+), 44 deletions(-) create mode 100644 routes/__init__.py create mode 100644 routes/jwt_routes.py diff --git a/README.md b/README.md index 242c77a..ef720a4 100644 --- a/README.md +++ b/README.md @@ -27,17 +27,18 @@ pip install flycatch-auth from flask import Flask, request, jsonify from flycatch_auth import Auth, AuthCoreJwtConfig -class MockUserService: - def load_user(self, username: str): - return { - "id": "1", - "username": "testuser", - "password": "password123", - "grants": ["read_user"], - } - app = Flask(__name__) +class Userservice(IdentityService): + def load_user(username: str) -> Identity: + user = user_db.get_users() + return { + "id": "", + "username": "", + "password": "", + "grants": [list(map(lambda user: user.permission.name, users))] # read_user + } + auth = Auth( user_service=MockUserService(), credential_checker=lambda input, user: input == user["password"], @@ -65,14 +66,15 @@ def login(): refresh_token = auth.jwt.generate_refresh_token(user) return jsonify({"access_token": access_token, "refresh_token": refresh_token}), 200 -@app.route("/protected") +@app.route("/me") @auth.verify() -def protected(): +def get_curr_user(): return jsonify({"message": "Access granted"}), 200 if __name__ == "__main__": app.run(debug=True) ``` + This snippet demonstrates how to configure and initialize Auth using MockUserService and JWT-based authentication ## Configuration Options diff --git a/flycatch_auth/auth.py b/flycatch_auth/auth.py index 217fbe7..360309f 100644 --- a/flycatch_auth/auth.py +++ b/flycatch_auth/auth.py @@ -1,5 +1,7 @@ from typing import Optional -from .flask_auth import FlaskAuth, Identity +from routes.jwt_routes import create_jwt_routes +from .flask_auth import FlaskAuth +from .identity import Identity # from .fastapi_auth import FastAPIAuth @@ -15,9 +17,11 @@ def __init__(self, user_service, credential_checker, jwt=None): def init_app(self, app): """Initialize Auth with Flask""" + app.auth = self + create_jwt_routes(self) - def authenticate(self, username: str, password: str)-> Optional[Identity]: + def authenticate(self, username: str, password: str) -> Optional[Identity]: """Use to authenticate user""" user = self.user_service.load_user(username) if user and self.credential_checker(password, user["password"]): @@ -26,12 +30,21 @@ def authenticate(self, username: str, password: str)-> Optional[Identity]: def login(self, username, password): """Authenticate user and return JWT token""" - user = self.authenticate(username, password) + user = self.authenticate(username, password) if user: - token = self.jwt.generate_token(user) - return {"access_token": token} + access_token = self.jwt.generate_token(user) + refresh_token = self.jwt.generate_refresh_token(user) + return {"access_token": access_token, "refresh_token": refresh_token} return {"error": "Invalid credentials"} + def refresh(self, refresh_token: str): + """Refresh the access token using a valid refresh token""" + user = self.jwt.verify_refresh_token(refresh_token) + if user: + access_token = self.jwt.generate_token(user) + return {"access_token": access_token} + return {"error": "Invalid refresh token"} + def verify(self): """Verify token for Flask or FastAPI""" def wrapper(func): @@ -41,3 +54,10 @@ def wrapper(func): return self.fastapi_verify(func) # FastAPI verification return func return wrapper + + def has_grants(self, required_grants): + """General method to return the correct grant-based verification""" + return { + "flask": self.flask_has_grants(required_grants), + # "fastapi": self.fastapi_has_grants(required_grants), + } diff --git a/flycatch_auth/fastapi_auth.py b/flycatch_auth/fastapi_auth.py index a3aec09..f9a3d8f 100644 --- a/flycatch_auth/fastapi_auth.py +++ b/flycatch_auth/fastapi_auth.py @@ -1,5 +1,3 @@ -# This file contains the FastAPIAuth class which is used to verify JWT tokens in FastAPI - from fastapi import Depends, HTTPException, Security from fastapi.security import HTTPBearer from starlette.requests import Request @@ -11,8 +9,25 @@ def __init__(self, user_service, credential_checker, jwt): self.user_service = user_service self.credential_checker = credential_checker self.jwt = jwt - + async def fastapi_verify(self, request: Request, token: str = Security(security)): """Verify JWT token in FastAPI""" if not token.credentials or not self.jwt.verify_token(token.credentials): raise HTTPException(status_code=401, detail="Unauthorized") + + return self.jwt.verify_token(token.credentials) # Return user payload + + def fastapi_has_grants(self, required_grants): + """FastAPI dependency for grant-based access control""" + async def dependency(token: str = Security(security)): + payload = self.jwt.verify_token(token.credentials) + if not payload: + raise HTTPException(status_code=401, detail="Unauthorized") + + user_grants = payload.get("grants", []) + if not all(grant in user_grants for grant in required_grants): + raise HTTPException(status_code=403, detail="Forbidden") + + return payload # Return user payload for further use in FastAPI routes + + return dependency diff --git a/flycatch_auth/flask_auth.py b/flycatch_auth/flask_auth.py index 2e3db90..ce0110a 100644 --- a/flycatch_auth/flask_auth.py +++ b/flycatch_auth/flask_auth.py @@ -1,4 +1,6 @@ from flask import request, jsonify +from functools import wraps + class FlaskAuth: def __init__(self, user_service, credential_checker, jwt): @@ -10,7 +12,38 @@ def flask_verify(self, func): """Decorator to verify JWT token in Flask""" def wrapper(*args, **kwargs): token = request.headers.get("Authorization") - if not token or not self.jwt.verify_token(token.replace("Bearer ", "")): - return jsonify({"error": "Unauthorized"}), 401 + if not token: + return jsonify({"error": "Token missing"}), 401 + + token = token.replace("Bearer ", "") + try: + # Decode the token and attach the user data to the request + user = self.jwt.decode_token(token) + if not user: + return jsonify({"error": "Invalid token"}), 401 + request.user = user # Attach user to the request + except Exception as e: + return jsonify({"error": str(e)}), 401 + return func(*args, **kwargs) return wrapper + + def flask_has_grants(self, required_grants): + """Flask decorator for grant-based access control""" + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + # Check if the user is available in request + + if not hasattr(request, "user") or not request.user: + return jsonify({"error": "Unauthorized"}), 401 + + user_grants = request.user.get("grants", []) + print(user_grants) + # Check if the user has all the required grants + if not all(grant in user_grants for grant in required_grants): + return jsonify({"error": "Forbidden"}), 403 + + return func(*args, **kwargs) + return wrapper + return decorator diff --git a/flycatch_auth/identity.py b/flycatch_auth/identity.py index 37b19ae..a6a6e95 100644 --- a/flycatch_auth/identity.py +++ b/flycatch_auth/identity.py @@ -1,8 +1,10 @@ from abc import ABC, abstractmethod from typing import List, Optional + class Identity(ABC): """Manage user authentication and authorization.""" + def __init__(self, user_id: str, username: str, grants: List[str]): self.id = user_id self.username = username @@ -13,18 +15,22 @@ def has_grant(self, grant: str) -> bool: """Check if the identity has a specific grant.""" pass + class IdentityService(ABC): @abstractmethod def load_user(self, username: str) -> Optional[Identity]: """Override this method to fetch a user from DB.""" pass - @abstractmethod - def create_user(self, username: str, password: str) -> Identity: - """Override this method to create a new user in DB.""" - pass - @abstractmethod - def validate_password(self, password: str, hashed_password: str) -> bool: - """Override this method to validate password.""" - pass +# class MockUserService(IdentityService): +# def load_user(self, username: str) -> Identity: +# return Identity(user_id="1", username="testuser", grants=["read_user"]) + +# def create_user(self, username: str, password: str) -> Identity: +# # Mock implementation for creating a user +# return Identity(user_id="2", username=username, grants=[]) + +# def validate_password(self, password: str, hashed_password: str) -> bool: +# # Mock implementation for validating a password +# return password == hashed_password diff --git a/flycatch_auth/jwt_auth.py b/flycatch_auth/jwt_auth.py index 1036bcd..373e547 100644 --- a/flycatch_auth/jwt_auth.py +++ b/flycatch_auth/jwt_auth.py @@ -15,6 +15,8 @@ def generate_token(self, user_data): payload = { "sub": user_data["id"], "exp": datetime.datetime.utcnow() + datetime.timedelta(hours=int(self.expiresIn.replace("h", ""))), + "username": user_data["username"], + "grants": user_data["grants"], } token = jwt.encode(payload, self.secret, algorithm="HS256") @@ -24,8 +26,6 @@ def generate_token(self, user_data): def generate_refresh_token(self, user_data): """Generate JWT refresh token""" - print("inside generate_refresh_token") - print(user_data) payload = { "sub": user_data["id"], "exp": datetime.datetime.utcnow() + datetime.timedelta(days=7), @@ -36,6 +36,26 @@ def generate_refresh_token(self, user_data): token = token.decode("utf-8") return token + def decode_token(self, token): + """Decode JWT token and return user data""" + try: + decoded = jwt.decode(token, self.secret, algorithms=["HS256"]) + return decoded # Return the user data + except jwt.ExpiredSignatureError: + return None + except jwt.InvalidTokenError: + return None + + def decode_token(self, token): + """Decode JWT token and return user data""" + try: + decoded = jwt.decode(token, self.secret, algorithms=["HS256"]) + return decoded # Return the user data + except jwt.ExpiredSignatureError: + return None + except jwt.InvalidTokenError: + return None + def verify_token(self, token): """Verify JWT token""" try: diff --git a/routes/__init__.py b/routes/__init__.py new file mode 100644 index 0000000..1f9185c --- /dev/null +++ b/routes/__init__.py @@ -0,0 +1,3 @@ +from .jwt_routes import create_jwt_routes + +__all__ = ["create_jwt_routes"] \ No newline at end of file diff --git a/routes/jwt_routes.py b/routes/jwt_routes.py new file mode 100644 index 0000000..09bf536 --- /dev/null +++ b/routes/jwt_routes.py @@ -0,0 +1,32 @@ +# flycatch_auth/auth/jwt_routes.py + +from flask import Blueprint, request, jsonify + +auth_bp = Blueprint('auth', __name__, url_prefix='/auth/jwt') + +def create_jwt_routes(auth): + # Delay the import of Auth here to avoid circular import + from flycatch_auth import Auth # Import inside the function + + @auth_bp.route("/login", methods=["POST"]) + def login(): + data = request.json + username = data.get("username") + password = data.get("password") + + response = auth.login(username, password) + if "access_token" in response: + return jsonify(response), 200 + else: + return jsonify({"message": response["error"]}), 401 + + @auth_bp.route("/refresh", methods=["POST"]) + def refresh(): + data = request.json + refresh_token = data.get("refresh_token") + + response = auth.refresh(refresh_token) + if "access_token" in response: + return jsonify(response), 200 + else: + return jsonify({"message": response["error"]}), 401 diff --git a/tests/test_flask_integration.py b/tests/test_flask_integration.py index 3c2cda3..cfe65dc 100644 --- a/tests/test_flask_integration.py +++ b/tests/test_flask_integration.py @@ -1,13 +1,16 @@ import pytest from flask import Flask, jsonify, request -from flycatch_auth import Auth, AuthCoreJwtConfig +from flycatch_auth import Auth, AuthCoreJwtConfig, IdentityService, Identity -class MockUserService: - def load_user(self, username: str): + +class MockUserService(IdentityService): + def load_user(self, username: str) -> Identity: return {"id": "1", "username": "testuser", "password": "password123", "grants": ["read_user"]} + @pytest.fixture def app(): + app = Flask(__name__) auth = Auth( user_service=MockUserService(), @@ -20,9 +23,8 @@ def app(): prefix="/auth/jwt", ), ) - - auth.init_app(app) + auth.init_app(app) @app.route("/auth/jwt/login", methods=["POST"]) def login(): @@ -39,25 +41,30 @@ def login(): return jsonify({"access_token": access_token, "refresh_token": refresh_token}), 200 - @app.route("/me") @auth.verify() + @auth.has_grants(["read_user"])["flask"] def get_curr_user(): return jsonify({"name": "Test User"}), 200 - return app + @pytest.fixture def client(app): return app.test_client() - def test_me_route_with_auth(client): - login_response = client.post("/auth/jwt/login", json={"username": "testuser", "password": "password123"}) - assert login_response.status_code == 200 - token = login_response.json["access_token"] + login_response = client.post( + "/auth/jwt/login", json={"username": "testuser", "password": "password123"}) + + assert login_response.status_code == 200, "Login failed" + + token = login_response.json.get("access_token") + assert token, "Token is missing from response" + + headers = {"Authorization": f"Bearer {token}"} + response = client.get("/me", headers=headers) - response = client.get("/me", headers={"Authorization": f"Bearer {token}"}) - assert response.status_code == 200 + assert response.status_code == 200, "Access denied" assert response.json["name"] == "Test User" From 11179c550f8e236366f0a42bf91d5597ab34bfa0 Mon Sep 17 00:00:00 2001 From: ABINFLYCATCH Date: Thu, 6 Feb 2025 17:15:47 +0530 Subject: [PATCH 14/28] changed to new --- flycatch_auth/__init__.py | 2 +- flycatch_auth/fastapi_auth.py | 2 +- flycatch_auth/flask_auth.py | 4 ++-- flycatch_auth/flask_middleware.py | 15 +++++++++++++++ 4 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 flycatch_auth/flask_middleware.py diff --git a/flycatch_auth/__init__.py b/flycatch_auth/__init__.py index aeef13d..5248460 100644 --- a/flycatch_auth/__init__.py +++ b/flycatch_auth/__init__.py @@ -3,4 +3,4 @@ from .identity import Identity, IdentityService # It will helps to call on top level import these methods -__all__ = ["Auth", "FlaskAuth","AuthCoreJwtConfig", "Identity", "IdentityService"] +__all__ = ["Auth", "FlaskAuth","AuthCoreJwtConfig", "Identity", "IdentityService","authenticate_request"] diff --git a/flycatch_auth/fastapi_auth.py b/flycatch_auth/fastapi_auth.py index f9a3d8f..817458d 100644 --- a/flycatch_auth/fastapi_auth.py +++ b/flycatch_auth/fastapi_auth.py @@ -4,6 +4,7 @@ security = HTTPBearer() + class FastAPIAuth: def __init__(self, user_service, credential_checker, jwt): self.user_service = user_service @@ -14,7 +15,6 @@ async def fastapi_verify(self, request: Request, token: str = Security(security) """Verify JWT token in FastAPI""" if not token.credentials or not self.jwt.verify_token(token.credentials): raise HTTPException(status_code=401, detail="Unauthorized") - return self.jwt.verify_token(token.credentials) # Return user payload def fastapi_has_grants(self, required_grants): diff --git a/flycatch_auth/flask_auth.py b/flycatch_auth/flask_auth.py index ce0110a..5c81eb4 100644 --- a/flycatch_auth/flask_auth.py +++ b/flycatch_auth/flask_auth.py @@ -1,6 +1,6 @@ from flask import request, jsonify from functools import wraps - +from .flask_middleware import authenticate_request # Import the middleware function class FlaskAuth: def __init__(self, user_service, credential_checker, jwt): @@ -10,6 +10,7 @@ def __init__(self, user_service, credential_checker, jwt): def flask_verify(self, func): """Decorator to verify JWT token in Flask""" + @wraps(func) def wrapper(*args, **kwargs): token = request.headers.get("Authorization") if not token: @@ -34,7 +35,6 @@ def decorator(func): @wraps(func) def wrapper(*args, **kwargs): # Check if the user is available in request - if not hasattr(request, "user") or not request.user: return jsonify({"error": "Unauthorized"}), 401 diff --git a/flycatch_auth/flask_middleware.py b/flycatch_auth/flask_middleware.py new file mode 100644 index 0000000..b07bace --- /dev/null +++ b/flycatch_auth/flask_middleware.py @@ -0,0 +1,15 @@ +from flask import request, jsonify + +def authenticate_request(jwt): + """Middleware to authenticate the request by checking the JWT token.""" + token = request.headers.get("Authorization") + if not token: + return jsonify({"message": "Authorization token is missing"}), 401 + + token = token.replace("Bearer ", "") # Remove 'Bearer ' part + user = jwt.verify_token(token) + if not user: + return jsonify({"message": "Invalid or expired token"}), 401 + + # Set the authenticated user in the request context + request.user = user From 246b9c16c976ff5060e1e1fc7e79d68b02fa8ab8 Mon Sep 17 00:00:00 2001 From: ABINFLYCATCH Date: Fri, 7 Feb 2025 17:19:34 +0530 Subject: [PATCH 15/28] complete folder structure changed --- .gitignore => flycatch_auth/.gitignore | 0 LICENSE => flycatch_auth/LICENSE | 0 README.md => flycatch_auth/README.md | 0 flycatch_auth/__init__.py | 12 +- flycatch_auth/auth.py | 124 ++++++++++-------- flycatch_auth/identity.py | 36 ----- flycatch_auth/jwt_auth.py | 77 ----------- flycatch_auth/model_types.py | 37 ++++++ poetry.lock => flycatch_auth/poetry.lock | 0 .../pyproject.toml | 2 +- {routes => flycatch_auth/routes}/__init__.py | 0 flycatch_auth/routes/jwt_routes.py | 83 ++++++++++++ flycatch_auth/services/__init__.py | 4 + flycatch_auth/{ => services}/fastapi_auth.py | 0 flycatch_auth/{ => services}/flask_auth.py | 0 .../{ => services}/flask_middleware.py | 0 flycatch_auth/services/jwt_services.py | 79 +++++++++++ {tests => flycatch_auth/tests}/__init__.py | 0 flycatch_auth/tests/test_auth.py | 99 ++++++++++++++ .../tests}/test_fastapi_integration.py | 0 .../tests}/test_flask_integration.py | 42 ++---- routes/jwt_routes.py | 32 ----- tests/test_auth.py | 115 ---------------- 23 files changed, 390 insertions(+), 352 deletions(-) rename .gitignore => flycatch_auth/.gitignore (100%) rename LICENSE => flycatch_auth/LICENSE (100%) rename README.md => flycatch_auth/README.md (100%) delete mode 100644 flycatch_auth/identity.py delete mode 100644 flycatch_auth/jwt_auth.py create mode 100644 flycatch_auth/model_types.py rename poetry.lock => flycatch_auth/poetry.lock (100%) rename pyproject.toml => flycatch_auth/pyproject.toml (94%) rename {routes => flycatch_auth/routes}/__init__.py (100%) create mode 100644 flycatch_auth/routes/jwt_routes.py create mode 100644 flycatch_auth/services/__init__.py rename flycatch_auth/{ => services}/fastapi_auth.py (100%) rename flycatch_auth/{ => services}/flask_auth.py (100%) rename flycatch_auth/{ => services}/flask_middleware.py (100%) create mode 100644 flycatch_auth/services/jwt_services.py rename {tests => flycatch_auth/tests}/__init__.py (100%) create mode 100644 flycatch_auth/tests/test_auth.py rename {tests => flycatch_auth/tests}/test_fastapi_integration.py (100%) rename {tests => flycatch_auth/tests}/test_flask_integration.py (56%) delete mode 100644 routes/jwt_routes.py delete mode 100644 tests/test_auth.py diff --git a/.gitignore b/flycatch_auth/.gitignore similarity index 100% rename from .gitignore rename to flycatch_auth/.gitignore diff --git a/LICENSE b/flycatch_auth/LICENSE similarity index 100% rename from LICENSE rename to flycatch_auth/LICENSE diff --git a/README.md b/flycatch_auth/README.md similarity index 100% rename from README.md rename to flycatch_auth/README.md diff --git a/flycatch_auth/__init__.py b/flycatch_auth/__init__.py index 5248460..01a5fb5 100644 --- a/flycatch_auth/__init__.py +++ b/flycatch_auth/__init__.py @@ -1,6 +1,8 @@ -from .auth import Auth -from .jwt_auth import AuthCoreJwtConfig -from .identity import Identity, IdentityService +from .auth import AuthCore +from .model_types import Identity, IdentityService +from .services import AuthCoreJwtConfig, JwtService -# It will helps to call on top level import these methods -__all__ = ["Auth", "FlaskAuth","AuthCoreJwtConfig", "Identity", "IdentityService","authenticate_request"] +auth = AuthCore() + +# It enables top-level imports like `from flycatch_auth import auth, AuthCoreJwtConfig, JwtService` +__all__ = ["auth", "Identity", "IdentityService", "AuthCoreJwtConfig", "JwtService"] \ No newline at end of file diff --git a/flycatch_auth/auth.py b/flycatch_auth/auth.py index 360309f..64b6242 100644 --- a/flycatch_auth/auth.py +++ b/flycatch_auth/auth.py @@ -1,63 +1,71 @@ -from typing import Optional -from routes.jwt_routes import create_jwt_routes -from .flask_auth import FlaskAuth -from .identity import Identity -# from .fastapi_auth import FastAPIAuth +import logging +# from flask_session import Session +# from authlib.integrations.flask_client import OAuth +from .model_types import IdentityService, Identity, CredentialChecker +from .services import AuthCoreJwtConfig, JwtService +from .routes import create_jwt_routes +logger = logging.getLogger("auth_core") -class Auth(FlaskAuth): - def __init__(self, user_service, credential_checker, jwt=None): + +class AuthCore: + def __init__(self): + self.configurations = {} + self.app = None + self.oauth = None + + def init_app(self, app, user_service: IdentityService, credential_checker: CredentialChecker, jwt: AuthCoreJwtConfig): + self.app = app + self.jwt = jwt self.user_service = user_service self.credential_checker = credential_checker - self.jwt = jwt + + if jwt and jwt.enable: + JwtService(user_service, credential_checker, jwt) + create_jwt_routes(app, jwt, user_service, credential_checker) + + + # if configurations.get("session", {}).get("enabled", False): + # self.setup_session(app, configurations["session"]) + + # if configurations.get("google", {}).get("enabled", False): + # self.setup_google_auth(app, configurations["google"]) + + # def setup_session(self, app, session_config): + # app.config["SESSION_TYPE"] = session_config.get("type", "filesystem") + # app.config["SESSION_PERMANENT"] = session_config.get("permanent", False) + # app.config["SESSION_USE_SIGNER"] = session_config.get("use_signer", True) + # app.config["SESSION_KEY_PREFIX"] = session_config.get("key_prefix", "sess:") + # Session(app) + + # def setup_google_auth(self, app, google_config): + # self.oauth = OAuth(app) + # self.oauth.register( + # "google", + # client_id=google_config["clientID"], + # client_secret=google_config["clientSecret"], + # authorize_url="https://accounts.google.com/o/oauth2/auth", + # authorize_params=None, + # access_token_url="https://accounts.google.com/o/oauth2/token", + # access_token_params=None, + # client_kwargs={"scope": "openid email profile"}, + # ) + + + + + # def verify_google(self): + # def decorator(func): + # @wraps(func) + # def wrapper(*args, **kwargs): + # user = session.get("user") + # if not user: + # logger.warning("Unauthorized Google OAuth access attempt") + # return jsonify({"error": "Unauthorized"}), 401 + # request.user = user + # return func(*args, **kwargs) + # return wrapper + # return decorator + - # Initialize FlaskAuth and FastAPIAuth - FlaskAuth.__init__(self, user_service, credential_checker, jwt) - # FastAPIAuth.__init__(self, user_service, credrintrint(user["password"]) (user["password"]) ential_checker, jwt) - - def init_app(self, app): - """Initialize Auth with Flask""" - - app.auth = self - create_jwt_routes(self) - - def authenticate(self, username: str, password: str) -> Optional[Identity]: - """Use to authenticate user""" - user = self.user_service.load_user(username) - if user and self.credential_checker(password, user["password"]): - return user - return None - - def login(self, username, password): - """Authenticate user and return JWT token""" - user = self.authenticate(username, password) - if user: - access_token = self.jwt.generate_token(user) - refresh_token = self.jwt.generate_refresh_token(user) - return {"access_token": access_token, "refresh_token": refresh_token} - return {"error": "Invalid credentials"} - - def refresh(self, refresh_token: str): - """Refresh the access token using a valid refresh token""" - user = self.jwt.verify_refresh_token(refresh_token) - if user: - access_token = self.jwt.generate_token(user) - return {"access_token": access_token} - return {"error": "Invalid refresh token"} - - def verify(self): - """Verify token for Flask or FastAPI""" - def wrapper(func): - if hasattr(self, 'flask_verify'): - return self.flask_verify(func) # Flask verification - elif hasattr(self, 'fastapi_verify'): - return self.fastapi_verify(func) # FastAPI verification - return func - return wrapper - - def has_grants(self, required_grants): - """General method to return the correct grant-based verification""" - return { - "flask": self.flask_has_grants(required_grants), - # "fastapi": self.fastapi_has_grants(required_grants), - } +auth = AuthCore() diff --git a/flycatch_auth/identity.py b/flycatch_auth/identity.py deleted file mode 100644 index a6a6e95..0000000 --- a/flycatch_auth/identity.py +++ /dev/null @@ -1,36 +0,0 @@ -from abc import ABC, abstractmethod -from typing import List, Optional - - -class Identity(ABC): - """Manage user authentication and authorization.""" - - def __init__(self, user_id: str, username: str, grants: List[str]): - self.id = user_id - self.username = username - self.grants = grants - - @abstractmethod - def has_grant(self, grant: str) -> bool: - """Check if the identity has a specific grant.""" - pass - - -class IdentityService(ABC): - @abstractmethod - def load_user(self, username: str) -> Optional[Identity]: - """Override this method to fetch a user from DB.""" - pass - - -# class MockUserService(IdentityService): -# def load_user(self, username: str) -> Identity: -# return Identity(user_id="1", username="testuser", grants=["read_user"]) - -# def create_user(self, username: str, password: str) -> Identity: -# # Mock implementation for creating a user -# return Identity(user_id="2", username=username, grants=[]) - -# def validate_password(self, password: str, hashed_password: str) -> bool: -# # Mock implementation for validating a password -# return password == hashed_password diff --git a/flycatch_auth/jwt_auth.py b/flycatch_auth/jwt_auth.py deleted file mode 100644 index 373e547..0000000 --- a/flycatch_auth/jwt_auth.py +++ /dev/null @@ -1,77 +0,0 @@ -import jwt -import datetime - - -class AuthCoreJwtConfig: - def __init__(self, enable, secret, expiresIn, refresh, prefix): - self.enable = enable - self.secret = secret - self.expiresIn = expiresIn - self.refresh = refresh - self.prefix = prefix - - def generate_token(self, user_data): - """Generate JWT token""" - payload = { - "sub": user_data["id"], - "exp": datetime.datetime.utcnow() + datetime.timedelta(hours=int(self.expiresIn.replace("h", ""))), - "username": user_data["username"], - "grants": user_data["grants"], - } - token = jwt.encode(payload, self.secret, algorithm="HS256") - - if isinstance(token, bytes): - token = token.decode("utf-8") - return token - - def generate_refresh_token(self, user_data): - """Generate JWT refresh token""" - payload = { - "sub": user_data["id"], - "exp": datetime.datetime.utcnow() + datetime.timedelta(days=7), - } - token = jwt.encode(payload, self.secret, algorithm="HS256") - - if isinstance(token, bytes): - token = token.decode("utf-8") - return token - - def decode_token(self, token): - """Decode JWT token and return user data""" - try: - decoded = jwt.decode(token, self.secret, algorithms=["HS256"]) - return decoded # Return the user data - except jwt.ExpiredSignatureError: - return None - except jwt.InvalidTokenError: - return None - - def decode_token(self, token): - """Decode JWT token and return user data""" - try: - decoded = jwt.decode(token, self.secret, algorithms=["HS256"]) - return decoded # Return the user data - except jwt.ExpiredSignatureError: - return None - except jwt.InvalidTokenError: - return None - - def verify_token(self, token): - """Verify JWT token""" - try: - jwt.decode(token, self.secret, algorithms=["HS256"]) - return True - except jwt.ExpiredSignatureError: - return False - except jwt.InvalidTokenError: - return False - - def verify_refresh_token(self, token): - """Verify JWT refresh token""" - try: - jwt.decode(token, self.secret, algorithms=["HS256"]) - return True - except jwt.ExpiredSignatureError: - return False - except jwt.InvalidTokenError: - return False diff --git a/flycatch_auth/model_types.py b/flycatch_auth/model_types.py new file mode 100644 index 0000000..62edc7c --- /dev/null +++ b/flycatch_auth/model_types.py @@ -0,0 +1,37 @@ +from abc import ABC, abstractmethod +from typing import List, Optional, TypedDict + + +class Identity: + """Manage user authentication and authorization.""" + + def __init__(self, user_id: str, username: str, grants: List[str]): + self.id = user_id + self.username = username + self.grants = grants + + def has_grant(self, grant: str) -> bool: + """Check if the identity has a specific grant.""" + return grant in self.grants + + +class IdentityService(ABC): + @abstractmethod + def load_user(self, username: str) -> Optional[Identity]: + """Override this method to fetch a user from DB.""" + pass + + +class UserType(TypedDict): + id: str + username: str + password: str + +class CredentialChecker: + def __init__(self): + pass + + def verify(self, input_password: str, user_password: str) -> bool: + """Checks if the provided password matches the stored password.""" + return input_password == user_password + diff --git a/poetry.lock b/flycatch_auth/poetry.lock similarity index 100% rename from poetry.lock rename to flycatch_auth/poetry.lock diff --git a/pyproject.toml b/flycatch_auth/pyproject.toml similarity index 94% rename from pyproject.toml rename to flycatch_auth/pyproject.toml index 223b52f..7d041b8 100644 --- a/pyproject.toml +++ b/flycatch_auth/pyproject.toml @@ -2,7 +2,7 @@ name = "flycatch_auth" version = "0.1.0" description = "A Python authentication package supporting Flask and FastAPI" -authors = ["Your Name "] +authors = ["flycatch"] license = "MIT" readme = "README.md" repository = "https://github.com/yourusername/flycatch_auth" diff --git a/routes/__init__.py b/flycatch_auth/routes/__init__.py similarity index 100% rename from routes/__init__.py rename to flycatch_auth/routes/__init__.py diff --git a/flycatch_auth/routes/jwt_routes.py b/flycatch_auth/routes/jwt_routes.py new file mode 100644 index 0000000..9fcff9b --- /dev/null +++ b/flycatch_auth/routes/jwt_routes.py @@ -0,0 +1,83 @@ +from flask import Blueprint, request, jsonify +import jwt +import datetime +import logging + +logger = logging.getLogger(__name__) + + +def create_jwt_routes(app, config, user_service, credential_checker): + router = Blueprint("jwt_auth", __name__) + prefix = config.prefix or "/auth/jwt" + + def create_access_token(user): + """Generate JWT Access Token""" + payload = { + "id": user["id"], + "username": user["username"], + "type": "access", + "exp": datetime.datetime.utcnow() + datetime.timedelta(hours=8), + } + return jwt.encode(payload, config.secret, algorithm="HS256") + + def create_refresh_token(user): + """Generate JWT Refresh Token""" + payload = { + "id": user["id"], + "username": user["username"], + "type": "refresh", + "exp": datetime.datetime.utcnow() + datetime.timedelta(days=7), + } + return jwt.encode(payload, config.secret, algorithm="HS256") + + @router.route(f"{prefix}/login", methods=["POST"]) + def login(): + """Login route to generate access and refresh tokens""" + data = request.json + username, password = data.get("username"), data.get("password") + logger.info(f"Login attempt for username: {username}") + + user = user_service.load_user(username) + if not user or not credential_checker(password, user["password"]): + logger.warning(f"Login failed for username: {username}") + return jsonify({"error": "Invalid username or password"}), 401 + + access_token = create_access_token(user) + refresh_token = create_refresh_token(user) + logger.info(f"Login successful for username: {username}") + return jsonify({"access_token": access_token, "refresh_token": refresh_token}) + + @router.route(f"{prefix}/refresh", methods=["POST"]) + def refresh(): + """Refresh access token using a valid refresh token""" + auth_header = request.headers.get("Authorization") + logger.info("Refresh token attempt received") + if not auth_header: + logger.warning("Refresh token missing in request") + return jsonify({"error": "Refresh token is required"}), 400 + + try: + refresh_token = auth_header.split(" ")[1] + decoded_token = jwt.decode(refresh_token, config.secret, algorithms=["HS256"]) + + if decoded_token.get("type") != "refresh": + logger.warning("Invalid token type for refresh") + return jsonify({"error": "Invalid token type"}), 403 + + user = {"id": decoded_token["id"], "username": decoded_token["username"]} + access_token = create_access_token(user) + new_refresh_token = create_refresh_token(user) + + logger.info(f"Access token refreshed for username: {user['username']}") + return jsonify({"access_token": access_token, "refresh_token": new_refresh_token}) + except jwt.ExpiredSignatureError: + logger.warning("Refresh token expired") + return jsonify({"error": "Refresh token expired"}), 403 + except jwt.InvalidTokenError: + logger.warning("Invalid refresh token provided") + return jsonify({"error": "Invalid refresh token"}), 403 + except Exception as e: + logger.error("JWT Refresh Error", exc_info=True) + return jsonify({"error": "Internal Server Error"}), 500 + + app.register_blueprint(router) diff --git a/flycatch_auth/services/__init__.py b/flycatch_auth/services/__init__.py new file mode 100644 index 0000000..9ac1810 --- /dev/null +++ b/flycatch_auth/services/__init__.py @@ -0,0 +1,4 @@ +from .jwt_services import AuthCoreJwtConfig, JwtService + +# It will helps to call on top level import these methods +__all__ = ["AuthCoreJwtConfig", "JwtService"], diff --git a/flycatch_auth/fastapi_auth.py b/flycatch_auth/services/fastapi_auth.py similarity index 100% rename from flycatch_auth/fastapi_auth.py rename to flycatch_auth/services/fastapi_auth.py diff --git a/flycatch_auth/flask_auth.py b/flycatch_auth/services/flask_auth.py similarity index 100% rename from flycatch_auth/flask_auth.py rename to flycatch_auth/services/flask_auth.py diff --git a/flycatch_auth/flask_middleware.py b/flycatch_auth/services/flask_middleware.py similarity index 100% rename from flycatch_auth/flask_middleware.py rename to flycatch_auth/services/flask_middleware.py diff --git a/flycatch_auth/services/jwt_services.py b/flycatch_auth/services/jwt_services.py new file mode 100644 index 0000000..26e48a0 --- /dev/null +++ b/flycatch_auth/services/jwt_services.py @@ -0,0 +1,79 @@ +import jwt +import datetime +from typing import Optional +from dataclasses import dataclass +from flycatch_auth.model_types import Identity, IdentityService + + +@dataclass +class AuthCoreJwtConfig: + enable: bool + secret: str + expiresIn: str + refresh: bool + prefix: str + + def generate_token(self, user_data: dict) -> str: + """Generate JWT access token""" + exp_hours = int(self.expiresIn.replace("h", "")) + payload = { + "sub": user_data["id"], + "exp": datetime.datetime.utcnow() + datetime.timedelta(hours=exp_hours), + "username": user_data["username"], + "grants": user_data["grants"], + } + return jwt.encode(payload, self.secret, algorithm="HS256") + + def generate_refresh_token(self, user_data: dict) -> str: + """Generate JWT refresh token""" + payload = { + "sub": user_data["id"], + "exp": datetime.datetime.utcnow() + datetime.timedelta(days=7), + } + return jwt.encode(payload, self.secret, algorithm="HS256") + + def decode_token(self, token: str) -> Optional[dict]: + """Decode JWT token and return user data""" + try: + return jwt.decode(token, self.secret, algorithms=["HS256"]) + except (jwt.ExpiredSignatureError, jwt.InvalidTokenError): + return None + + def verify_token(self, token: str) -> bool: + """Verify JWT token""" + return self.decode_token(token) is not None + + def verify_refresh_token(self, token: str) -> bool: + """Verify JWT refresh token""" + return self.decode_token(token) is not None + + +class JwtService: + def __init__(self, user_service: IdentityService, credential_checker, jwt_config: AuthCoreJwtConfig): + self.user_service = user_service + self.credential_checker = credential_checker + self.jwt_config = jwt_config + + def authenticate(self, username: str, password: str) -> Optional[Identity]: + """Authenticate user""" + user = self.user_service.load_user(username) + if user and self.credential_checker(password, user["password"]): + return user + return None + + def login(self, username: str, password: str) -> dict: + """Authenticate user and return JWT tokens""" + user = self.authenticate(username, password) + if user: + return { + "access_token": self.jwt_config.generate_token(user), + "refresh_token": self.jwt_config.generate_refresh_token(user), + } + return {"error": "Invalid credentials"} + + def refresh(self, refresh_token: str) -> dict: + """Refresh access token using a valid refresh token""" + user_data = self.jwt_config.decode_token(refresh_token) + if user_data: + return {"access_token": self.jwt_config.generate_token(user_data)} + return {"error": "Invalid refresh token"} diff --git a/tests/__init__.py b/flycatch_auth/tests/__init__.py similarity index 100% rename from tests/__init__.py rename to flycatch_auth/tests/__init__.py diff --git a/flycatch_auth/tests/test_auth.py b/flycatch_auth/tests/test_auth.py new file mode 100644 index 0000000..a3dcd09 --- /dev/null +++ b/flycatch_auth/tests/test_auth.py @@ -0,0 +1,99 @@ +import pytest +from flask import Flask, request, jsonify +from flycatch_auth import auth, AuthCoreJwtConfig, IdentityService, Identity + + +class MockUserService(IdentityService): + def load_user(self, username: str)-> Identity: + return { + "id": "1", + "username": "testuser", + "password": "password123", + "grants": ["read_user"], + } + + +@pytest.fixture +def app(): + app = Flask(__name__) + app.config["TESTING"] = True + + jwt_config = AuthCoreJwtConfig( + enable=True, + secret="mysecret", + expiresIn="2h", + refresh=True, + prefix="/auth/jwt", + ) + + auth.init_app( + app=app, + user_service=MockUserService(), + credential_checker=lambda input, user: input == user, + jwt=jwt_config, + ) + + @app.post("/auth/jwt/login") + def login(): + username = request.json.get("username") + password = request.json.get("password") + user = auth.authenticate(username, password) + if user: + tokens = auth.generate_tokens(user) + return jsonify(tokens), 200 + return jsonify({"message": "Invalid credentials"}), 401 + + @app.post("/auth/jwt/refresh") + def refresh(): + refresh_token = request.json.get("refresh_token") + new_token = auth.refresh_access_token(refresh_token) + if new_token: + return jsonify({"access_token": new_token}), 200 + return jsonify({"message": "Invalid refresh token"}), 401 + + @app.get("/me") + # @auth.verify() + def protected(): + return jsonify({"message": "Access granted"}), 200 + + return app + + +@pytest.fixture +def client(app): + return app.test_client() + + +def test_jwt_token_generation(client): + response = client.post( + "/auth/jwt/login", json={"username": "testuser", "password": "password123"} + ) + assert response.status_code == 200 + assert "access_token" in response.json + assert "refresh_token" in response.json + + +def test_protected_route_access(client): + login_response = client.post( + "/auth/jwt/login", json={"username": "testuser", "password": "password123"} + ) + token = login_response.json["access_token"] + + response = client.get("/me", headers={"Authorization": f"Bearer {token}"}) + assert response.status_code == 200 + assert response.json["message"] == "Access granted" + + +def test_refresh_token(client): + login_response = client.post( + "/auth/jwt/login", json={"username": "testuser", "password": "password123"} + ) + refresh_token = login_response.json.get("refresh_token") + print(refresh_token) + + refresh_response = client.post( + "/auth/jwt/refresh", + headers={"Authorization": f"Bearer {refresh_token}"}, + ) + assert refresh_response.status_code == 200 + assert "access_token" in refresh_response.json diff --git a/tests/test_fastapi_integration.py b/flycatch_auth/tests/test_fastapi_integration.py similarity index 100% rename from tests/test_fastapi_integration.py rename to flycatch_auth/tests/test_fastapi_integration.py diff --git a/tests/test_flask_integration.py b/flycatch_auth/tests/test_flask_integration.py similarity index 56% rename from tests/test_flask_integration.py rename to flycatch_auth/tests/test_flask_integration.py index cfe65dc..156af51 100644 --- a/tests/test_flask_integration.py +++ b/flycatch_auth/tests/test_flask_integration.py @@ -1,6 +1,6 @@ import pytest from flask import Flask, jsonify, request -from flycatch_auth import Auth, AuthCoreJwtConfig, IdentityService, Identity +from flycatch_auth import auth, AuthCoreJwtConfig, IdentityService, Identity class MockUserService(IdentityService): @@ -12,38 +12,24 @@ def load_user(self, username: str) -> Identity: def app(): app = Flask(__name__) - auth = Auth( + jwt_config = AuthCoreJwtConfig( + enable=True, + secret="mysecret", + expiresIn="2h", + refresh=True, + prefix="/auth/jwt", + ) + + auth.init_app( + app=app, user_service=MockUserService(), credential_checker=lambda input, user: input == user, - jwt=AuthCoreJwtConfig( - enable=True, - secret="test-secret", - expiresIn="1h", - refresh=True, - prefix="/auth/jwt", - ), + jwt=jwt_config, ) - auth.init_app(app) - - @app.route("/auth/jwt/login", methods=["POST"]) - def login(): - data = request.json - username = data.get("username") - password = data.get("password") - - user = auth.authenticate(username, password) - if not user: - return jsonify({"message": "Invalid credentials"}), 401 - - access_token = auth.jwt.generate_token(user) - refresh_token = auth.jwt.generate_refresh_token(user) - - return jsonify({"access_token": access_token, "refresh_token": refresh_token}), 200 - @app.route("/me") - @auth.verify() - @auth.has_grants(["read_user"])["flask"] + # @auth.verify() + # @auth.has_grants(["read_user"])["flask"] def get_curr_user(): return jsonify({"name": "Test User"}), 200 return app diff --git a/routes/jwt_routes.py b/routes/jwt_routes.py deleted file mode 100644 index 09bf536..0000000 --- a/routes/jwt_routes.py +++ /dev/null @@ -1,32 +0,0 @@ -# flycatch_auth/auth/jwt_routes.py - -from flask import Blueprint, request, jsonify - -auth_bp = Blueprint('auth', __name__, url_prefix='/auth/jwt') - -def create_jwt_routes(auth): - # Delay the import of Auth here to avoid circular import - from flycatch_auth import Auth # Import inside the function - - @auth_bp.route("/login", methods=["POST"]) - def login(): - data = request.json - username = data.get("username") - password = data.get("password") - - response = auth.login(username, password) - if "access_token" in response: - return jsonify(response), 200 - else: - return jsonify({"message": response["error"]}), 401 - - @auth_bp.route("/refresh", methods=["POST"]) - def refresh(): - data = request.json - refresh_token = data.get("refresh_token") - - response = auth.refresh(refresh_token) - if "access_token" in response: - return jsonify(response), 200 - else: - return jsonify({"message": response["error"]}), 401 diff --git a/tests/test_auth.py b/tests/test_auth.py deleted file mode 100644 index 22a9e39..0000000 --- a/tests/test_auth.py +++ /dev/null @@ -1,115 +0,0 @@ -import pytest -from flask import Flask, request, jsonify -from flycatch_auth import Auth, AuthCoreJwtConfig -import jwt - -class MockUserService: - def load_user(self, username: str): - return { - "id": "1", - "username": "testuser", - "password": "password123", - "grants": ["read_user"], - } - - -@pytest.fixture -def app(): - app = Flask(__name__) - app.config["TESTING"] = True - - auth = Auth( - user_service=MockUserService(), - credential_checker=lambda input, user: input == user, - jwt=AuthCoreJwtConfig( - enable=True, - secret="test-secret", - expiresIn="1h", - refresh=True, - prefix="/auth/jwt", - ), - ) - - auth.init_app(app) - - @app.route("/auth/jwt/login", methods=["POST"]) - def login(): - username = request.json.get("username") - password = request.json.get("password") - user = auth.authenticate(username, password) - if user: - access_token = auth.jwt.generate_token(user) - refresh_token = auth.jwt.generate_refresh_token(user) - return jsonify({"access_token": access_token, "refresh_token": refresh_token}), 200 - else: - return jsonify({"message": "Invalid credentials"}), 401 - - @app.route("/auth/jwt/refresh", methods=["POST"]) - def refresh(): - refresh_token = request.json.get("refresh_token") - if not refresh_token or not auth.jwt.verify_refresh_token(refresh_token): - return jsonify({"message": "Invalid refresh token"}), 401 - - # Assuming user ID is stored in the token payload - decoded_token = jwt.decode(refresh_token, auth.jwt.secret, algorithms=["HS256"]) - user_id = decoded_token.get("sub") - - # Load user using MockUserService or actual UserService - user = auth.user_service.load_user(user_id) - - if user: - new_access_token = auth.jwt.generate_token(user) - return jsonify({"access_token": new_access_token}), 200 - else: - return jsonify({"message": "User not found"}), 404 - - - @app.route("/protected") - @auth.verify() - def protected(): - return jsonify({"message": "Access granted"}), 200 - - return app - - -@pytest.fixture -def client(app): - return app.test_client() - - -def test_jwt_token_generation(client): - print("inside test_jwt_token_generation") - response = client.post( - "/auth/jwt/login", json={"username": "testuser", "password": "password123"}) - assert response.status_code == 200 - assert "access_token" in response.json - assert "refresh_token" in response.json - - -def test_protected_route_access(client): - """Test access to protected route with valid JWT token""" - login_response = client.post( - "/auth/jwt/login", json={"username": "testuser", "password": "password123"}) - token = login_response.json["access_token"] - - response = client.get( - "/protected", headers={"Authorization": f"Bearer {token}"}) - assert response.status_code == 200 - assert response.json["message"] == "Access granted" - - -def test_refresh_token(client): - """Test refresh token endpoint""" - # First, get a valid access token (using the login flow) - login_response = client.post( - "/auth/jwt/login", json={"username": "testuser", "password": "password123"}) - assert login_response.status_code == 200 - refresh_token = login_response.json.get("refresh_token") - - # Then, test the refresh endpoint - refresh_response = client.post( - "/auth/jwt/refresh", json={"refresh_token": refresh_token}) - print("token...") - print(refresh_response.json) - assert refresh_response.status_code == 200 - assert "access_token" in refresh_response.json From 6ebc286bc6a892a679069031f8b53677c571d7b2 Mon Sep 17 00:00:00 2001 From: ABINFLYCATCH Date: Mon, 10 Feb 2025 12:47:21 +0530 Subject: [PATCH 16/28] test and changed the floder and structure --- flycatch_auth/LICENSE => LICENSE | 0 flycatch_auth/README.md => README.md | 12 - __pycache__/hello.cpython-312.pyc | Bin 0 -> 1833 bytes dist/flycatch_auth-0.1.0-py3-none-any.whl | Bin 0 -> 20286 bytes dist/flycatch_auth-0.1.0.tar.gz | Bin 0 -> 16275 bytes flycatch_auth/__init__.py | 4 +- flycatch_auth/auth.py | 73 +-- flycatch_auth/poetry.lock | 513 ------------------ flycatch_auth/pyproject.toml | 34 -- flycatch_auth/services/__init__.py | 7 +- flycatch_auth/services/base.py | 25 + flycatch_auth/services/flask_middleware.py | 15 - flycatch_auth/services/jwt_services.py | 24 +- flycatch_auth/services/session_service.py | 18 + flycatch_auth/tests/test_auth.py | 16 +- .../tests/test_fastapi_integration.py | 2 +- poetry.lock | 267 +++++++++ pyproject.toml | 21 + {flycatch_auth/tests => tests}/__init__.py | 0 tests/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 164 bytes .../test_auth.cpython-312-pytest-7.4.4.pyc | Bin 0 -> 8561 bytes .../test_auth.cpython-312-pytest-8.3.4.pyc | Bin 0 -> 8826 bytes ...i_integration.cpython-312-pytest-7.4.4.pyc | Bin 0 -> 3121 bytes ...k_integration.cpython-312-pytest-7.4.4.pyc | Bin 0 -> 4876 bytes ...k_integration.cpython-312-pytest-8.3.4.pyc | Bin 0 -> 5000 bytes tests/test_auth.py | 99 ++++ tests/test_fastapi_integration.py | 38 ++ .../tests => tests}/test_flask_integration.py | 0 28 files changed, 517 insertions(+), 651 deletions(-) rename flycatch_auth/LICENSE => LICENSE (100%) rename flycatch_auth/README.md => README.md (81%) create mode 100644 __pycache__/hello.cpython-312.pyc create mode 100644 dist/flycatch_auth-0.1.0-py3-none-any.whl create mode 100644 dist/flycatch_auth-0.1.0.tar.gz delete mode 100644 flycatch_auth/poetry.lock delete mode 100644 flycatch_auth/pyproject.toml create mode 100644 flycatch_auth/services/base.py delete mode 100644 flycatch_auth/services/flask_middleware.py create mode 100644 flycatch_auth/services/session_service.py create mode 100644 poetry.lock create mode 100644 pyproject.toml rename {flycatch_auth/tests => tests}/__init__.py (100%) create mode 100644 tests/__pycache__/__init__.cpython-312.pyc create mode 100644 tests/__pycache__/test_auth.cpython-312-pytest-7.4.4.pyc create mode 100644 tests/__pycache__/test_auth.cpython-312-pytest-8.3.4.pyc create mode 100644 tests/__pycache__/test_fastapi_integration.cpython-312-pytest-7.4.4.pyc create mode 100644 tests/__pycache__/test_flask_integration.cpython-312-pytest-7.4.4.pyc create mode 100644 tests/__pycache__/test_flask_integration.cpython-312-pytest-8.3.4.pyc create mode 100644 tests/test_auth.py create mode 100644 tests/test_fastapi_integration.py rename {flycatch_auth/tests => tests}/test_flask_integration.py (100%) diff --git a/flycatch_auth/LICENSE b/LICENSE similarity index 100% rename from flycatch_auth/LICENSE rename to LICENSE diff --git a/flycatch_auth/README.md b/README.md similarity index 81% rename from flycatch_auth/README.md rename to README.md index ef720a4..f87c1f7 100644 --- a/flycatch_auth/README.md +++ b/README.md @@ -53,18 +53,6 @@ auth = Auth( auth.init_app(app) -@app.route("/auth/jwt/login", methods=["POST"]) -def login(): - data = request.json - username = data.get("username") - password = data.get("password") - user = auth.authenticate(username, password) - if not user: - return jsonify({"message": "Invalid credentials"}), 401 - - access_token = auth.jwt.generate_token(user) - refresh_token = auth.jwt.generate_refresh_token(user) - return jsonify({"access_token": access_token, "refresh_token": refresh_token}), 200 @app.route("/me") @auth.verify() diff --git a/__pycache__/hello.cpython-312.pyc b/__pycache__/hello.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..66ac2603eb0515198912eb13fcad85533cd4a0d1 GIT binary patch literal 1833 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 literal 0 HcmV?d00001 diff --git a/dist/flycatch_auth-0.1.0.tar.gz b/dist/flycatch_auth-0.1.0.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..c25db5142feb53088d6d4fdebc2299cbbd094cd1 GIT binary patch literal 16275 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 literal 0 HcmV?d00001 diff --git a/flycatch_auth/__init__.py b/flycatch_auth/__init__.py index 01a5fb5..1a4365f 100644 --- a/flycatch_auth/__init__.py +++ b/flycatch_auth/__init__.py @@ -1,8 +1,8 @@ from .auth import AuthCore from .model_types import Identity, IdentityService -from .services import AuthCoreJwtConfig, JwtService +from .services import AuthCoreJwtConfig auth = AuthCore() # It enables top-level imports like `from flycatch_auth import auth, AuthCoreJwtConfig, JwtService` -__all__ = ["auth", "Identity", "IdentityService", "AuthCoreJwtConfig", "JwtService"] \ No newline at end of file +__all__ = ["auth", "Identity", "IdentityService", "AuthCoreJwtConfig"] \ No newline at end of file diff --git a/flycatch_auth/auth.py b/flycatch_auth/auth.py index 64b6242..7f55522 100644 --- a/flycatch_auth/auth.py +++ b/flycatch_auth/auth.py @@ -1,71 +1,42 @@ import logging # from flask_session import Session # from authlib.integrations.flask_client import OAuth -from .model_types import IdentityService, Identity, CredentialChecker -from .services import AuthCoreJwtConfig, JwtService +from .model_types import IdentityService, CredentialChecker +from .services import AuthCoreJwtConfig, JwtAuthService from .routes import create_jwt_routes - logger = logging.getLogger("auth_core") class AuthCore: def __init__(self): - self.configurations = {} self.app = None - self.oauth = None - - def init_app(self, app, user_service: IdentityService, credential_checker: CredentialChecker, jwt: AuthCoreJwtConfig): + self.jwt = None + self.user_service = None + self.credential_checker = None + self.auth_service = None + + def init_app( + self, + app, + user_service: IdentityService, + credential_checker: CredentialChecker, + jwt: AuthCoreJwtConfig, + ): + """Initialize authentication with JWT if enabled.""" self.app = app self.jwt = jwt self.user_service = user_service self.credential_checker = credential_checker - - if jwt and jwt.enable: - JwtService(user_service, credential_checker, jwt) - create_jwt_routes(app, jwt, user_service, credential_checker) - - - # if configurations.get("session", {}).get("enabled", False): - # self.setup_session(app, configurations["session"]) - # if configurations.get("google", {}).get("enabled", False): - # self.setup_google_auth(app, configurations["google"]) - - # def setup_session(self, app, session_config): - # app.config["SESSION_TYPE"] = session_config.get("type", "filesystem") - # app.config["SESSION_PERMANENT"] = session_config.get("permanent", False) - # app.config["SESSION_USE_SIGNER"] = session_config.get("use_signer", True) - # app.config["SESSION_KEY_PREFIX"] = session_config.get("key_prefix", "sess:") - # Session(app) - - # def setup_google_auth(self, app, google_config): - # self.oauth = OAuth(app) - # self.oauth.register( - # "google", - # client_id=google_config["clientID"], - # client_secret=google_config["clientSecret"], - # authorize_url="https://accounts.google.com/o/oauth2/auth", - # authorize_params=None, - # access_token_url="https://accounts.google.com/o/oauth2/token", - # access_token_params=None, - # client_kwargs={"scope": "openid email profile"}, - # ) - - + if jwt and jwt.enable: + # Initialize JWT authentication service + self.auth_service = JwtAuthService( + user_service, credential_checker, jwt) + # Set up JWT authentication routes + create_jwt_routes(app, jwt, user_service, credential_checker) - # def verify_google(self): - # def decorator(func): - # @wraps(func) - # def wrapper(*args, **kwargs): - # user = session.get("user") - # if not user: - # logger.warning("Unauthorized Google OAuth access attempt") - # return jsonify({"error": "Unauthorized"}), 401 - # request.user = user - # return func(*args, **kwargs) - # return wrapper - # return decorator + return self.auth_service auth = AuthCore() diff --git a/flycatch_auth/poetry.lock b/flycatch_auth/poetry.lock deleted file mode 100644 index 39a8b7f..0000000 --- a/flycatch_auth/poetry.lock +++ /dev/null @@ -1,513 +0,0 @@ -# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. - -[[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"] -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 = "anyio" -version = "4.8.0" -description = "High level compatibility layer for multiple asynchronous event loop implementations" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a"}, - {file = "anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a"}, -] - -[package.dependencies] -idna = ">=2.8" -sniffio = ">=1.1" -typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} - -[package.extras] -doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] -trio = ["trio (>=0.26.1)"] - -[[package]] -name = "blinker" -version = "1.9.0" -description = "Fast, simple object-to-object and broadcast signaling" -optional = false -python-versions = ">=3.9" -groups = ["main"] -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 = "click" -version = "8.1.8" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, - {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[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"] -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 = "sys_platform == \"win32\""} - -[[package]] -name = "fastapi" -version = "0.115.8" -description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "fastapi-0.115.8-py3-none-any.whl", hash = "sha256:753a96dd7e036b34eeef8babdfcfe3f28ff79648f86551eb36bfc1b0bf4a8cbf"}, - {file = "fastapi-0.115.8.tar.gz", hash = "sha256:0ce9111231720190473e222cdf0f07f7206ad7e53ea02beb1d2dc36e2f0741e9"}, -] - -[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.46.0" -typing-extensions = ">=4.8.0" - -[package.extras] -all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.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.5)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] - -[[package]] -name = "flask" -version = "3.1.0" -description = "A simple framework for building complex web applications." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "flask-3.1.0-py3-none-any.whl", hash = "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136"}, - {file = "flask-3.1.0.tar.gz", hash = "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac"}, -] - -[package.dependencies] -blinker = ">=1.9" -click = ">=8.1.3" -itsdangerous = ">=2.2" -Jinja2 = ">=3.1.2" -Werkzeug = ">=3.1" - -[package.extras] -async = ["asgiref (>=3.2)"] -dotenv = ["python-dotenv"] - -[[package]] -name = "idna" -version = "3.10" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.6" -groups = ["main"] -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 = "iniconfig" -version = "2.0.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, -] - -[[package]] -name = "itsdangerous" -version = "2.2.0" -description = "Safely pass data to untrusted environments and back." -optional = false -python-versions = ">=3.8" -groups = ["main"] -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 = "jinja2" -version = "3.1.5" -description = "A very fast and expressive template engine." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, - {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, -] - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "markupsafe" -version = "3.0.2" -description = "Safely add untrusted strings to HTML/XML markup." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, - {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, -] - -[[package]] -name = "packaging" -version = "24.2" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, - {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, -] - -[[package]] -name = "pluggy" -version = "1.5.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, - {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] - -[[package]] -name = "pydantic" -version = "2.10.6" -description = "Data validation using Python type hints" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584"}, - {file = "pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236"}, -] - -[package.dependencies] -annotated-types = ">=0.6.0" -pydantic-core = "2.27.2" -typing-extensions = ">=4.12.2" - -[package.extras] -email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata"] - -[[package]] -name = "pydantic-core" -version = "2.27.2" -description = "Core functionality for Pydantic validation and serialization" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, - {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"}, - {file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"}, - {file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"}, - {file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"}, - {file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"}, - {file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"}, - {file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"}, - {file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"}, - {file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"}, - {file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"}, - {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"}, - {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"}, - {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"}, - {file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"}, - {file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"}, - {file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"}, - {file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"}, - {file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"}, - {file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"}, - {file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"}, -] - -[package.dependencies] -typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" - -[[package]] -name = "pyjwt" -version = "2.10.1" -description = "JSON Web Token implementation in Python" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"}, - {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"}, -] - -[package.extras] -crypto = ["cryptography (>=3.4.0)"] -dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] -docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] -tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] - -[[package]] -name = "pytest" -version = "8.3.4" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, - {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=1.5,<2" - -[package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] - -[[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"] -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 = "starlette" -version = "0.45.3" -description = "The little ASGI library that shines." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "starlette-0.45.3-py3-none-any.whl", hash = "sha256:dfb6d332576f136ec740296c7e8bb8c8a7125044e7c6da30744718880cdd059d"}, - {file = "starlette-0.45.3.tar.gz", hash = "sha256:2cbcba2a75806f8a41c722141486f37c28e30a0921c5f6fe4346cb0dcee1302f"}, -] - -[package.dependencies] -anyio = ">=3.6.2,<5" - -[package.extras] -full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] - -[[package]] -name = "typing-extensions" -version = "4.12.2" -description = "Backported and Experimental Type Hints for Python 3.8+" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, -] - -[[package]] -name = "werkzeug" -version = "3.1.3" -description = "The comprehensive WSGI web application library." -optional = false -python-versions = ">=3.9" -groups = ["main"] -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)"] - -[metadata] -lock-version = "2.1" -python-versions = ">=3.11" -content-hash = "22b6e8acad34d08d460e3e33266b43c2bc6bd46d32c82390b65ab54f2f195eff" diff --git a/flycatch_auth/pyproject.toml b/flycatch_auth/pyproject.toml deleted file mode 100644 index 7d041b8..0000000 --- a/flycatch_auth/pyproject.toml +++ /dev/null @@ -1,34 +0,0 @@ -[tool.poetry] -name = "flycatch_auth" -version = "0.1.0" -description = "A Python authentication package supporting Flask and FastAPI" -authors = ["flycatch"] -license = "MIT" -readme = "README.md" -repository = "https://github.com/yourusername/flycatch_auth" -packages = [{ include = "flycatch_auth" }] -keywords = ["authentication", "jwt", "flask", "fastapi"] -classifiers = [ - "Development Status :: 3 - Alpha", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12" -] - -[tool.poetry.dependencies] -python = ">=3.11" -flask = "^3.1.0" -fastapi = "^0.115.8" -pyjwt = "^2.10.1" -pydantic = "^2.10.6" - - -[tool.poetry.group.dev.dependencies] -pytest = "^8.3.4" - - -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" diff --git a/flycatch_auth/services/__init__.py b/flycatch_auth/services/__init__.py index 9ac1810..09bb482 100644 --- a/flycatch_auth/services/__init__.py +++ b/flycatch_auth/services/__init__.py @@ -1,4 +1,5 @@ -from .jwt_services import AuthCoreJwtConfig, JwtService +from .base import AuthService +from .jwt_services import JwtAuthService, AuthCoreJwtConfig +# from .session_service import SessionAuthService # Uncomment when session auth is implemented -# It will helps to call on top level import these methods -__all__ = ["AuthCoreJwtConfig", "JwtService"], +__all__ = ["AuthService", "JwtAuthService","AuthCoreJwtConfig"] diff --git a/flycatch_auth/services/base.py b/flycatch_auth/services/base.py new file mode 100644 index 0000000..ff7cbc9 --- /dev/null +++ b/flycatch_auth/services/base.py @@ -0,0 +1,25 @@ +from abc import ABC, abstractmethod +from typing import Optional, Dict + + +class AuthService(ABC): + """Abstract base class for authentication services.""" + + @abstractmethod + def authenticate(self, username: str, password: str) -> Optional[Dict]: + """Authenticate a user and return user data if successful.""" + raise NotImplementedError + + @abstractmethod + def login(self, username: str, password: str) -> Dict: + """Handle user login and return authentication tokens or session details.""" + raise NotImplementedError + + @abstractmethod + def refresh(self, refresh_token: str) -> Dict: + """Refresh authentication tokens (if applicable).""" + raise NotImplementedError + + + + diff --git a/flycatch_auth/services/flask_middleware.py b/flycatch_auth/services/flask_middleware.py deleted file mode 100644 index b07bace..0000000 --- a/flycatch_auth/services/flask_middleware.py +++ /dev/null @@ -1,15 +0,0 @@ -from flask import request, jsonify - -def authenticate_request(jwt): - """Middleware to authenticate the request by checking the JWT token.""" - token = request.headers.get("Authorization") - if not token: - return jsonify({"message": "Authorization token is missing"}), 401 - - token = token.replace("Bearer ", "") # Remove 'Bearer ' part - user = jwt.verify_token(token) - if not user: - return jsonify({"message": "Invalid or expired token"}), 401 - - # Set the authenticated user in the request context - request.user = user diff --git a/flycatch_auth/services/jwt_services.py b/flycatch_auth/services/jwt_services.py index 26e48a0..51a5481 100644 --- a/flycatch_auth/services/jwt_services.py +++ b/flycatch_auth/services/jwt_services.py @@ -1,9 +1,10 @@ import jwt import datetime -from typing import Optional +from typing import Optional, Dict from dataclasses import dataclass -from flycatch_auth.model_types import Identity, IdentityService - +from .base import AuthService +from flycatch_auth.model_types import IdentityService +from .base import AuthService @dataclass class AuthCoreJwtConfig: @@ -48,21 +49,24 @@ def verify_refresh_token(self, token: str) -> bool: return self.decode_token(token) is not None -class JwtService: + +class JwtAuthService(AuthService): + """JWT-based authentication service.""" + def __init__(self, user_service: IdentityService, credential_checker, jwt_config: AuthCoreJwtConfig): self.user_service = user_service self.credential_checker = credential_checker self.jwt_config = jwt_config - def authenticate(self, username: str, password: str) -> Optional[Identity]: - """Authenticate user""" + def authenticate(self, username: str, password: str) -> Optional[Dict]: + """Authenticate user credentials.""" user = self.user_service.load_user(username) if user and self.credential_checker(password, user["password"]): return user return None - def login(self, username: str, password: str) -> dict: - """Authenticate user and return JWT tokens""" + def login(self, username: str, password: str) -> Dict: + """Authenticate and return JWT access & refresh tokens.""" user = self.authenticate(username, password) if user: return { @@ -71,8 +75,8 @@ def login(self, username: str, password: str) -> dict: } return {"error": "Invalid credentials"} - def refresh(self, refresh_token: str) -> dict: - """Refresh access token using a valid refresh token""" + def refresh(self, refresh_token: str) -> Dict: + """Refresh JWT access token.""" user_data = self.jwt_config.decode_token(refresh_token) if user_data: return {"access_token": self.jwt_config.generate_token(user_data)} diff --git a/flycatch_auth/services/session_service.py b/flycatch_auth/services/session_service.py new file mode 100644 index 0000000..654c3e6 --- /dev/null +++ b/flycatch_auth/services/session_service.py @@ -0,0 +1,18 @@ +from typing import Dict, Optional +from .base import AuthService + + +class SessionAuthService(AuthService): + """Session-based authentication service (to be implemented).""" + + def authenticate(self, username: str, password: str) -> Optional[Dict]: + """Authenticate user using session management.""" + pass + + def login(self, username: str, password: str) -> Dict: + """Handle user login via session.""" + pass + + def refresh(self, refresh_token: str) -> Dict: + """Refresh session authentication (if applicable).""" + pass diff --git a/flycatch_auth/tests/test_auth.py b/flycatch_auth/tests/test_auth.py index a3dcd09..5b6fa26 100644 --- a/flycatch_auth/tests/test_auth.py +++ b/flycatch_auth/tests/test_auth.py @@ -1,10 +1,10 @@ import pytest from flask import Flask, request, jsonify -from flycatch_auth import auth, AuthCoreJwtConfig, IdentityService, Identity +from flycatch_auth import auth, AuthCoreJwtConfig -class MockUserService(IdentityService): - def load_user(self, username: str)-> Identity: +class MockUserService: + def load_user(self, username: str): return { "id": "1", "username": "testuser", @@ -30,7 +30,7 @@ def app(): app=app, user_service=MockUserService(), credential_checker=lambda input, user: input == user, - jwt=jwt_config, + configurations=jwt_config, ) @app.post("/auth/jwt/login") @@ -52,7 +52,7 @@ def refresh(): return jsonify({"message": "Invalid refresh token"}), 401 @app.get("/me") - # @auth.verify() + @auth.verify() def protected(): return jsonify({"message": "Access granted"}), 200 @@ -89,11 +89,7 @@ def test_refresh_token(client): "/auth/jwt/login", json={"username": "testuser", "password": "password123"} ) refresh_token = login_response.json.get("refresh_token") - print(refresh_token) - refresh_response = client.post( - "/auth/jwt/refresh", - headers={"Authorization": f"Bearer {refresh_token}"}, - ) + refresh_response = client.post("/auth/jwt/refresh", json={"refresh_token": refresh_token}) assert refresh_response.status_code == 200 assert "access_token" in refresh_response.json diff --git a/flycatch_auth/tests/test_fastapi_integration.py b/flycatch_auth/tests/test_fastapi_integration.py index d2766f2..d9fbf05 100644 --- a/flycatch_auth/tests/test_fastapi_integration.py +++ b/flycatch_auth/tests/test_fastapi_integration.py @@ -1,7 +1,7 @@ import pytest from fastapi import FastAPI from fastapi.testclient import TestClient -from flycatch_auth import Auth, AuthCoreJwtConfig, IdentityService +from services import Auth, AuthCoreJwtConfig, IdentityService class MockIdentityService(IdentityService): def load_user(self, username: str): diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..f6da45e --- /dev/null +++ b/poetry.lock @@ -0,0 +1,267 @@ +# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. + +[[package]] +name = "blinker" +version = "1.9.0" +description = "Fast, simple object-to-object and broadcast signaling" +optional = false +python-versions = ">=3.9" +groups = ["main"] +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 = "click" +version = "8.1.8" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[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"] +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 = "sys_platform == \"win32\""} + +[[package]] +name = "flask" +version = "3.1.0" +description = "A simple framework for building complex web applications." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "flask-3.1.0-py3-none-any.whl", hash = "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136"}, + {file = "flask-3.1.0.tar.gz", hash = "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac"}, +] + +[package.dependencies] +blinker = ">=1.9" +click = ">=8.1.3" +itsdangerous = ">=2.2" +Jinja2 = ">=3.1.2" +Werkzeug = ">=3.1" + +[package.extras] +async = ["asgiref (>=3.2)"] +dotenv = ["python-dotenv"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +description = "Safely pass data to untrusted environments and back." +optional = false +python-versions = ">=3.8" +groups = ["main"] +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 = "jinja2" +version = "3.1.5" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, + {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "markupsafe" +version = "3.0.2" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + +[[package]] +name = "packaging" +version = "24.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pyjwt" +version = "2.10.1" +description = "JSON Web Token implementation in Python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"}, + {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"}, +] + +[package.extras] +crypto = ["cryptography (>=3.4.0)"] +dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] +docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] + +[[package]] +name = "pytest" +version = "8.3.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, + {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "werkzeug" +version = "3.1.3" +description = "The comprehensive WSGI web application library." +optional = false +python-versions = ">=3.9" +groups = ["main"] +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)"] + +[metadata] +lock-version = "2.1" +python-versions = "^3.12" +content-hash = "6ef42315c2ffd19d45577bbefb1bee3f01e9961e05ef0fcdd6f86b7cb88555da" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b4491c9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,21 @@ +[tool.poetry] +name = "flycatch_auth" +version = "0.1.0" +description = "A Python authentication package for Flask and FastAPI" +authors = ["flycatch_auth"] +packages = [{include = "flycatch_auth"}] # This ensures Poetry detects the package + + + +[tool.poetry.dependencies] +python = "^3.12" +flask = "^3.1.0" +pyjwt = "^2.10.1" + + +[tool.poetry.group.dev.dependencies] +pytest = "^8.3.4" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/flycatch_auth/tests/__init__.py b/tests/__init__.py similarity index 100% rename from flycatch_auth/tests/__init__.py rename to tests/__init__.py diff --git a/tests/__pycache__/__init__.cpython-312.pyc b/tests/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bb6907639b12a56ec88caf00256483a921cf34d8 GIT binary patch literal 164 zcmX@j%ge<81R`@5ri19mAOanHW&w&!XQ*V*Wb|9fP{ah}eFmxd<*c8PpPQ7bg9zQ^{GwFdw4B7^Z2gkd;*w(h`1s7c%#!$cy@JYL95%W6 cDWy57c15f}GZ}%n7{vI<%*e=C#0+Es03Wm|R{#J2 literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_auth.cpython-312-pytest-7.4.4.pyc b/tests/__pycache__/test_auth.cpython-312-pytest-7.4.4.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3f97ccf9b32e4e64319db18c8812d927c2ad8780 GIT binary patch literal 8561 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 YP6On=mPGc2uUjQ{?^1h|?We2w7hLmke*gdg literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_auth.cpython-312-pytest-8.3.4.pyc b/tests/__pycache__/test_auth.cpython-312-pytest-8.3.4.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dd5875099ae3a8ab1126010f18b17d3a943f23c1 GIT binary patch literal 8826 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 W{a%szAN;;o;ZJP!$M{J)i~j@^^3o&# literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_fastapi_integration.cpython-312-pytest-7.4.4.pyc b/tests/__pycache__/test_fastapi_integration.cpython-312-pytest-7.4.4.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dc86c1b8c2eb95f5c8a6bf8b32bc0e36313e5476 GIT binary patch literal 3121 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 U+OZHW2yYDFI8!siQ7)qH-?Va&L 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 literal 0 HcmV?d00001 diff --git a/tests/test_auth.py b/tests/test_auth.py new file mode 100644 index 0000000..a3dcd09 --- /dev/null +++ b/tests/test_auth.py @@ -0,0 +1,99 @@ +import pytest +from flask import Flask, request, jsonify +from flycatch_auth import auth, AuthCoreJwtConfig, IdentityService, Identity + + +class MockUserService(IdentityService): + def load_user(self, username: str)-> Identity: + return { + "id": "1", + "username": "testuser", + "password": "password123", + "grants": ["read_user"], + } + + +@pytest.fixture +def app(): + app = Flask(__name__) + app.config["TESTING"] = True + + jwt_config = AuthCoreJwtConfig( + enable=True, + secret="mysecret", + expiresIn="2h", + refresh=True, + prefix="/auth/jwt", + ) + + auth.init_app( + app=app, + user_service=MockUserService(), + credential_checker=lambda input, user: input == user, + jwt=jwt_config, + ) + + @app.post("/auth/jwt/login") + def login(): + username = request.json.get("username") + password = request.json.get("password") + user = auth.authenticate(username, password) + if user: + tokens = auth.generate_tokens(user) + return jsonify(tokens), 200 + return jsonify({"message": "Invalid credentials"}), 401 + + @app.post("/auth/jwt/refresh") + def refresh(): + refresh_token = request.json.get("refresh_token") + new_token = auth.refresh_access_token(refresh_token) + if new_token: + return jsonify({"access_token": new_token}), 200 + return jsonify({"message": "Invalid refresh token"}), 401 + + @app.get("/me") + # @auth.verify() + def protected(): + return jsonify({"message": "Access granted"}), 200 + + return app + + +@pytest.fixture +def client(app): + return app.test_client() + + +def test_jwt_token_generation(client): + response = client.post( + "/auth/jwt/login", json={"username": "testuser", "password": "password123"} + ) + assert response.status_code == 200 + assert "access_token" in response.json + assert "refresh_token" in response.json + + +def test_protected_route_access(client): + login_response = client.post( + "/auth/jwt/login", json={"username": "testuser", "password": "password123"} + ) + token = login_response.json["access_token"] + + response = client.get("/me", headers={"Authorization": f"Bearer {token}"}) + assert response.status_code == 200 + assert response.json["message"] == "Access granted" + + +def test_refresh_token(client): + login_response = client.post( + "/auth/jwt/login", json={"username": "testuser", "password": "password123"} + ) + refresh_token = login_response.json.get("refresh_token") + print(refresh_token) + + refresh_response = client.post( + "/auth/jwt/refresh", + headers={"Authorization": f"Bearer {refresh_token}"}, + ) + assert refresh_response.status_code == 200 + assert "access_token" in refresh_response.json diff --git a/tests/test_fastapi_integration.py b/tests/test_fastapi_integration.py new file mode 100644 index 0000000..d2766f2 --- /dev/null +++ b/tests/test_fastapi_integration.py @@ -0,0 +1,38 @@ +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient +from flycatch_auth import Auth, AuthCoreJwtConfig, IdentityService + +class MockIdentityService(IdentityService): + def load_user(self, username: str): + if username == "test_user": + return {"id": "1", "username": "test_user", "password": "securepass", "grants": ["read_user"]} + return None + +def mock_credential_checker(input_password, stored_password): + return input_password == stored_password + +@pytest.fixture +def fastapi_app(): + app = FastAPI() + auth = Auth(MockIdentityService(), mock_credential_checker, AuthCoreJwtConfig( + enable=True, + secret="test-secret", + expiresIn="1h", + refresh=True, + prefix="/auth/jwt", + ),) + + @app.get("/protected") + async def protected_route(user=auth.verify): + return {"message": "Access granted", "user": user} + return app + +@pytest.fixture +def client(fastapi_app): + return TestClient(fastapi_app) + +def test_fastapi_protected_route(client): + response = client.get("/protected") + print(response.json) + assert response.status_code == 401 # Should be unauthorized without token diff --git a/flycatch_auth/tests/test_flask_integration.py b/tests/test_flask_integration.py similarity index 100% rename from flycatch_auth/tests/test_flask_integration.py rename to tests/test_flask_integration.py From 05400a3fcc200c0ea6f42bb5af18b752a0071433 Mon Sep 17 00:00:00 2001 From: ABINFLYCATCH Date: Mon, 10 Feb 2025 12:49:56 +0530 Subject: [PATCH 17/28] Removed dist/ from tracking --- dist/flycatch_auth-0.1.0-py3-none-any.whl | Bin 20286 -> 0 bytes dist/flycatch_auth-0.1.0.tar.gz | Bin 16275 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 dist/flycatch_auth-0.1.0-py3-none-any.whl delete mode 100644 dist/flycatch_auth-0.1.0.tar.gz diff --git a/dist/flycatch_auth-0.1.0-py3-none-any.whl b/dist/flycatch_auth-0.1.0-py3-none-any.whl deleted file mode 100644 index 90582243bae7f4a1376e4ce659dcf13814aa9452..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20286 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 diff --git a/dist/flycatch_auth-0.1.0.tar.gz b/dist/flycatch_auth-0.1.0.tar.gz deleted file mode 100644 index c25db5142feb53088d6d4fdebc2299cbbd094cd1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16275 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 From 4f601561dd090c559e2d29e77864bbafa891a64b Mon Sep 17 00:00:00 2001 From: ABINFLYCATCH Date: Mon, 10 Feb 2025 14:31:05 +0530 Subject: [PATCH 18/28] changes --- dist/flycatch_auth-0.1.0-py3-none-any.whl | Bin 0 -> 20286 bytes dist/flycatch_auth-0.1.0.tar.gz | Bin 0 -> 16275 bytes flycatch_auth/.gitignore | 1 + 3 files changed, 1 insertion(+) create mode 100644 dist/flycatch_auth-0.1.0-py3-none-any.whl create mode 100644 dist/flycatch_auth-0.1.0.tar.gz diff --git a/dist/flycatch_auth-0.1.0-py3-none-any.whl b/dist/flycatch_auth-0.1.0-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..90582243bae7f4a1376e4ce659dcf13814aa9452 GIT binary patch literal 20286 zcmaI6V~}W3vn1NKZQHhO+qUiQ)3$Bf#%bHOZQFD1doeHGcW=!6sEGZmR#auKtc==A zK^hnY1poj5;$IL-pw#LhVE+3;{}&kl!pz3g*wDq;Lf_EU#e$yB+|tF;+|J&~G%Ak1 zTt-f2Mj|F|^>kr@x;$ELMn2*g2teUK(q*YUu9f{u!wLuh@Ly^FGo8M^rJbdVzCN9U zr)iulTp$BN$Sdz~u^mgrPG5*&h<4gYGL*6#4F=%#i(LSdJ^-;Ou)Mi&^|g(z!! zsdMfg3a2yPpL5HVcBq=-fnQ$JHOG@$KhDm@^9X*N3<_(a8S9bLaHX<(#G`6DV@Ab z(P&UN7ZwiUa5?uK#c)eH$^<(pWNaCZk?9hp0VQKU=;0$uZe$-7LdQ>STh$AD=o6^5 zZvsx0?IA2q30zK?x&irtEuL<`F_3*UX0YA4xMrx8lp96*cel|mpJHyeQ$}Hr;nv4h z8S4gkTpOGXG6WX`y>5LB^hKHAWPu_M2thYSbM^A?$UJn8 zY|v}+qOjGOL zIt3Q}xEP1o`mhZWK)gtxQHRr8(C5^RZhnf@4dk3Heqy7Nf~Me;qCk?_&;s;oKaSqk z9p+{IY7A~jj?f&Pd7o6?^-7MqBVEkK&NO@JLU&6O@zZ|(;0C+D9g7{pE;N{9B!IR7 zb8IYW|NTf~39H-DJiAS0s*o4_)8He!EQun9y4j;g_c(6Uvy@LOw~7=g&j=mwx$teb_mc&%V27yWRxvqkLHTv7oj}c~+m?aa^l) zV9}kv#i^qlJodXoo}zB@K`Bb+3yl3N(b1vP8lZQzs^ucLj{t{g3;}#k3Cp4+01I>H zG4PKh;x?ta2jM7Q7hKMRT5WNhPg%9IF*`wmw5&zs)JsB(QO7L6JkkfJCV*mk-d-r+|C_e<@WhqgK*XskTfP-nZ&2D(^SL zl$4ua0GTWBnieryZ*Lct?PzFPq__%ybYKYC@jI;jR8s3Bd>Jk@%z456k;<+>vB_x3 z45!w|_;ikUvV13jx^$rnm-PP=S#(66OV)pgO8pCh|ADNNy{n6x1V zspuo9TF31^y#vcoq_pI@8Ao z(_jALScog`MC!?8Ip%+xU^4DT$1G`SX+YNKa&zjFm#r0vT7g}O2dUj6Wc47tC`gUD zikWa{4i1H$m$FyABho!~+VV0PX*Fj9`qP0J1$r1o!2}&|a^n5~dKTwKPYETbu7F~R zB8s|HoE)GA)vP5kZ>a!jyBVDvxN4J-Aoqvn?lvMN9&;b6-K?2%f;2gfVDU11Z?qCC zjPMT$8<}NUnxhz4nOJ*@LX$a)o7}^XrUa5cW(f6CG%+<-WPB>>W2AQ8@o$ue&zW7F~OgyANF)dg8%i;&tj!`X`_s-D&kzVu_BtlUiCB!PHH?9W86 zMXZW7Q0&{B0q3Z&9&1kxm(C-joZ_Qv2a6xoIg3mhQS#-hE*FMYXllsLq^&6txW6c< zB)*Y7K^;KV_;QH0XhKZgI@1SvH$7i>ve|el7@Guww6oOrw{ma()sFZ#y~;@L9%kxwmzttnPpcvCq>_Fit|upPn7`VTN!2VW0QpU2R;&8ATOUbMXIZ0z4UN{ltn;69?3sbkVUlDB8P+~G;gsHeIZVq2Pmyb&U}n_>CmrfGrOCkLnd>tENBbWwGn2q+&WBR0v$_qG9)@k&jY}nQ zt6IgIpx4vuIqtz0fPw3f2MsFepVZ5k&nuiR?baogzFoOHj9x)PyWH3tycpNh;lo@V z0nc%=_H#Y$M(VFz%xBJ1&~KL^~zT>PNOLBelu4kA?1gFTm~Dj4jq(Gy7vBKzjws`p9o zdIL91;=ku549U#h=(#!g`E0?;2;JWT|6n+c$oSy%H0;9{>I?)@Ce!>t@H=d|CX4w4 z|EF4kkc#)l{*#Q)fAK%1!`aly&C>Wk^~y9>7H)t5CHOVl=%JJJpNTr3u|1r?2z)kV z1cg!P=xJFi9=ZPVVI~6$+g2qHb)7|EhEvAlilyIdVu<>r&@8DeW_6|UL{65jf=w{* zhTU1~vQXHJP;Tt63r3w&^#lgQ%J#v}HqKK7dwFv!I7PW);(Hi9$wH7icT|D$&bAC9 zg#7RoPyD%=7yS$PpVeGWBhQiko2n~7004OZLkR!BYK#n>P5+5tP5i9fAOp4NmaL@X~>-+1v;|1US%oB-jV+ywW5%p*LdJV7HJ*APw-UxO@pF>eW{-BW??&V+7L zIUgPg32c0MS%$#hgC6BE0p{JKW_jN`9m!i1=(E+RgE(aAK)&SP@cyH8sW#Zg>z&dh z@w>c}Zhnch51T2@GY{NyK2+sP?Eh@=<2y9?!@mZbfC2!J{hu3bX6Wo<=wSK(82W#W zE>Z4}U1UJ${-hS$Pn0SE>aS3Ok1_zwR#VsuP)k_Nu$eZyX2_)pt}{>MsAJ8HZU#S80-t{u_h@$PJLb>y6>+*WtG?-lJ0^%z=A9Jd^2yTXB>lFSqX2S`V{JiHth75j<}>O@stKvUDC zlj_-5N|z5UQSyaX-46s~7!q4!C)FhG9H!D2r~fzeY!Fl9GMQw2ahCK>MSicWpHvbF zO6q4BW3|BDjD}ZBosVF>GY(e7ovn!$w7yrk4G}oZr0M<@>w_OS+2tpnK?u$Wuq4#~9|~9uJ>g;2ZQmS+gQ0Ba!{@N|t~F0FeA&SYu=8 zZ2iAElcFkX|IhSxf2t$g3{y5547yP%7;KCGx!E!dGh{%?kZa~p5~CdNH2n1~HoRzXeFuVz5$$iEr;Qe=wrVS{2})j(JJ;wYgR zs3+faEPKl$PAK|6ehqY3rHB#z0nESydC&fXwTg6R zs5Jlzk&Y_nSCTxTmM^5>g&V`caQs^CCdT(iD>v%sbyIcHoFiXBMu#4pm$Tx~EkDd(f(~sku4`gmWx;g?u zOnx*em#vkUTWM0`(?Mh!WS>Gri?O0(jT=Q~1yrfW#mYZ_yR77Slwcj>lVD*E++jw6 z$Jr1^Eq6q5S>4$=c1kI;0_u~jEhI6IJnNwVZi>}nqmcZK@vcS0NFt+4_2JE#M)MBS zuDOoCuwN4VX{B1_QavNBr1qBx`6mckxcawoVR0a$Nz>!XG=xJ5`l!75M<668$nbS^D^%49x%_*NUniKE)*5? z%j^TmRzy-2PO!F%u=x+)B$(1cXf z3JMZ)%7x7?P_No#g1QfKB!&@*E#}Rlh+XbjY6=61)d4{wSkAO|e@3AyO{LtROzi4q zB_obVkB)*GoD8PuBMaqQ2s=LBbLI*Gg6)0xG^@heA6nKVMa_a8ZMV&7*JK7-3qgnG zxuoXJ6PrV|y~;so=dTJ<*88m5!b@@cJclOE$Y5O=04`Y4h^_J6I`@q5$&Xy!Eq>~w zGe^))t3|V)vNPke?FgcGq&_j!5}j&6|7tqQ3QEyf)*et6 zEK5n0u4ZXIy?;z^x+^j%O?JNct1oVh*O&hmu#xhXuYVrtBdP~7!kjhEwyM)8PqSoT z-`D{y%+xfLOkp8TOoFBC55!{R@zcUD2B6S`hLNnFOl{Xufb8KpB zEQZ7`zy)_Cb!Mjd6;bH`tWMelkM(w~qR7{*`NN&z?`w(%oD+2qF~gKfs7WBMq?h2D4z5S$e^?E)O7&)T|i8 z69zbKWlR`1yf?=tn+ zJu84})!RrdW;qEj#7u)>_pTxVHL{Sw+>_CqmHbUFWA^svZlGq4^)E-i=soD3pp^h- zyc}zr8veX*p>O;P+vN@L&qtVA3Hgg&nb+Lfcg!!)f2th-uX&mRJph1g1ONc(e{Ui! z105qB1D%PbvkR@IotZtojHIxroU-V!ueLL`c;b!scO=x`@aO=G$=n9_YDjSb6q9`z zQ76d>tnM7JPJyS&`Iu{H%d0y6cT+RnPQbZXU(VQVjD)M&iHV8Nni}4(+qTQum$vuk zFFQ9=d;Z=oEgh|o-}mS5EgVm*-|w5u)^h64!=;})_)q_jiFN#*-yd`1E7R^x_Tj_Q zCiAq@FX_($$C;hg%NtJX>~Dov)m+!7mnFK&<&f{w(o45%`d!~PyH#}8$G7dek(oT5 zraH^z!WQmTo2Xjnj%%x~!ed*tV>bGYTFO-4QySi`V$ilX<9#jT%a&`lCo|1zon@}9 z)hfTsN;|7_&#n!VWn)F&jl#hP)$R3anoILE{dDEi<6k@MG~2Y2L1lPsatr4a>&Ath z7wL^@z4O>+JF``n8%klFPFM7N&D9u7UN(p1&dv4_KkCZApl7qLk2T$QS!dd`dQF?1 z*H;Pd?y{{Hk*NLs^x9vn?zZc>vo3pNS{BfA zo4I-gmsVSB)Mu15=#CIm1__1Mwz~^bARp)hJOq5Q-cKaC4D{KQ`PumclwYy-fYPm- zDI&Jb&f14Mnk}oF03Q%b3#PR3fKnhAY2Kj>mtC^c41P7I(X_n&(&FXT zrNQ*r>g(o0Lo=qk0B{Tldl?7v9QubVwUz>#!JNbIC@6MrusIPge+!P|^4ug?-2y)B zl3d#*n#Oa3MUarBI21AN0QkI1SGRef-WQ>?wW}M7ed#u)-&t$~%{4j>An9MHp`=e{ z7I*;^<7$mdkI|=@=h45@|3;c4F~MyvUbfo?+LF#0CQPW^tTSXI?glW^yw+6a4R(6Q zux6M+^iJfl>RoSDB2$@ntUlPP$T_rIZy`(K6~G|S?bEm~je z{M85|@#oZu>l?UrI7>$8Bk~a&nF80E{7$fSC$0HJ03#9=%Y0d=P+76|}%|LKgQ?0yplxo7TPUpJ2(2$P_f)vj^Bq-<)@yWPz zb4g2Fqk)?+5Rc|WmsN8Hcp@;M44xu>YWFdLgJ(Rnk=0$`w$Jq^Ps5FPf zKuAcaCBDZGt=n)sTG?F}u&kRl?-^S7ydKdRl?E705y~4Vs6=ye2^%4ifM^xNS+bhB zHW?(vP4i^(C%iFKplFx&T&k*8fu@R9upL;Lf#uCx4kY`SfZ7NnT^k6M_C>|R?3ngD zOT%jW&<)rz9fEsaXtHy{ni`RvxSvi8)!9Q4u&2V6VXk`(jHwG2Pz+#R$b|VpQL5Dw z2=eC6e60I3_Gej0@5L1F+Wt4d3gdt*P(&1REKU?sd5~+vk5z;@i@DC-w`s6nFmEVS zmimMbS$4ZJc-C5NmVqmsHSdB5J&`0vti6phyg4C^SHS3kF?6UB9$bGPBD4iUw}*IL ziv1TvsEB;*#7TGMihfB6K^ou{br=!yW=ER%@RgWOrC_10(bfp`t){Be3sxhrEMKJ7 zD~z@_GriE*6F?~#fqX^ghywzwQR!$jlGfM@rOj~z-79_9x3jJ3^7ZB6KX_4)tmKvN4~UqRBkv z(nx7hOn7&q-ke!=UWnwZ6*OL?8q!p3L_J_Lp=&f;u0J#e|7A)8XkbMhKc-ySzQQ6V zY-ht$EbE;30P}=X*K?9;L{8#Z?yMrBXKw>if1Cu16*bpTPmtqMPOqBq)!f5pu|QA0NeE{oxgYU+YGbUnqE^ z!zaprIwPmXS|d8U=Z-8ILXBc@P)l`Hk(NUi(Z8ooB>a*dD;B7P~5S&Vx&+Gt=EbgQCby%lPlKmjuPU^y&5 z#7$&I!1xXBg8dz*{hTwx)Au+6Er}h?@r4@dai1I!63gAS>>P+7@tDR-IY=NrB@ml^ zZ)nNMTPhm@#pxl1v97F^3{#15Yc{$?2S$ac2dy1GWF<;HD7rTp$mfgn8l-Wuo?_Y# zIWV7uoy7fy!c(Uv!~rx3rf%r1yAklVT(hqoAX{ zZ$p^`IxGy;0?goaSZ~1HsIR@YjkAMbtR*ZD#BV5O(Hi;5!?_;dFNl^Iv_*#PDKBK) ziu@f|)oJAY-5EMQFBknGVuK5D9MM&kmNp)%y3a~Q+e}L}dV$3T0;FTDblguF=#S`@ z>L9wtov1zh3C=(0;v#tvOPQ+2cA=V>OW99lTO2lr4#k?Ven8kX90vC{z*bWOO3f2i zj~&XaP7OrKZCwHv^+uDP3NjxKr+-_CP9SQ)Jk@AmujA?_|S202A5_yvJrn=rdtKZh2*vtH-Ygh-rIr z36$OxL7e*iiC>!oaLGpK)W!^z5uQw>%nYVK5?t`UukZ=dYGz);^<;Rd)H5_942#sJ zxKp13Nz6V~_00%3T0ya-8h<{n5c}h$`k2Fa5DQYg8AlpzWoJd<)8eKRRfnVWiFW3B zyVyk(Cdq;nvcla|DsZMDj_33FRN6B0wcjb@kvbS%&yyc!8=WJ19yU9~g+$SXodj>g zj>RDNyD~8G|8s2#k!$h$+aV*bF){ z*}jAJl$1zM`ikn8*BFfSn(2f!tkW#~@ zH36B{@`Q;e0Yx3)D+0eh-Kg9kTj`}%J=z4eCor1RPQ#;!Hlf-Q5Y7&~H5Ny0(uL?R z#SVNp`;Y~h5y*!fWXY&Rl{7OI4QL!GhS@I>K$62lkY=^^qRmYieP;p;TU?l9$5;54 zP~xaSX5UCOJB&x?9eN7smXD*{fOWq|KQQ$!Q7#zjau7OCIeV|FR-)Ha61d!$x;oK_ zb$$hK5Fis4k@?E6EZY?A=@_{nt3;3m$il9~34*9)eLTlffv$%s7QC+TWEH+62ob+n zy6B`@D>uAAV{)OpE^H_8S&ix}(hxeZnbS-Xos^6t*~S;Ht8Ai%R65vA6$z_XUM2&u z>`_7N`(BP4;D%vKHHH`6^ zUMr55??ravqR>m%78I{eLnBKC{5V|I&)+Th`F^eXcDIt%BbSPh2DBTJJ&tyxNCj;1F-GX^OD=pN%(fj~t zz647kEI`n>cNhY*C5mVa&W^&Jw7h3eh_;ci?}NPJ+rsIr4w82v2mJuN%!+ID^qI!OfsL1`O-C=i96S|DM2Mu%EE`=P^Ir@Z z-w~K~N34{WO}`;bg?da!ET_VYNGL>X>{opyH~yM*769{8xt~2cgpNUYKaN!8(?@M) zi8#1G+$?5tcC_F)H}U(}?;>NhN_%}z^Z(oJCisDfK5NY zF*i3H2x3H+3@E5c(Y^{Uz~EK=w0Pmb!lP5F z^sa|p#rjiQsr3tMiZY+62L%IphzWQp;VZOx>&6MY>`o3c@@vVz2JSew0FOQAci(#iziw za?CFQM%Qg-q)OyF|4Kc$-WSP)M=H5sB1~a;5(R5@UMM~qdM#JMNOUJ<(Pr<#zXjcV z^Zi=^#eFIVVMclIB(|K7Vq1;1FR37!vq=$X`#|xdC?pY3 zDJ6fbif=rc5Ozm?;t(_P_L4MwD3>b!MY<&_sdtfbcjIXFxb#v^-%2V`X?O4*XYm-Q zD)1<1AgCKkFaJ}&l8%=>D`oc7OJ-9XA{OCHq)rx4aCr-|qarQcp;XKY;vUhgZ$lcq zvu0La(G!u)-|-D}CE8%hRCOa>>4zY7&I}Jv=q(E$%lyA1+~pNP|6(-&!UZrmt1%DM6w@IwpMopOXgjIwnoIa1PGEvWw6LRr~3__TrSDcr1UbPi2ODrtGA`|BBbfE4RH($m+~YdAshqQWTcdhOXUHTP#Wk79&HJtG;Bj0Tc3Sqq-=71I{97h!Qm zzCx~>%vf~!9K16dxzh!Mf)j}W4oe|8Ap)zObR6dB_q&)#2FXj?NTx7nl_J0-=)pq>CV^-i%NGLE{8^lG#p9TzK|{4K!=$kP5OBxJ?A(UXz=1@mQy5AsLiM;LSAf!s1G$RT|fUUqK z!=EyX8T5|ovM1pzUm=L1(ty0en85<`joO9%Apn_0Qj(rL7Zd53;VV&CaRYDe=81$7 z@=)?PuihOH$0=ND>_>)3mzMade&C8QWe-Y8AMgWZ97jcGyHPNJ`wzK2hB2cVg+ol*>I zd_-XcrF)J3RgJj!lX3$g4;BdaGhk+a*r61Q%tD+(yH8$NNsJf z2Sl}Qcy!{PF%ZEQ2nppvy?6m81`fxNbZiqwx^cV@IVA>$J0{ZlJ42Qe0aOwb3CQ7o zm|&P3MzV;o^R>aI{qXpn_}?OAsvMaxJw-lNU1;uwyD6M|H7$l9sVtSck1)wJGwp?T z;Q~|tpg}oVz1K;*?KB8&8wCo=viv{D*B=MPA1m zH6en9sRQhK;QW!gN6< zySi{t_9AguFGHSWC(#>tkbBK0M$TIjWPt$ZtO%8LMuTz&D6=EQ<~V}aw3O<8z@TE> zA(1WsDc)N{S?EdxQ8;XfcHd#?Cfb1AIX|u1F-(Y7G67?prR4gIMIf;_LVvW{V10@0 zSdKC1RFyA#eZ1sG4JE?%660l0Zcy85o!Fs1mCwFrqisKDZ^1f<9-`W7P`lh_E=L5x zi;%5^)|$by(7I7<00>Bv#-P^{@@8J!`3^vjaHxz{@rpw1Doj~6H_>Hbj3?AdMOGhU z0Zf`1p-B(Qh|nc1DfAXqk)eR#$Ri!4%)yH35@UA%ba1GL+20W7aN?^r| zP^0cS^*H!VNiF)C2~BJEn)W}GZpO9>M%U7(Xzyl(5tJ7UkJ_Z!<*3A$EL>PwersSD zTVxYj&5d01j{VJxAC)0{5E& zH;e^`v+x^=wF;F-hsko>&;F?W+x3bgDe(uyO-MO^?Hdk#%WD$N`5 zY(ZDHLqFj_Xml>cq?l%XZ-g9=cy|HMN+5wPHWSI*GU{T?m|l=cLaQqKQ!Pjfw^Z*3vGqqfPLHRDW>^}v{C@>d|tJE@xXypmYsE=dZ{ zFj3+&kdp#l@IIS@!2jxyoI?|VxWCf4gpot6)ET%KHDu_tb;T*;aP!T_ZN)MQX$S}0 zCw>XN8B;%OCNZi7`3*bIl@5_5sppi!DkbmTO+^bgn&`vRZuYeykC%s(~ z&&2Y ze6$d1#to_1>Rw6ET<^C9>s)c9mohpiNNpz@8vWl>wOGD8oq(E3TQKs54e^n)jl<53ve}B0?UyjEz;#CJUbjVx zxVmDc~h58;;p1Js#N9F_{Kp{AsO` z$@gj!FaRW2rmfE$Sz`sHg7&}TpxqQAr4A4+x4~bFLShp*psie-H|C}p1Tb4oWWc4k zXn>Cjv>0^HL)~;2*oox))~18UmG>lM91+!HE{{WX)pGFqU`s9DR|0|$7cW53NOj=w zz!+k+3$jHt5Y{Hn?7OA~Y`PQXMn{V&qGQ~b_$S7M-Fo9P!*jF-Vj!7`NpCZQD@Vcd zk1-HO(T4!}*!v+;f*Zv`IrGJ4q&$~maa>eYJ2WTz^1_Dkt6Ut5 zqUy#U&0fHOYgo-GXT@CZrt)i~Z82i0jYA7qV1A#vr`Ibr-Mx^v;t(aCkl$NbjeX~W zpVHzjkVwePxi}L2gXERk**K%#ADd*3N1z*!S44v*QjIPTczW}zOTIhp)3~VY1nisYNWFl#CII918O9xBHd{o3PU6ixA$1&ZL8~F4q zEOEKS?Fil{;RHDxh{m<#MIpgwNtbIO2)Kq))j$LX_n|)wZQeppeVDT5^A(9Xe|=_F zWEnw6E_fe6_lpQq1|4@UT(wG9#k5eTFWVUE<9(tJVrEGSgBlTH*Weg>kyKOffr_Nqx(paoHX-k^zIg^8#6;7hTBxP zQF*n0-aMt7D3Sxy!E&xi7@R~cHyoytna{z_6VQoUZ;^c3vn$|4n`pD!dk_)L>8>b+ z5t@{JBpM$-Gc4Gr!y7v!>P!tY3xkH9F>4Cn-Yo67rS8b2n1)9fe+ijxFq3}2+fm+& z4NJC?+M9o|?G=+)1>B_hZ7Y%c?>EsUv;Bq{hErZqs!zGkjW@rk1iYAVxw6H=H}R-Z zr=9X`lM=J3tCA4;wl-oaBTkta_4OjM@dgM7j;WbYotXm7R7A<@0vP4%ENaCUGr8}a z(gv|g;M01M`+=-c`Q@OZ!6_p&6Kai!k!Yn^X+vYeXoGpl4F8UkYm!*^LV?2?IU1j( z`gj&Q`pZgO%M~&F$(;#KXi}kWlS;gOkUq8&51o9eBgy-c$-X6x7L{p_ zWvdc#m>qbeij42?X2mKBM((I(|Ix_$9+`1%U)?Cfbg#8!&QdGTibpN86BwNv zktg_wI}q4`b_S+}z{^I5w23pN>lqz78obe-F_lPk8KiYxc)v&nlKA5;e#LWIq2RD# zFW2GuvhllkB9h8(_&5x&B}*5ngKnC6U}RbI7Y7AAd0v5@?e+CC+ly9)r7b0u!g>`W{eKZ&9G=yYWOvMH%k#{TlG zC*S$^yjBuT{mooV{l$12CM!<}smv;vJCd{{6viQg&Ugh;2Np`-XILZh87XstDa6GD z*GG-=NRz!H4m`Lfp<(g;KorsW1yz!GAsbf_1jAr&q!7rY$r(QwZW-kWlb=8lE*fHp z$A<4SOXX5K<-2erY5+34A8Q#JKL^9`DGM@En9~NPZPdvAFaWV)Q&%V{rA8aLflZ@F z79;=n^e6zbn4nQF>ghY#dA@!_S!rIVX6MAr1SC6LmKsULBIva_=Q2lhjYc6j)BRi> zngNB{EF##omkbn>h;^CK>Ma8qs$^?Uno45cQgqrz+6=IGuCc4Gg23k29yH#9+`Fef z*OM358GO7XVH>pK<^EdL)8{pS9E1Fjd7+*S_P!I;P)JDF>}B~+Y2FfG`%W$)(yx73 zz+MY+LK{L;*mnaC^(#G0JHk+W@BvO`TruqOelBd{(AIyeK?&u0sfsLK_Ypko1aeO< z%op)8>B_KTCLb)ao@V1$QJYIPxaY`6v>~SO*qL%3N)7r35KE0%uMOx93@%;cV}kMp z2{`P!dDu{+$Ro_e07=1;6QG!r%+z{Et*!hK9egDo zdb+dSs7Z^+&fy*C%aj1Li8Ti4m~v47iZPlF9$6ri%Wc}@l{a4c4*RXoYYer6#+QA3 z7R?%nOO+k)D^Ly!8)x+$yRYERNxrBLiUyKU!3(yu*>NLSB@DI%a~^JC2rbq|G!_BBv;m(j@jevAVtmIRML4@k_^p;tf7S!&8^`i)I56g__#{`G! znE|on1VDc>#?&jvZgUaTa2qogpeDoMjY z0NejWTpbF(6T^VAeJ|TWgWbTWV&;j}YVf4NC38LtpI%`ew?@V%%?J#>(eL~b<+ny2W6gqW<~Y1bZ*d>G2%f{je(D~{ zL~giD^dkroMW-BD>kKZq`rfsrXXRsV>leB#D>L5XGvV@&T5UpOw@JGHU1wkvsVE3! zb0m5Ve%cLHL0Yfw8n-yRjKlBEf<1lA0I|Sgg*~E_X%!rSwF~Qa9~x7%Wq&&VO9ed| zQyyUsaNoJVXCL9dM?Kt37?dr4Ej)6fo2YWIonDO=j3T|89Y6^R;z0-wE-9(MusEOj zfOc>kR)j2!3%Mtmd~PsyM)br8aCt>p(V^ZKvT9s^V8O#=@PShe;PZ+B1z{?BD6THC zP6lEv8Pjw4tF+{W_?8y;~T@bLoyHQsl95mF#sBiXUPuKj_;O;~P@vOC@i}-5yYc(a+i> zI@&qC1G5I%7hCL%(kW`&0KkG!9xkM?YKp`meO%@2wsFsk?!L4ZjX;9-3PJLr{8qyWSmIzt1-Vz8#(Jyxn{B%ATU_ zcTsQdE^m#gTfouVsMkjnGVY9R0e)O z_TFA{0E_Ek@<0zce;pltJzqchdYNl(t^JRaem&YVlc%-8=_Lbp-<3Kz1B-9@$F9k^ zr^Bo511rI|0rM6<8oXLd=#gz1g5ED5)-L9@yjwf_n^SZ(1&Jtxfm^iK-$oZ8(JOzy z+>j86uI7=l_2*+8Z&GtM7g>(lZ?8v8s=yiFXTJax0xP@SS^g<{e6-%#GLOX8j^8iK z+P5D6e9`HviwV71ddQviZFO({-cMi0agX+_-5E6ZzJ=rIZCtgE4m75ZbD_vd;eo>m zb=s(*b}-Ose{f%)o*p2wo0H%7j2S4Z24SlHkEY$5ljyTxvZ*vsz|c+TV8alG(l4lQ zLZlIOq6X~G^ONrb;9610(|<2?Uf)-}lZ-;{a3tMcZotAl#RF(C@B#re2D-cRd^5k< zdgUpAy|KP0UY2%tXXw%2z`t|O5+L@Z6kL7f&4-s4=v@%Gi?~Oi2;@4%A2TojK|HP@ z!(K*il?-MNcEUs~>+Eg>|D1eZ8Jb}leeAkB^Z2s&l8G?JP1Diy?;`f@L;e+<+wv!a z)6to<-TOOr5Y_cM!i>3i9kzaO@wlCSSN{lGfFR8TNXAYmzm+EsOSenMH>Z|{|GaS( zkkY#m=Yo2B^0apKeG6?Sz8w~zLm_wO@;C56vHbQF`n?r`F9MO`?k8W8R8{X?;4CB- zgGU1nf-`kDj1XVR61(?|JXBuOcmSb0PG6Z&d@Qo%=KCf7N_%A(J zWoL9uKN1&}Txrq*2hY(1M`7 zf(;D@5m76*G}u%A*VKb}l;mdIXeZu#+*#u7`w%+Bp+Ej{mmITLLb5aO+rq!KvgfKv zH5GtuqTs^qM?L)=POsF}B<54nVkY6dGmlfAv2~)Q zFlPD5aO4aOe-7;lg1;Sdf#Q4a(4Fw#pUFzX-XmY*-vpLg#H_+{u0;a_mj6}AnTA8T zHUOAoZEU0LlF42|^;rtpLe`Xov9?UIq%4uF8Aej}WEoi|dmK6#**~%+%h=ap?7|$z zl(GA!b2?}8`nsCA=9*t~U(fr_^}NsX22t!z@Ff!tA6(b+h!Lp#b6BV+!X{FSP)H+l?kr<}|i{&e(Q z5UIgs&*g8O9*I-E!!%;@Y%x_$ymt9+mFQeRpPN#&boD(@nKr#B(}!oWYW{?`mqk}i z`-f)Acf_-LcJ!214wpU{-@3l$hgmM&{3E>1WAAW{g39}86md5#>uAPxOSd)Fy^E>e(wb-_B@AQPdw z&@N{We(*;%i(hF%TAkAd?kjr8*lg7_O?$wsu`QAv^|94 zbtGXtVmsvPIM~@!j#9-xh$LcsXg8B8>IWr!Pl=4AKSZ#`JGNRcggPdABx+k`*|cpG z7QobWdU)>`??yL*xgMpn35~?uo4=~V}$bh#sX62MlTML}=tiGfh zM4vx(7rZULW98nVW$ogk>()Z*KZi%K>zw_qEmmK4RqRZp;B3Y12EFz9=BH66f@dRq zhtF^Po{?a`j;QDhtR#GPo-`)TS)F|Mfg5JXxT#ZMsVM!TLDO6PtkOO1S_AwZMhtuS z8LZ=qP=9X=OmU}Zz-9ywTakI>3D@Q27fLHf>p;{cEEUVAB9cxm$bxfi+q)Y5H1&Bt z519!oI|Wj!jNv8CoV4e zj>d5#?^|}yY^&}+k`I>`i0LLD#g8D1nFplI14r@=wM^7C)lJmnq70qt)xaUk8=UU> zY5Fl=O9nM^7~xXs2%9^hNLU}X4|^ykU>ussS@jtCf#~1uhFl$vzRBY5Qa}6Tbx1#hxxkrYR9IG1 zp4_|vXk!suB(LqJazfhFl3gwie?1J9EIpgN-XYdiqFtQNTEk>t;3{wr<8e{Xc%cwv zy9!NlPl<2wnaOg<)@grQ0@V7w5jpDPsv6VtZ#+VWm38&^ZIBneL3 zs9tPi3~3VLTauI*p65W|t>>zsnjpi(**u+2_Iq8R;-LS zK(QekvtYh4OXQ>-1qa`X;stR8_>FgP zA5nF0$C~8HcRpV1%`LH*WNdoOV|aS6x)o#&GgP92wF$#fdsY}~T$bZnrCizDSQ^;^ z_ZwW(c$LQ^ychUs&Y70^wyp_lMAdfhx!yFFrDOhZy-c}U4))Ywg%kNk78Mp4bISMO zk-}*Qf{rK{VLZp;BHxdONAYaY?^@avJxyFyV<#cc=Fcr@Vw$iXK7n-!yIkw)EE!p4!TC=HXRTZhRP<>!Y`gMCaxX@s3tI(+CqTi+78B_A-Vg!KW9OhZb2v z_MB~TPTGifS#gygUqlXGX9>a^zVd@gWjfMhR7u4?@RNf@PQe+Wqy}N0S)pAO;%fog zx*`YkBzz0&s7-C>V{~bzBa^kp->O-U2pNiIyA_wpLn9?VH9L>X-X}bQ?PyD=h7aHcO9AxWrBd&7mXV9$9@{OYLvn7gI0Y>=r)#fYO(~aWO=;@eNib^Fem&M` zx(>1wZDj01>5hyi3F$Ol^`6WsY~-8;HxrxkxP=TB98JZko|oI?2j>xvVWkdjhQe`9 zhY509G==)C;%=;@FQs;|7Tg*q8lkZ6j~rL|QglLdL*De%sKnV1HIVRYFXI_M-uK5` zfX0-F7tr3d+}P|`tyymG@O03ZCLzCQXkc+=1I(O!C6%u6_HTuesnbCs2@LG&{@*-e z>fbnMex8mZ&+ETl8G+xv_LrY1At@jQSd99cK}D4jXmo(`L#YLb0j8NK7%B3Tc>iF& zhnj#0U;(Um2yl*pOrQ`+VS16L+)qY%P>%#+fr=mnTTL#Q?ZX~e@V*O! zKqPRBLqT5A`W5n@4G)kB+#pbxcG|yU{pF literal 0 HcmV?d00001 diff --git a/dist/flycatch_auth-0.1.0.tar.gz b/dist/flycatch_auth-0.1.0.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..c25db5142feb53088d6d4fdebc2299cbbd094cd1 GIT binary patch literal 16275 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 literal 0 HcmV?d00001 diff --git a/flycatch_auth/.gitignore b/flycatch_auth/.gitignore index 43ae0e2..df7e995 100644 --- a/flycatch_auth/.gitignore +++ b/flycatch_auth/.gitignore @@ -1,2 +1,3 @@ __pycache__/ *.py[cod] +dist/ From 059edab0b37f2c906d016327182a275d967a92d9 Mon Sep 17 00:00:00 2001 From: ABINFLYCATCH Date: Mon, 10 Feb 2025 14:33:57 +0530 Subject: [PATCH 19/28] Removed __pycache__, dist, and compiled Python files from tracking --- __pycache__/hello.cpython-312.pyc | Bin 1833 -> 0 bytes tests/__pycache__/__init__.cpython-312.pyc | Bin 164 -> 0 bytes .../test_auth.cpython-312-pytest-7.4.4.pyc | Bin 8561 -> 0 bytes .../test_auth.cpython-312-pytest-8.3.4.pyc | Bin 8826 -> 0 bytes ...tapi_integration.cpython-312-pytest-7.4.4.pyc | Bin 3121 -> 0 bytes ...lask_integration.cpython-312-pytest-7.4.4.pyc | Bin 4876 -> 0 bytes ...lask_integration.cpython-312-pytest-8.3.4.pyc | Bin 5000 -> 0 bytes 7 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 __pycache__/hello.cpython-312.pyc delete mode 100644 tests/__pycache__/__init__.cpython-312.pyc delete mode 100644 tests/__pycache__/test_auth.cpython-312-pytest-7.4.4.pyc delete mode 100644 tests/__pycache__/test_auth.cpython-312-pytest-8.3.4.pyc delete mode 100644 tests/__pycache__/test_fastapi_integration.cpython-312-pytest-7.4.4.pyc delete mode 100644 tests/__pycache__/test_flask_integration.cpython-312-pytest-7.4.4.pyc delete mode 100644 tests/__pycache__/test_flask_integration.cpython-312-pytest-8.3.4.pyc diff --git a/__pycache__/hello.cpython-312.pyc b/__pycache__/hello.cpython-312.pyc deleted file mode 100644 index 66ac2603eb0515198912eb13fcad85533cd4a0d1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1833 zcmZux&2Jl35TCbSUVH6)lBO*!2?7a>l$fs;gxX3Wh%}|jCFSDFYPzvL z_27y~?XieMK4}+LJ55nH&g|^G`JUgr-%6zd z;A8%{UH?V}_?te4Pj0~M8w_9zJn&cmA*-@bsVbpbRT;^YKnwM%9vW36%vE!tSv46b zz!}f)pdCHcf&xq6shi+wJhy{!QmKVnhNpXmmqTja&+q7ilOmqzYbUd>qm-j3ZtV<2 zUSSh>@xHaAWda7guy*>We#U#nVJ2Q_6Nc8+QJ(ik*53vYlW6z)OYHRfqeDvuow3eX zdHe~fl~t*K9ysy3%n5$2&ErJo*5YR5*E(fQYEC<8$ngcFi%r2VT~8L9QO&Q*(PfWE ziJx>ql9^8|Tq@I>gW~p*kikKq1{9*Kssh1{YqE`G4k}mWOn4IbzKZ(D8 zreD3c_;7aNmos~(miMMF?H>E$sb!26`e5V>Vj9&~%7!#;n_6hwvS8a`(`yIB7j64m z+X)6IYMcl`xDc-)*@o0O%}rP&B|^7AOzCbyUorKW=Ap?81Qo z#mT$NyEB#D$#?gx_a1z(Yn|QI&ZZ6s1fk7W zd2~XX9e7bg9zQ^{GwFdw4B7^Z2gkd;*w(h`1s7c%#!$cy@JYL95%W6 cDWy57c15f}GZ}%n7{vI<%*e=C#0+Es03Wm|R{#J2 diff --git a/tests/__pycache__/test_auth.cpython-312-pytest-7.4.4.pyc b/tests/__pycache__/test_auth.cpython-312-pytest-7.4.4.pyc deleted file mode 100644 index 3f97ccf9b32e4e64319db18c8812d927c2ad8780..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8561 zcmeGiTWl29b!K*EcJ|>N+q-_nBy3C)m=NrMc?C=ep@e|IsR9w{rpad68QW|2l{;g= z%WP#DB#iozpjIMPsmf0!yAY}B7gB$sepT(yx~^(xgycxgPyS3y70FVio^$85b{Ctd zw8}@X)@SbH+}Anhp2xlZb5m28f%NDq3htH5Fp1SV2z7EW@tHAW#A-PN@NkJ}$1fO%OU0qwuq7W;wW5(T z#!qSFd@ie5l3S7WSIRKFQ&`#nnH!8o36|niRtu<{#t-po;0h3JoL2-(K3U41drQ}d zH{=);ikX6Di9|ETNHNRRjg9<3N-ee0p;cDp)PTwlv#M~DKgB5Gpv7*tBm;JY<5-b$ zM%OQth`N18zZD`{MonWqY?8~VBnpFAl8a)A!z5EQ^l3)nEMC|0Lli;B`IE`zH~U6P z1+8x=Kc3AP*^xdN^_*8qBgy&@Y|8@4whbY1SGD4z^ge_XySF@Uh4Lk5MU}2qgy{8} zf-t}S1b_+Vi`KUJ&YtIGgrEcILeP!?hbNr~-~`w}gpA$>;0m+Ii9-0+rbPxmQ=Jd+WA{Mh z6|Bd#QvSTiBI^x+b}~FJ-gfG$aF$GF)D;#N5xy!n0MNj(XaX(+j{ypI#M{n^tg;4f zfu>PWBdu|H@Gf%UFR1QG! zg

bW>`EP6G!x^U@S`m`Ap%gnt8d>mMNEe2lAzCCa=E?8M`_OW9rC>31({SEh`75iNTT4r>5*w=8=b?aP;jbHx?ng^Z&cXtu=* zigp_H#LKN=m{C`G`&A%vF8QmB;nhEKZ_ew@`r{SFB<*cyO&~ZN>8ZnSB(DC* z0bg*;9SZQ4$HMH&-z|)Zp6U>+KCvFT}ADD}8nu%|^b$mA7H!0m~R42#h zegmrneFo4@k1R4ClR)4+jn-IXTxM5U zlO?i&%9SRSGub+3+oIsNHWme|$=Q$1GGJ&n3&LQRNIwirb^@rhxsu_aCn*ofE@)Fk zDjQ@sBJV*!MI+%3ykbNu5gd6L(5z5VyWmI)o&iS?$a9ddBvy!lGgL3M=%)dIAarj0 zRrKf4xy~&!om-|h&UU^u*>o?_IhWWrlh`&@m`w~!hCYuZki~;j$7Ls!4}zMhE%|T4UBGVcvREg-*q4ax45GCD(H75G7-hx(b>~ z38}F3Jgl;$R;Wx$hL$xnwbH&~(_TFi*0JmlM5eg|rL@1+v2#}5^;^9r@2g4s=;D|5(dSZcDn7yZpaC81b~ig z!=q}SRfc~;oJBrsxP5p#=LWl%h>H&auZ?wJ@>M*%>{uDzLC@QQKp}tx+p94JzJVsLL*wN8z zqTNDHKoyF;j;;jwXpz`Q2d^ERjclm#8~%MQo`-agfB0HlzxF0~NAS!DXoW^vr^=~f zHDGdP0Bw1dH+lV21KCQ`C?45WNe!C(5UWbqXNYsXAvLtNN9EGzJa~n5PR)QB22!C=yoXn1~E-AEWvtV*Wn z?!*257$U!mY?XsQz8X-QszKmMVpTUN0zZOg;5<{6Ole(i#7qgeA!84p8+9vdM2n!i zqVGJ{d~P)Vm$(rGZb-n5mj82O-81w*iws zaMhk*I}X!N&fu<4?JE)TI()21-3qPnE9G%J(kE6+8Um$!nrLN`EfvZcqFGUBPM;mi z<&9iXw?sRXvO3fHNNFsuQq=TtzH}CXBYG7QO6!>+4M(tIhv=SirQ#7nO2mq%hf1W7 zG13qN<}<~N0WFlY;549RBB$u$a6+3J+pUOS*jZobw+r@oyXy;k>(O4UFYM4U45uE1 z=S5gubnEDJP-imjL=U*KGGHHuKOJt)z*x@9Z695_c4=Pfno524?o9I3Vt{GxUShar zapLuFL`K*=$3OR2ehd8bbC2WqH~#qre(T(0`mNy~^nl0mFkj`{_y?+p*F5z0!n3bX z;UqPvN@~dDe!=__oZdTO&)W!?fkwVVl~*HG!Q@SWdK;oCf_+=_CI&382==WB`}p=v zjrn2jXaftk!rSo2M_sZi`9U&x8$MsLhelJ~NO92s95g^8VCT%H5C}uZm{K6e(B2W?0YH&DP+; z0k{$dt|YLJ&y~79jp*myi>-s_`AuruO^*6p5w%_IFe464qK{0Is7nj?>(C?wGznKl zuri&itqd6i!jd--psFXY0`J0$)-_IMKk zs!f6+DR~>g4-jC$Ouh#|VJoe!<#0^VPDNKZ@*Xszeb`y4w>`UU+p}AVQd3bEP`{5O zW)Pf3kVT*(&=3qE7)CGxKxv{1-HOq>6EG6C*|P*YVy6qvA@V4Ka|rSX3J8h_N&xEB z`mXxI9vv@CJx%Uw^KPomy`VKsN@H*lWC#Dmgg$6y^cn!r=WX3L-nstH3Vq)GH0bko zabghSv!?F3=*F4o##^zOXy3$}YNOiJEe9vN9_VPQ1SG zm{dR5j|G0u+_(Jwh2OJ=cK*|2(#{PpDZTSk<@`G%(~E!Ty|oiz0oA>*PcevE4 zTOB@|tV-+Ya<#P@YOK#e&xcF_B1hEWpVrONh#PTFhu2xwx;~BQ=IL;-fZuJd`P^uG zh-LjxxnWxo0jx;-8de0Ie1b1hgdorn7zp|icoxHIcJD=9&Ai%tP;->RPCrEOE&$N! z7(DnE!4k?OS2W1`So^4kA!2*_7qJgr?T;|GzAe!4IMiu;`@X)F>U(!HwsDyNM2vX! ze*#!v&9`luOKhG=Y@SPOok?t+dIN54Cr;kuLzCP7%6ERz@#Kw5*TLuSo^MIsIy!Z7 z>R_#Bf3107QD8bZEir*kvBoF9kr<(Gj_-XezXg8B+++Gto1k=9k{iBJqiuDi?SQY> z4zeKB=_T3)l7q*SkXMDTbF_|f7M|#6jA#k;wb;Jm<$=ERHrZ-&Uz+J_F}>-v?|e~{ z+wZr?J}U^XeDF@L*>|^Si)oNWEi;+GB08IN+3rK0{V@D8xdshrM|5;K76UBHF5q+T zhfNENGQ%hf%!yg%!~-$NhCePnVBoWuU^waOQ<9G_dTG YP6On=mPGc2uUjQ{?^1h|?We2w7hLmke*gdg diff --git a/tests/__pycache__/test_auth.cpython-312-pytest-8.3.4.pyc b/tests/__pycache__/test_auth.cpython-312-pytest-8.3.4.pyc deleted file mode 100644 index dd5875099ae3a8ab1126010f18b17d3a943f23c1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8826 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 W{a%szAN;;o;ZJP!$M{J)i~j@^^3o&# diff --git a/tests/__pycache__/test_fastapi_integration.cpython-312-pytest-7.4.4.pyc b/tests/__pycache__/test_fastapi_integration.cpython-312-pytest-7.4.4.pyc deleted file mode 100644 index dc86c1b8c2eb95f5c8a6bf8b32bc0e36313e5476..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3121 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 U+OZHW2yYDFI8!siQ7)qH-?Va&L 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 From 6b7082833ed4847b3974dc76e27ce2ac49487c60 Mon Sep 17 00:00:00 2001 From: ABINFLYCATCH Date: Mon, 10 Feb 2025 14:35:49 +0530 Subject: [PATCH 20/28] removed files --- __pycache__/hello.cpython-312.pyc | Bin 0 -> 1833 bytes tests/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 164 bytes .../test_auth.cpython-312-pytest-7.4.4.pyc | Bin 0 -> 8561 bytes .../test_auth.cpython-312-pytest-8.3.4.pyc | Bin 0 -> 8826 bytes ...tapi_integration.cpython-312-pytest-7.4.4.pyc | Bin 0 -> 3121 bytes ...lask_integration.cpython-312-pytest-7.4.4.pyc | Bin 0 -> 4876 bytes ...lask_integration.cpython-312-pytest-8.3.4.pyc | Bin 0 -> 5000 bytes 7 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 __pycache__/hello.cpython-312.pyc create mode 100644 tests/__pycache__/__init__.cpython-312.pyc create mode 100644 tests/__pycache__/test_auth.cpython-312-pytest-7.4.4.pyc create mode 100644 tests/__pycache__/test_auth.cpython-312-pytest-8.3.4.pyc create mode 100644 tests/__pycache__/test_fastapi_integration.cpython-312-pytest-7.4.4.pyc create mode 100644 tests/__pycache__/test_flask_integration.cpython-312-pytest-7.4.4.pyc create mode 100644 tests/__pycache__/test_flask_integration.cpython-312-pytest-8.3.4.pyc diff --git a/__pycache__/hello.cpython-312.pyc b/__pycache__/hello.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..66ac2603eb0515198912eb13fcad85533cd4a0d1 GIT binary patch literal 1833 zcmZux&2Jl35TCbSUVH6)lBO*!2?7a>l$fs;gxX3Wh%}|jCFSDFYPzvL z_27y~?XieMK4}+LJ55nH&g|^G`JUgr-%6zd z;A8%{UH?V}_?te4Pj0~M8w_9zJn&cmA*-@bsVbpbRT;^YKnwM%9vW36%vE!tSv46b zz!}f)pdCHcf&xq6shi+wJhy{!QmKVnhNpXmmqTja&+q7ilOmqzYbUd>qm-j3ZtV<2 zUSSh>@xHaAWda7guy*>We#U#nVJ2Q_6Nc8+QJ(ik*53vYlW6z)OYHRfqeDvuow3eX zdHe~fl~t*K9ysy3%n5$2&ErJo*5YR5*E(fQYEC<8$ngcFi%r2VT~8L9QO&Q*(PfWE ziJx>ql9^8|Tq@I>gW~p*kikKq1{9*Kssh1{YqE`G4k}mWOn4IbzKZ(D8 zreD3c_;7aNmos~(miMMF?H>E$sb!26`e5V>Vj9&~%7!#;n_6hwvS8a`(`yIB7j64m z+X)6IYMcl`xDc-)*@o0O%}rP&B|^7AOzCbyUorKW=Ap?81Qo z#mT$NyEB#D$#?gx_a1z(Yn|QI&ZZ6s1fk7W zd2~XX9e7bg9zQ^{GwFdw4B7^Z2gkd;*w(h`1s7c%#!$cy@JYL95%W6 cDWy57c15f}GZ}%n7{vI<%*e=C#0+Es03Wm|R{#J2 literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_auth.cpython-312-pytest-7.4.4.pyc b/tests/__pycache__/test_auth.cpython-312-pytest-7.4.4.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3f97ccf9b32e4e64319db18c8812d927c2ad8780 GIT binary patch literal 8561 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 YP6On=mPGc2uUjQ{?^1h|?We2w7hLmke*gdg literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_auth.cpython-312-pytest-8.3.4.pyc b/tests/__pycache__/test_auth.cpython-312-pytest-8.3.4.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dd5875099ae3a8ab1126010f18b17d3a943f23c1 GIT binary patch literal 8826 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 W{a%szAN;;o;ZJP!$M{J)i~j@^^3o&# literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_fastapi_integration.cpython-312-pytest-7.4.4.pyc b/tests/__pycache__/test_fastapi_integration.cpython-312-pytest-7.4.4.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dc86c1b8c2eb95f5c8a6bf8b32bc0e36313e5476 GIT binary patch literal 3121 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 U+OZHW2yYDFI8!siQ7)qH-?Va&L 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 literal 0 HcmV?d00001 From 9e55d3d57451ea05c42d2110383726f50490cb80 Mon Sep 17 00:00:00 2001 From: ABINFLYCATCH Date: Mon, 10 Feb 2025 14:36:02 +0530 Subject: [PATCH 21/28] removed --- flycatch_auth/.gitignore => .gitignore | 0 __pycache__/hello.cpython-312.pyc | Bin 1833 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) rename flycatch_auth/.gitignore => .gitignore (100%) delete mode 100644 __pycache__/hello.cpython-312.pyc diff --git a/flycatch_auth/.gitignore b/.gitignore similarity index 100% rename from flycatch_auth/.gitignore rename to .gitignore diff --git a/__pycache__/hello.cpython-312.pyc b/__pycache__/hello.cpython-312.pyc deleted file mode 100644 index 66ac2603eb0515198912eb13fcad85533cd4a0d1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1833 zcmZux&2Jl35TCbSUVH6)lBO*!2?7a>l$fs;gxX3Wh%}|jCFSDFYPzvL z_27y~?XieMK4}+LJ55nH&g|^G`JUgr-%6zd z;A8%{UH?V}_?te4Pj0~M8w_9zJn&cmA*-@bsVbpbRT;^YKnwM%9vW36%vE!tSv46b zz!}f)pdCHcf&xq6shi+wJhy{!QmKVnhNpXmmqTja&+q7ilOmqzYbUd>qm-j3ZtV<2 zUSSh>@xHaAWda7guy*>We#U#nVJ2Q_6Nc8+QJ(ik*53vYlW6z)OYHRfqeDvuow3eX zdHe~fl~t*K9ysy3%n5$2&ErJo*5YR5*E(fQYEC<8$ngcFi%r2VT~8L9QO&Q*(PfWE ziJx>ql9^8|Tq@I>gW~p*kikKq1{9*Kssh1{YqE`G4k}mWOn4IbzKZ(D8 zreD3c_;7aNmos~(miMMF?H>E$sb!26`e5V>Vj9&~%7!#;n_6hwvS8a`(`yIB7j64m z+X)6IYMcl`xDc-)*@o0O%}rP&B|^7AOzCbyUorKW=Ap?81Qo z#mT$NyEB#D$#?gx_a1z(Yn|QI&ZZ6s1fk7W zd2~XX9e Date: Mon, 10 Feb 2025 14:37:19 +0530 Subject: [PATCH 22/28] dist removed --- dist/flycatch_auth-0.1.0-py3-none-any.whl | Bin 20286 -> 0 bytes dist/flycatch_auth-0.1.0.tar.gz | Bin 16275 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 dist/flycatch_auth-0.1.0-py3-none-any.whl delete mode 100644 dist/flycatch_auth-0.1.0.tar.gz diff --git a/dist/flycatch_auth-0.1.0-py3-none-any.whl b/dist/flycatch_auth-0.1.0-py3-none-any.whl deleted file mode 100644 index 90582243bae7f4a1376e4ce659dcf13814aa9452..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20286 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 diff --git a/dist/flycatch_auth-0.1.0.tar.gz b/dist/flycatch_auth-0.1.0.tar.gz deleted file mode 100644 index c25db5142feb53088d6d4fdebc2299cbbd094cd1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16275 zcmbuG<8~&D4uxymw#}(++qP}nw%w_1+qOBiZM^5Zhm$|KfM8|sXN52Z3JU1I00EfW zdYKrxnphebxw%@>GchnTFfq6qIRoAJ*|=!r9DX*YdmD*anoXu@+5 zPj5-iwWT>(6$Ya+f&;wl0t}4JidQkkH&d)48U~^ z{DKrzR+XTa177MspuPF`@%v!=wgq+svhcvh@AN^qz=Tm~&H=s!scPECgYsCszbf|` z6bixPIy&=dFt5=kRkDormxdi2#f%~AX31dXqdk35Q)HfcY+iV)_wHD-Pn(?Yr&VQ`MrU_ znC~Xpsx(xmLVn;5<`;C6)nt(|TFsJ=+;9gUhVJMm8CL$-rR)d=m~KdBT23h=Caw0p z&s+>CmQG3J?<<>XLzXg+rLj#19i4%j!Jt{lV{yS;H9TaI&8GUemZxHsG;#2{(n<#8 zX7|LiM=(PD)KiG7nhGQiYVX<&R8`#PULIQF&D?pwc+bv({bN#T`lnJgKsIjAzy$qK z>Vs95(jasU7?Jar(g~uLQb0~of=FPD8bE&f8A@sqRedg_R{E4)GheQoqGn(LlsDLF z7UF{Z_QXr5wF1E>Xc=ZLp6^Hy+C=ASp!EapX_F+GipIs(T{1@5XFdsv&j|0rbOfRZ%@F>g$VfIF$9o)@4Z$pt*7<;5LF7v8LQJGGu?5 zhDapOI87Z>)HO;LWPxom$uHRZc2ZgKSmZ%AAVpWqlu?(LVlE-hVSioqM=kJJ{wxn) zb!-D|#}*9>JrXI{qYYX;#5rak@*j`JVRJAz$`)e})ZbeK)vry~nA1>pYAo6xLmxS6 zQLGbrWwInX0$VsG*E=RSBJ)!8M>>kHLViSbG*io%Rwl0vE5u1L49H`4JiHUxM`x3p z!S?{4SEmn1UWjG?Ww#G6?DWXcr3>TqF7o2<)l`lTf<;lFiO~!EMMzfy?>~WpC8o&w%VdZ^ zRf;YaBMPmn7SUjQ$cs}U_V4~RI+I~Vfi~h9ODCMhC>J_02DqvV#F=;sLm0SM4IDkF zLs?YzzFsv2`tSl>Q9_+Ai46(}S(?r=0r7JJ7OSie<`TCso*THgGJKnq2shD({Q^h5 zFA~qeC8;SQK_buv30c;FDl_zB)y%v0f*;(>;iNz?2H8F*JqW}1aMdYyVSw0oI+5#z`{4yCm2}I~dDuY?U7x;Mm zUjc=h`4Y_QhP(zvfWs0CvM=g4bwibsASN`D*avW5G|lJ`8G;ukc1`LrYbG%44vZp? zrQm>$5`9M$XyTw@H<}#ywD9aeB2i}vr{p0dE8*cyizd?2l2!tmxYghlOffdEARd%w zmD0&z*EEI3u31Yau7RXUF}-0E6=-1gG_AKO^2sdT45VSOa#U$GOfpNwGVo>v>!NQk zv{ChwR--0}3kXCgno%N^7jVWZ?BBunAq&ViduUI|f6U zCvJ4Q7<%9ikuoC7`!_5o&gi6?K%<2n;|jxuD^*rnxyt>KqEq!FlM|=RWE~>~O1F7y_w_I*W+|g< zziWt*;lTDFlA@_ZA4yci)|oon^@5ACr!&K5Dhy7Ck_7|dW}~P4$Z|?ip$V|c^Omki zHu{)WGi1;>@Q#EdlBMJVHNeIy2}~pEy;|>Z8HJKD1E-WkI!0&pNec(i4V@_W>>4JG zXd5U4KTZFjz&uL+CM`VyV@4}l(d4f+f;_KyB?*XX#+DO_GI&SO)q!LdQ+NfIMhfF; zE+-vHGEvcL`Oi$SgiAPatbGYvd{H_mgiIb*8{VDYuRhy)yLv_=I>ZU(%`WF!l)gb) z#K&Vdcu9X%cUq3$FKW}~LbBMO1j65np$t~V_W5M~Z2;GDC3Vh|v(B7&t3~trQxu6o zG-#Sck{rmcloj+~b|62nODfuJSQ6)O8Y>A>T+7IhqgZ~etW6E?hjcB$l*K!gZ-h6T zu2E_n3|z4T%*&pLnQSen!_pu=3q+(t%-+F*Z~$(=HMJZdim@+f4yr!M1T{Psxs=bk zSzxI+x|qCy0MHaSt9=9BR~lduJX3ND(;0z573*qcsBuDq2?xIFtUzTkTuhio&kSU3 zlNMbX?#hXM1_jO_skOj176$T2-4HZXwj?awswc5Y{AKST0>BWCMD3PCVW$`}(Hhcu zLiFQ8(Il$^LEKKw<~D84-LrL}@JRdrkrcm;X9&{k_Kc5`oscp7k<=yz5da6!;263< z`f!keN(DB=!EMI}qaIYCkjX6|+*_ohv=-f^p1SPBg^H4(;!T!g^BKKCWde^pORU)4 z7nrWCpgalYYiLVCj71I`EFvk?leR55&BvSNNkSEVG()5oC6P|0kcw#2!BI}b=@x+? zGzbh!FWiB^^#T_*X)^r@)^QVj z{WJ=qkYHE*hnnOzkUD-_XRANLdRnkbCDPkCCgHW}@DRyp1RblP^D zzWhBlugGqT(rps_cRS8IsZ@kmvk2Y;`B9jLf<=qhu3=7 ztkVWoY)C_7Q4)rfusj{i7>Yl=5gbYE4YMcovG;A!Ri)eSz=H(jKbr1;k+UFhFhQ{) z1(^A{-Cocb3s^nEtpsg<{#k1QN(jLTNR3faO63TtN<)(f&ghXB3W`(i%}O&Z;neiYMbE zjoR>EGfj$Wqep_r)(ntyjT%hM9aO>U1r?rFw~ZBx0m*MBV4%+-tB%Cv3*9YIadrqS zWSD{q+X`$gkuwoWDK($=ck5FKR?j6sR4 zxS55iMJNeBENXiQsW(&vZ*nkD%rqY@U&ReWZWuPyH4=`5jEL2!6oP>UN{nmT2}1D$ z_}~kkdHq?$3zpo3oca9R$qk4WhT7RxxSKf zE~EIp=7Q%xvX^nPViYp7ENk#FyohAKW}J{fyhfydS**G4wPa-yU|8{|^5V>!!V%fm z%2@}{1w^zN$DA}%8faM^zH9b@wE-$Sh3h!Q+_5pC(0Wg~1g}DWx2~ln?Rc`>lp2s3 z1b`qlg-f495om0-=tpy0V&w;D7QjeSgkR|slW5cYHZ5er-FKo}UT;cRs`i=5Vhb>4 zmSbzGsxphj6SDNoI6fqzHek27MDts+Vpye^E0~3FEiTl(S_Vc`+myAkO*Ct*8Q`p2 z)C_xoXX!!}IbbM^x)rV5qBqeDr@*y|om5d>=(2Tr8jo*TsArhl9Dymck|n7GJDE7O z*aqxRiZoay3H7-~?v@gnzuc(WYp4p^jFE zu#pC^u-j>UcoFo)(Iv}FjuIPixq$l>gJ&~RM`L{8s7R!Ln*aP`dJWtvLyhEL1NM)G zEJ25=?qJxkT5Q8xMi-ATJ{e)Se-Kl?N-WJM1RXP^E!@QkS`8FJgh}D>N@q~U8%{8? z*<5=09N_{l2fy~%L-*vQG^^AlOvY3H_%c2z328B$!kr)u8zjeipt7P(mMMLt;!IzY zqi^fL@9E`N-0*S&oqI4FqE+FRV}m^7J;}w;&K4D%AY4}(`|NC@>ec+HOafAwtA?q@XREeT`5@fl= zThBeZ>_neN;dG`_JHr#AG||NtSIh>jYC{c@wZh|r=`P%%QE8hO6=4cb;|wF~nwdkk zb3|&cl#Lw)Z)&xYPM@lzhPm*`1WGT2?~<%5wk3cGSk}M`0otLCA||0CW@f346Dg4& z@EN2l!S{_%a`I1j+BC*}6O=2#R$w#Cs9kcxbr)gQ!NGI01fX)!=4Q z^15ieL~OnhyQ!vcI!aE1dQVR(ZK93GB}8oaWw>WrILlg$t9k#|F@iT?4hhR>ds`d) zhl!8h8B8K>&<~3hqCMT%e(H`}R9f%-#y{_3Mw z&^ivxt^qoE+a}%dJR~Y0P{ErLo))uY#7zf07|BkAm`QGj@ng(gpMG%fqMZIX{3kZ- z0w)z@6$GKrVNiE8p{OgQyev0;6vI7Oe}|Tox%b& z20$=tE>1es5#6Q)AB&aWgC@QBEqO%WRdMF8Ha!W7nJidutHLLh`jT!` zsnw~nnlbt&gT;CAf2RZ9i-?&3XlQLNkqM(xHVvU5I(&+>t^|w_35{X*7z*uu9m*tx zzVDiDs$Kb;oRV$<&tUGMwe=~azq+H?FzHL1U}&?GseHt>Sz z6s-)>hR8@=-y6y=;@V7hBeBv*^P;Xr=>%Y{)%+vODKyfL;v$<((-pC-H9SH(IWd{) zc){g~B^JnVc?;cFJU;m3a*z(oWcF)~e7z zbC0^1J{L?HNF$_ly-cVEVypC(7z1dNv~BrZA3`NHay`Cr9ZB!W6(s->)b}S5k(U( zV%2U~yjCsv_1(qA~TflrnZ|eS&qaP-$|k%1ydy;6R?? zDvQ{Fpj6807%)+gV$ouQKlP0Aj65uv-cyriSKItkOT&QYXj@qq3@7ZO;bf`^-K)V< z{@U6?NRlnD)5{eRCUw!gzVKPC5}6-CDTRRzOF<0rDeD0kGFB46g|^r^*)zsKrXUYu zBT}@fzJq8DyViE8k@pm8wWiQJCym?u%sNb2iV=`Kq9tzADpiq3Yt;lqItwF?spZk~ z94a3hH_YUo4qXZFsuOuS%1=?>GpP=(5)udb`26!ZFOdyXqV8yLHDshYwYbeRFfgrd zqJ`R*fZHrGY%9voTW;+D6H(>-8o{{{6TWp7t4y#ON*Z358-da6#YjO5bEm1DnsXGp z$rv*WSHp-9d@~voB~xz*7n~DID+cB&eR_Bd^`xn?5Q9T7bD2ZTc2l?q$GBpkAesBh z)`7`}0%8x6@cYjN@d~zb(~ejSBTOAcVg+)8zS^;};}EnXALXaAy}ZaZHtr9LIkOUa zI@xxUx)Y*(i9cH>sh-gwwq$X94-)2xwxjTTyz5J^jK@`J@$hB?1D|x*ajR&pH?XT+ z2i36hTI5Vj%aRZ|0vcwsmTHpO*ezAr3Uf$M5O`gKC?UGUqLW#BXfL7fT2Z)HEa4>W zAtggg(EIh!mY59|K)8q5*kX@VNWj9JN?t)V+eq1Jqi*x;2dmL)601eY+G5>d*YTl( z(*?QKwKMJ~nUWHwQ0Y`|a#5%ZLS1!C9%X!hhsBYw%f@Q}A-khA17#j8$`i|5(jZkG ze+zy+8HJTVQkiFp%|i)G|7@EE^%SKLLN6elD!0;+pz7liuQnPfly*DL~0 zuQaunfo7$B(WnOZR}(j6NXra;u|n564nl8=dbo8194+6|9zsjJX@VAsX|=XKVh9(i z7QwzL38HB+m062%MQSB^i-~6Vw3fEibncHvK8F_cI+;ypSy9IUi_}zv+XE6{`oy5@ zSivN7OlV>`_Xs=}O17dR)M-5h`pqSTGgPqx6E-x15hcCNMH#C}F1dD%9VOj{%$KyU zjg-RJiF`wZ=mx~}7>p1fXi;H+EDZNmk{mPy_RmAIKZeNZeexXF~uV zKzBuHIj2J&x@=Z85vo(aJ)_|?4B0W>d#0+4YMV8nwsJ{&todh)ZbD`;re2~MpDl4Z zLVp!FG+#tpwdJGGyZDTl?vYMb)vCQ)$#fbIXt;?ORm8zWxu=V(oa22%Ya2r6a5+wG zMQk1eF@@kz79U`A9GAUS54Eu1$sLp)kwe}~HzRX1a-o3+E;6H3Pe=_ljZ9}I^jq4A z)^ko`g@8fNI3t_ffoWbkglw{v!jw!|fkU>B+1NHg-r;8`Poh(Uh-^TtVuQ2s{>YRD zDw#;uhtV9C>$VRgJf@PN|L9~ucG8`yLNOR>iRp$(;x?0wjR;bm4U`)5y(iXS_0qj7 z^R!`fG`g%r1w-jFrh@gJ<#iEIEyhCefO&`T{Lp@ZO59l83qgePsMPj-Cz^+8mc3rpvs zC8|uuo#!Lf(nCtKt5}2a`S0P-HFwRW+WFKuJAGFWv%BrjMC0qnF~++&MbzXa(~=5T zE)_bd3kSC~;!h&{!3{-vtE5?L$x*Bo<%`;Msr{0-*|qX00b5!XoX>C$e<(V*kA zVSwbIQ?a)iR{eH@gH5S2)dPvskN#+%e?$EEJ(NEwG#N*+9SI-WveE(Gaq)&@{ZlaN zKs-K_!@4V0Y>Keub*=86^w^^so)wurG$e-neAlQ{zJ(c zeXmXtX5|`E_VmtSi%dcrXotXRNf*Mt6Dfc<^ zO70#z6%EW0keKurnDOm6beNcZU2;!KsIX9)sf&ta(Q+2^R8we7k)%#iM|-77I=?qb z;0ZT93wNYMj13FJW(jNVC|ot}S#hkLnjRZ%OqDZaGD@6|XVBlFEx)xjCh+5(9t*~& z`$;nI%@NU!ba4I!JANRS6-U8GQlV;dPrtziYs|T*JLzVddF0R*e%i?K1qUQUK{QL& z;wXyo7AW#PWu(ZEN} ziW8l)eyv>jd8$q#QKGqE-VK!t7R#vUnRP|TgL(1Uo87vT8ENdn;5-rYI4`sYf5+qW z4Xc-Yc$+36U>Ymf4@z>8im;?_HUE(uGkg?PTNHED90y5!1+sKcl825wv`tG2gXWtG?Sf@$KW~(1hk@yrjq-1r zOwdqH6x}Oj=pXpTIPE#ll&y+`LQPpZnc-GkW^k?uz9}b@Z7*mwddIPo%?ssZ9+#W6 zqppwpY?AQIy=N`ok==wX7su8sBL$tU+XdDM(7fC*rreeeksMPyGsS+%%@>&_HMZcX zrGIWlZtAb}0+nX&%P0>qqGVj&;cQPg2lw^)=Ps83vx_%aGy11CQ8O>32$#D}gKJR) zHQY#$WZ8d`ft+CQbi#ZZn;*e8v1%l4of!^{A_3zYQT|S)9@c|0HlYr<4V9dfj&ueq zE7MlU*hXzN?O^8Mu_In-2T_`oE_Cf4I-H~>q1lBxo#?Q<>1j?+dN}lr0nU-3Uvtildtym3os>|FPV+n{qk z>hw6dXwPZZ$b=~orphqGk@2LNgL= zgV_8d1exn4oL!1M2RCQ|3n;`~psAK4F_3pan|M}2n)Ke13a!tsXyWd`YyyE9%`K2s|fS*%jNX_z%NE>7SA&6Mj~rG<-T1tKlA zM#gx9pwkT|zhyQpE7q~nAckff`d!-z4oGTc)@H()JRV3o|xiH62&`dLk*^a7G2SQS=b-^_ZUwdt?6WRT7)^^G@UpRh*f#JGJsW&%Z zswy3vOdpfpk8R-4v)wyL6gn*=2O{>SL=jp-lc@oH&|#WZ!Pd^@{3uQZO?-@JO~83t z#JS%`ZbX~zP&q3g8ZtLv{yyVtLAj) zew<`cV8dh{gmhJWN1UjoRgI-1dGPGO3)Sbp?k2aGoY!_$oG%pnx5b2}F-8#XYxusY zj+COtT5m>q&Kpj=*nY5$5$bk_wA7nefk(z|b4e%FFGMSqGNl#Mi_f)?P0~M8D4gMt32j-_k(hc*U z$q-=ihXt>uj{;zwZ+r2H<6KVy{;Em%`ki!L+h8xdAH_S9YWcb@@C+pZ5p-)?PuFox zvJ-NZ`0cxx<85wtwWScOt4z05bplrnO0(NsvZ~I>zs~E?!W)~$8orKyp)t;~Asg%= zwN|EF{PqG}=yPoi_zL4=ne&Q|nc&E|24jGT@tZ>e{a&b0);aU;O1;V&I|h8`a6)C| zsoUn&YUEZrWtfh;ASl=@RKGlEF6(vTqE03XlwKx%nph*PiO=w_-yjg>;8Rr_TBO+*Y)KvCWsT60#VdJb*A^ zkZ@VYgVxwhzVOQ7q|%+RC?%&~6PK=Fq9KJSnv;D}UJn+HqO=BDV@YMZd&9rD$;#o< z?hMkZo z*x5n0%!4jZ?CuNE*c)CAu}!4rU+Gr&nYZS-SuHstHZOPhsmaqnp>J0Jt+>)Knc(u? z1x}T(tYMUQL4??&9A+S^zzAmPU#w7IT~WMrp1;qLN|m+oDR-3(W|g`f4^mlGu?$dF z=90A%L#XA1(m+V$Eot}(+Rkbl!DG3M+)l7`(yNY|9+2}8K)=G65r)AigNb2EA%IMd zsOx|kX?O4_VJ&ysc9@t$=|z>^5+`+;ft3lg+4I%1tlO~=B=Nxn;v)jKC@^+A7DUa- zQjD{oBt0}#;kdyTNNNSU3OUKAt5EmS%gSv7E;3<3#~>lU=G18ba%=o!Ms6~E%Di*X z*5NW8e-~TG#M0ml=Xa!b!0ELpBIxAmP<29ybuo=`8mvheCXAc0)3Ye`b5vF9vJ6xq zy=DJoo8>IS54a^xdJMxXiGOL|hk6bfjk}c{U8M?Kr-+zhbgT|ak<(uB|F_TOBb6t8fnqKqh*_{kjBugS-*RR2gkD5*{lL6-iOHYE+OtR}0f>Vo? zzIKp~PCkR5H(0X87Wb!=a_1bcC19^5H5!c$MRMn4wfgdl8Bf}l zer|x0D(s!~QE{~aw;S07zIGuPY<4Zd-g$Gn`jUd}l8cn1 zOv*ws?e$UK7b7RT_X$9e9+^r{DNX=CxlnDGgU#Fye!X!IjiOpu0=_oIMSr|3!BeQR zMo+x_uy(-#I=r-c?Z27M(qeVhN1BB~xOz{dq**ZxymZ)@xhU{YC|Q}+tZs4H0iH8A z<}JOfW6}0DX8(H8P@jonw(H8@YlH<&6x(!4j|oLdjACNt{e^8v?@pmB6>fUfkJRr$ zx#Y+GWHA3(1s$tWl zdTtc?7U-xuZRf2{9eNm!jGd5!Kx+@gF2_Gy(U+%FoE(z}5I}eKeLNUjhe$2jcf=;r zYL^0QQWuYd>1UX_z)d`Mh9-Q&FYv(der4bhfXDYZJNRoB)^0uq*XHZTwbHz+B%)RVppwChS6I+C-G-sWP>XVZF& z9Ik<@;Ru@%(YYD?K?IS5{-b!g7}w;wOdreohvx{oR*$e`Yeh#vxXstrUG1DMj3D%% z0tEjmWP!-Kc5641`yV8#)1zny%}|4dS)!}~4fV=bKlAoPegxvMuOh3n2d9_9WW#8K z*Aq%L`s|%#L`w1cRs51r@pT8wf_*ho$d;n59hMlu8hz=Rk=pdqJu&6H>Yc8Da(^nu z9u7Q_A~S}=N;B#}25%bO2^K?Y`yFWQ^gi}fDBK#%1S+i!KKz^|M~c6MrJT89J2^io zKZf2ocm&`Avhwm25f%av^$GI#5eM(b=iMoj+f1B1#rubV%FyY=cA%q0yH{$a0&%v{8FrzGD2%&tL#q#i- z|kYRu{=nY`}Ve;CB3@$O)kHjo@ne+Vi< z!eVd`O&ua34?hgXBE{n6911QL;0`doga798;^NB$>ATmiU0-e%$qaVnFq(MV!+G@6X8$0_tWO+nzk!3+ng{XhizqikmgMOH~d5A$a(QFVMsJ(ZsPU;PJ;b z;oh++fHy0jDE&2FUfwaaM1-Qm!i0f=13CdzuXP6H2#MMf;8U955dj$)mGTo10BIve z1MN#Q_&~Xzca2(%pRFh$3(T~`CxQixDlS<+fSAtY4~D`6vq#XI-8W{3JhaKf{p}AJ zS2-~Tzqgfge|&^OenJ7tLg57hHimw?Sv|fv?*37UU~!ZuZ?}yYymYgtwBCoop~xC0IDwJL?@7EMU;#tlE21LI#O-$uAl&Te6~iIK%O`=XR!?q{ z5k(&MGrm1sr2L2oqCISwI^69--{bNJIRLSh-vxq}NA%!!=o>#=Ow#_%iamdQG4;N` zEtT`+d_pcniXa1Vbq_A!{I5_0dn_LFQAFa z12A<7QYeC~&-{cBd*mLJVZ{Thv&87?9%F^`EL7%q!&BrY`hE^M{*I-!z4P&K1$Dz_ zQ2w5wys*E0TBS^c8UIj>!YGWKhk+L$bUGRU^AJ_BXuIdbBpYe0wC`k81hyZN9%QO8 zCfb;4NrxX^*10pMkXz>$ZrNN7ZzHmJxHA~Z-WDO(gCK;+uN|xwf;=Y{zMm(=_#yWnZ|mKj)wj$&&#;KC~~TN$O|~_bT1P zyO)1qm=_Ym`1#)HPl`x8M0uI%0w zyP5MAf@@0-jwD0?9Tyc>o3UTWhU-?hbzMWx!ThBndz?D24S73h7(YsObwv=16x-{k zy7cY~2dCL6-z`JY33{#cjDok^!?X?I>sHB`r19+^humR<_D#1qVINQ#b#i9S23qa@ zh8MJF&m_$qrXEO<__Tz{2RYSU`s&>Y1iL#sv5ZgVx~UlnuD#tiZNV%+tQ&>)HdX@ z(r+D0Rp*>c?%}nSPNW_@ZalBXXE_2L+tQ8u9MF264zI|S-?ZYiNJ2yz^C$eELPL_)Yj<|tqTY0e@#i8|E*JeltqCj6b zdcE3oEQeCxqTX=DVF{+p_AsCqx0hQbi+_7`7yxB<<<}5o}wwD!aA)UZlx)zhK z){3{gJvqBClg{xVAZw}(B)9=%kgg6YegxUW(m>hKGEEz)W{QE0Q5YEr8g^_jnf^>WI)7I=MIo1GfTa%|ti zJJ!Ryo-cnKE4y-JxZFKsYM6xx>)c$n!-6s$IU;=`xHwd<2x-n*LP@ho>5yq*XG|%a z8`q`dx1Ej*xHqF@r8f+LVyxKsn=j-N_Q22Ln91_|Iy!QEU42Hd82{4c=P--K{V0oj)A@ z{Xe!>4@dREwTwZ55a=+nfYtufvu|JM9KLSpxp~4$f!K`d0&}r%eZSU^ZyVp+Q^1o8 z9VNd2(nP59D z5h?mur*2Q^MLHt*-o$h}Pq5%&8 z{(`r=K|q2>+x9oW^=$G99uN?fz@(tA`;VCBK**?};?uECVQ!MO{JY#A;sWIQnVH)m z<8Q9;F{TCf+xGEWd}7Eg21R+7#1TP$b;%Q+kH+M+i1MCB6;kz4dmo!{!e}3R$^G@m zzWt@kuh8Mvpq*z?5)(wmV_+X{ax5n#fEqh6P;GgF`=vjmNl%L@+ z%l2l2(?$l}?%m#&gyByQpVvfx4@XWcxH4C5CK-SbCWXPFr!#l-Kh}VcLpXfrHM|Eb z{qt?0cL4wQFknC0|83sKR^&}4ClUzDN!}H3*l!Zejlp4Pm-#{^iSb zZK|-N7RDzS0d*+ZYRDsZS*Z0oNS=GlzjxS7yo45yyT{eb+FTmU9i!?;F*245z<=&1 zZ4y|9>kt7r2n6)3wgC$6Y_9=~761uM))G~N+ptWmG6bicR=ry*P5MgZWI`TEW^MPD zf=^5f4y-$$OTTo^)rQjAdjK0V6#R*Jkiz8p7R7H0ThAagToFX zF~t+SVd7ni?MTw#N-H7*T+E^&o?Eh}MR+h%dWGDU>&1kxDopohYjxHK%_PzDPJ;;E z@WRmky|V%rgZnZB5s}Si$obvMk%^xBc}%Noc-PC7vo6zk z2L-Fgn;q?j%{ln_+zq4jcpicluQ)IQw0r>BObdDs1vfY2^dErl$>lh}8~BmHnzYUx zg;!(B+&rK8ysOw_WY_GI0?!Z#0y?Ey!6iGTv?rI9!?QCiWg)xnF|}MN#WQ(_By@(} zXw+m_0b42LK02?++fss zcAs14ym4&)D_lO>mJY+q0=@vR#IX=P?JI@%`L`lKb(`-D>uJnu%6a9W3Mhp8!YBJj zM=0S-UEHidws$%v$FR~p;5y+w;NWX@@g8QHNB`T&6!Pn@U6!LHX#Et{6v_)id^Om8 z#5%B9hT-A5Mhs>MM|na4KW;2H))=U_l$)CXTEYD=Kok$QqaQxGar`vqtRFQVFmpXP zxNzuu&#Lmxm=MUfjQ1{LfA0OTeDHq$Y$oe@Xt*HbI|%z5s0x zvNbieOQ7ik=0mz<7CST=!%i|KVnfnjh^gogcM~5ge5UhrZfH7Y^zfg;#M7h?sqTi7 znfH7k^Irpek9zHfl%qr7=_E+?b32FlYWkEgW|cg(;zzNs07VE}ngiE$U(y{>6uU&5 zlW%ZpWQXiEB4v1MMj{3f1oo?^O7<`7-YPEEF}}PaYx)D@32biN{u@CL!F#&Kj7Rw3 z*0tL|H+t21tGBj-@2M!(W4hRoVo1Nd=@81~}6Gz$^l6aJjNohf1ecLCA z?y#^X=XE`Nb4UdoMbi^G_seQ(27o4E5RegA1==U|;sC5`qihQxA%=+I%> zC<0sLxzZphezaI?+A9{_^`>ci30!UM_bl;mY8X_=-^mRKUbrYRO5#P$pg-C5pZr<| z{TU5;^N>DJND<3B`8JNo2>+DK|02woKq+^{^iZMr>c}Y=DuhP7&(f{jw?~ZzElNbk zwtWkfS1FG>X7Wcpmzxn@mhR!PdbZ`D&cK@6fjb?4f1j8W)#?hWsJ zO1tT&^>#5w{dktL!mZhUEWi)-k<`hxy`z2*Mjbb^40xObcn}EKJOLn zOY8L9jad@^eJHVzv^J)EZC`vVIQYq!J@sETrO(Ak8T?~`>zd?B85hm`!8;wnmM^E{ ziWHL0`7LV@Dnnq(h4U_v-7EXKzOVH#=0RmxtZI~k33Li$!6 zj${~9DpuUOBlqbluT1HL2u(!t@Asv%i1j%g5bZ{9Y%;B#dBuo(eQeYg^2p=5`&}B( zvSCzS53AJpNf+2Dz04?r^Yr4bZ;;6@dG(-|nOj{aO(T8ru&r`Ic^hWTEVC@F^D%ruHw@gHHNKc9=-4$>xR`?Ut+Cb09^8W8RU zVE(ME`XMjIQ!FhS5d6hu`DVvmg?`P;3Fgd7Fv<9)?=TYRy#q(+X)CHUav5iiTtoi% ztJek4cE8rv$2<|k0bSaHMQ&R5yep@vkFy%isIrn?JYlW{rU$P8(0>-px%-Ye74kd- z=9(!Lwm$>THp_nB3RD4$)Sm|u*}m$SV&GD}KHBl8&j<`4&(S zoT;)kBmQrH=+|Y!;zyL&4uS8(Vff!_av?YE-L|*3518%)e

zA9H*)@V)#p9@*K$h5 zgycc-H&C4m%>=kn?o?KDBk)?b zoQ5q^+$}>)Z8mJB7denmj7fmpVMCCotIufdOkdlzI$GRYgYVlf>a;nu)f=_!AD;_M z1i#b#I{K3@%In#)Ros1Cr~KNPbL+n$viuCU$44W`P0 z!zKNIYPX;#ha9EfbCEDJ_1PDg({!0ByWZg!Q*L(!aQaz)Vy%S{AY=a0!GN(@UvO0S2{_C9O;Bj_ z`18_laQuWy;Wj=^0u1@toBS#Pnm_|_8ZG=DMrB&~4~)_46b!|e2RdHkMNK1-u4EI^ z_A?aOQ+}cxi2{9QLMD*w*Ttge$Fq==o+03*%HxH3u6`~ zy!RF%uF}ExDd8yZ0oC)NJceTSU&LxvRm!LjxfS#nc*1Mqr+lBr_W^hfu=ohGDa*H4 zPh=HIgg~(ww_-msfDrJ~9kD#TF42!3w>t`$Z`*zWtf06L0}KwH{#25>auQ6rwRR64 zJ;Za9ZI70~n-LI^D3#9|Bd1mG9yEIzIE4xM#G@VtNAMoo-`~KRj^5``G93}%WP55+ z^-y@Y(Jx5zL_SVKSZwXC77p~g^S^jyo-G?}HWKwqH+rU+jU^rsI3OP!)hc~aW02jW jO()&w;QQN}B-rxj%%3)Y{NJfP;8!4fN5u*V3mE8sT{|1> From bd7fc14872a5e126ee094e41f0c061ca20fdc49a Mon Sep 17 00:00:00 2001 From: ABINFLYCATCH Date: Mon, 10 Feb 2025 14:45:51 +0530 Subject: [PATCH 23/28] files removed and redme added --- README.md | 23 ++--- flycatch_auth/tests/test_auth.py | 95 ------------------- .../tests/test_fastapi_integration.py | 38 -------- 3 files changed, 12 insertions(+), 144 deletions(-) delete mode 100644 flycatch_auth/tests/test_auth.py delete mode 100644 flycatch_auth/tests/test_fastapi_integration.py diff --git a/README.md b/README.md index f87c1f7..80c2f20 100644 --- a/README.md +++ b/README.md @@ -39,19 +39,20 @@ class Userservice(IdentityService): "grants": [list(map(lambda user: user.permission.name, users))] # read_user } -auth = Auth( - user_service=MockUserService(), - credential_checker=lambda input, user: input == user["password"], - jwt=AuthCoreJwtConfig( - enable=True, - secret="your-secret-key", - expiresIn="1h", - refresh=True, - prefix="/auth/jwt", - ), +jwt_config = AuthCoreJwtConfig( + enable=True, + secret="mysecret", + expiresIn="2h", + refresh=True, + prefix="/auth/jwt", ) -auth.init_app(app) +auth.init_app( + app=app, + user_service=MockUserService(), + credential_checker=lambda input, user: input == user, + jwt=jwt_config, +) @app.route("/me") diff --git a/flycatch_auth/tests/test_auth.py b/flycatch_auth/tests/test_auth.py deleted file mode 100644 index 5b6fa26..0000000 --- a/flycatch_auth/tests/test_auth.py +++ /dev/null @@ -1,95 +0,0 @@ -import pytest -from flask import Flask, request, jsonify -from flycatch_auth import auth, AuthCoreJwtConfig - - -class MockUserService: - def load_user(self, username: str): - return { - "id": "1", - "username": "testuser", - "password": "password123", - "grants": ["read_user"], - } - - -@pytest.fixture -def app(): - app = Flask(__name__) - app.config["TESTING"] = True - - jwt_config = AuthCoreJwtConfig( - enable=True, - secret="mysecret", - expiresIn="2h", - refresh=True, - prefix="/auth/jwt", - ) - - auth.init_app( - app=app, - user_service=MockUserService(), - credential_checker=lambda input, user: input == user, - configurations=jwt_config, - ) - - @app.post("/auth/jwt/login") - def login(): - username = request.json.get("username") - password = request.json.get("password") - user = auth.authenticate(username, password) - if user: - tokens = auth.generate_tokens(user) - return jsonify(tokens), 200 - return jsonify({"message": "Invalid credentials"}), 401 - - @app.post("/auth/jwt/refresh") - def refresh(): - refresh_token = request.json.get("refresh_token") - new_token = auth.refresh_access_token(refresh_token) - if new_token: - return jsonify({"access_token": new_token}), 200 - return jsonify({"message": "Invalid refresh token"}), 401 - - @app.get("/me") - @auth.verify() - def protected(): - return jsonify({"message": "Access granted"}), 200 - - return app - - -@pytest.fixture -def client(app): - return app.test_client() - - -def test_jwt_token_generation(client): - response = client.post( - "/auth/jwt/login", json={"username": "testuser", "password": "password123"} - ) - assert response.status_code == 200 - assert "access_token" in response.json - assert "refresh_token" in response.json - - -def test_protected_route_access(client): - login_response = client.post( - "/auth/jwt/login", json={"username": "testuser", "password": "password123"} - ) - token = login_response.json["access_token"] - - response = client.get("/me", headers={"Authorization": f"Bearer {token}"}) - assert response.status_code == 200 - assert response.json["message"] == "Access granted" - - -def test_refresh_token(client): - login_response = client.post( - "/auth/jwt/login", json={"username": "testuser", "password": "password123"} - ) - refresh_token = login_response.json.get("refresh_token") - - refresh_response = client.post("/auth/jwt/refresh", json={"refresh_token": refresh_token}) - assert refresh_response.status_code == 200 - assert "access_token" in refresh_response.json diff --git a/flycatch_auth/tests/test_fastapi_integration.py b/flycatch_auth/tests/test_fastapi_integration.py deleted file mode 100644 index d9fbf05..0000000 --- a/flycatch_auth/tests/test_fastapi_integration.py +++ /dev/null @@ -1,38 +0,0 @@ -import pytest -from fastapi import FastAPI -from fastapi.testclient import TestClient -from services import Auth, AuthCoreJwtConfig, IdentityService - -class MockIdentityService(IdentityService): - def load_user(self, username: str): - if username == "test_user": - return {"id": "1", "username": "test_user", "password": "securepass", "grants": ["read_user"]} - return None - -def mock_credential_checker(input_password, stored_password): - return input_password == stored_password - -@pytest.fixture -def fastapi_app(): - app = FastAPI() - auth = Auth(MockIdentityService(), mock_credential_checker, AuthCoreJwtConfig( - enable=True, - secret="test-secret", - expiresIn="1h", - refresh=True, - prefix="/auth/jwt", - ),) - - @app.get("/protected") - async def protected_route(user=auth.verify): - return {"message": "Access granted", "user": user} - return app - -@pytest.fixture -def client(fastapi_app): - return TestClient(fastapi_app) - -def test_fastapi_protected_route(client): - response = client.get("/protected") - print(response.json) - assert response.status_code == 401 # Should be unauthorized without token From 405167b1ba96348cd02f4419312b86a73d12b0b1 Mon Sep 17 00:00:00 2001 From: ABINFLYCATCH Date: Tue, 11 Feb 2025 10:01:52 +0530 Subject: [PATCH 24/28] jwt route fixed and push --- README.md | 4 +- flycatch_auth/routes/jwt_routes.py | 62 +++----------------------- flycatch_auth/services/jwt_services.py | 58 +++++++++++++----------- hello.py | 43 ++++++++++++++++++ 4 files changed, 84 insertions(+), 83 deletions(-) create mode 100644 hello.py diff --git a/README.md b/README.md index 80c2f20..2bbd2db 100644 --- a/README.md +++ b/README.md @@ -25,12 +25,12 @@ pip install flycatch-auth ```python from flask import Flask, request, jsonify -from flycatch_auth import Auth, AuthCoreJwtConfig +from flycatch_auth import auth, AuthCoreJwtConfig app = Flask(__name__) class Userservice(IdentityService): - def load_user(username: str) -> Identity: + def load_user(self,username: str) -> Identity: user = user_db.get_users() return { "id": "", diff --git a/flycatch_auth/routes/jwt_routes.py b/flycatch_auth/routes/jwt_routes.py index 9fcff9b..9b7a8f4 100644 --- a/flycatch_auth/routes/jwt_routes.py +++ b/flycatch_auth/routes/jwt_routes.py @@ -1,7 +1,6 @@ from flask import Blueprint, request, jsonify -import jwt -import datetime import logging +from ..services import JwtAuthService logger = logging.getLogger(__name__) @@ -10,74 +9,27 @@ def create_jwt_routes(app, config, user_service, credential_checker): router = Blueprint("jwt_auth", __name__) prefix = config.prefix or "/auth/jwt" - def create_access_token(user): - """Generate JWT Access Token""" - payload = { - "id": user["id"], - "username": user["username"], - "type": "access", - "exp": datetime.datetime.utcnow() + datetime.timedelta(hours=8), - } - return jwt.encode(payload, config.secret, algorithm="HS256") - - def create_refresh_token(user): - """Generate JWT Refresh Token""" - payload = { - "id": user["id"], - "username": user["username"], - "type": "refresh", - "exp": datetime.datetime.utcnow() + datetime.timedelta(days=7), - } - return jwt.encode(payload, config.secret, algorithm="HS256") + jwt_auth = JwtAuthService(user_service, credential_checker, config) @router.route(f"{prefix}/login", methods=["POST"]) def login(): """Login route to generate access and refresh tokens""" data = request.json username, password = data.get("username"), data.get("password") - logger.info(f"Login attempt for username: {username}") - - user = user_service.load_user(username) - if not user or not credential_checker(password, user["password"]): - logger.warning(f"Login failed for username: {username}") - return jsonify({"error": "Invalid username or password"}), 401 - - access_token = create_access_token(user) - refresh_token = create_refresh_token(user) - logger.info(f"Login successful for username: {username}") - return jsonify({"access_token": access_token, "refresh_token": refresh_token}) + return jsonify(jwt_auth.login(username=username, password=password)) @router.route(f"{prefix}/refresh", methods=["POST"]) def refresh(): """Refresh access token using a valid refresh token""" auth_header = request.headers.get("Authorization") logger.info("Refresh token attempt received") + if not auth_header: logger.warning("Refresh token missing in request") return jsonify({"error": "Refresh token is required"}), 400 - try: - refresh_token = auth_header.split(" ")[1] - decoded_token = jwt.decode(refresh_token, config.secret, algorithms=["HS256"]) - - if decoded_token.get("type") != "refresh": - logger.warning("Invalid token type for refresh") - return jsonify({"error": "Invalid token type"}), 403 - - user = {"id": decoded_token["id"], "username": decoded_token["username"]} - access_token = create_access_token(user) - new_refresh_token = create_refresh_token(user) - - logger.info(f"Access token refreshed for username: {user['username']}") - return jsonify({"access_token": access_token, "refresh_token": new_refresh_token}) - except jwt.ExpiredSignatureError: - logger.warning("Refresh token expired") - return jsonify({"error": "Refresh token expired"}), 403 - except jwt.InvalidTokenError: - logger.warning("Invalid refresh token provided") - return jsonify({"error": "Invalid refresh token"}), 403 - except Exception as e: - logger.error("JWT Refresh Error", exc_info=True) - return jsonify({"error": "Internal Server Error"}), 500 + refresh_token = auth_header.split(" ")[1] + response = jwt_auth.refresh(refresh_token) + return jsonify(response), 200 if "access_token" in response else 403 app.register_blueprint(router) diff --git a/flycatch_auth/services/jwt_services.py b/flycatch_auth/services/jwt_services.py index 51a5481..c99ea16 100644 --- a/flycatch_auth/services/jwt_services.py +++ b/flycatch_auth/services/jwt_services.py @@ -6,6 +6,7 @@ from flycatch_auth.model_types import IdentityService from .base import AuthService + @dataclass class AuthCoreJwtConfig: enable: bool @@ -14,25 +15,6 @@ class AuthCoreJwtConfig: refresh: bool prefix: str - def generate_token(self, user_data: dict) -> str: - """Generate JWT access token""" - exp_hours = int(self.expiresIn.replace("h", "")) - payload = { - "sub": user_data["id"], - "exp": datetime.datetime.utcnow() + datetime.timedelta(hours=exp_hours), - "username": user_data["username"], - "grants": user_data["grants"], - } - return jwt.encode(payload, self.secret, algorithm="HS256") - - def generate_refresh_token(self, user_data: dict) -> str: - """Generate JWT refresh token""" - payload = { - "sub": user_data["id"], - "exp": datetime.datetime.utcnow() + datetime.timedelta(days=7), - } - return jwt.encode(payload, self.secret, algorithm="HS256") - def decode_token(self, token: str) -> Optional[dict]: """Decode JWT token and return user data""" try: @@ -49,7 +31,6 @@ def verify_refresh_token(self, token: str) -> bool: return self.decode_token(token) is not None - class JwtAuthService(AuthService): """JWT-based authentication service.""" @@ -65,19 +46,44 @@ def authenticate(self, username: str, password: str) -> Optional[Dict]: return user return None + def generate_token(self, user, token_type="access") -> str: + """Generate JWT access or refresh token.""" + expiration = ( + datetime.datetime.utcnow() + datetime.timedelta(hours=8) + if token_type == "access" + else datetime.datetime.utcnow() + datetime.timedelta(days=7) + ) + + payload = { + "id": user["id"], + "username": user["username"], + "type": token_type, + "exp": expiration, + } + return jwt.encode(payload, self.jwt_config.secret, algorithm="HS256") + def login(self, username: str, password: str) -> Dict: """Authenticate and return JWT access & refresh tokens.""" user = self.authenticate(username, password) if user: return { - "access_token": self.jwt_config.generate_token(user), - "refresh_token": self.jwt_config.generate_refresh_token(user), + "access_token": self.generate_token(user, "access"), + "refresh_token": self.generate_token(user, "refresh"), } return {"error": "Invalid credentials"} def refresh(self, refresh_token: str) -> Dict: """Refresh JWT access token.""" - user_data = self.jwt_config.decode_token(refresh_token) - if user_data: - return {"access_token": self.jwt_config.generate_token(user_data)} - return {"error": "Invalid refresh token"} + try: + decoded_token = jwt.decode( + refresh_token, self.jwt_config.secret, algorithms=["HS256"]) + if decoded_token.get("type") != "refresh": + return {"error": "Invalid token type"} + + user = {"id": decoded_token["id"], + "username": decoded_token["username"]} + return {"access_token": self.generate_token(user, "access")} + except jwt.ExpiredSignatureError: + return {"error": "Refresh token expired"} + except jwt.InvalidTokenError: + return {"error": "Invalid refresh token"} diff --git a/hello.py b/hello.py new file mode 100644 index 0000000..d52e1a1 --- /dev/null +++ b/hello.py @@ -0,0 +1,43 @@ +from flask import Flask, request, jsonify +from flycatch_auth import auth, AuthCoreJwtConfig, IdentityService, Identity + +app = Flask(__name__) + + +class Userservice(IdentityService): + def load_user(self, username: str) -> Identity: + # user = user_db.get_users() + return { + "id": "1", + "username": "testuser", + "password": "password123", + # read_user + # "grants": [list(map(lambda user: user.permission.name, users))] + "grants": ["read_user"] + } + + +jwt_config = AuthCoreJwtConfig( + enable=True, + secret="mysecret", + expiresIn="2h", + refresh=True, + prefix="/auth/jwt", +) + +auth.init_app( + app=app, + user_service=Userservice(), + credential_checker=lambda input, user: input == user, + jwt=jwt_config, +) + + +@app.route("/me") +# @auth.verify() +def get_curr_user(): + return jsonify({"message": "Access granted"}), 200 + + +if __name__ == "__main__": + app.run(debug=True) From 2f9a9c63961a0c6058315a96e72a9973dd7b2391 Mon Sep 17 00:00:00 2001 From: ABINFLYCATCH Date: Tue, 11 Feb 2025 12:39:36 +0530 Subject: [PATCH 25/28] readme file updated --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index df7e995..4cac839 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ __pycache__/ *.py[cod] dist/ +services/__pycache__/ +services/*.py[cod] +tests/__pycache__/ +tests/*.py[cod] +routes/__pycache__/ From d7f4009f5aa79f81c12279f055717cd4592b7f9a Mon Sep 17 00:00:00 2001 From: ABINFLYCATCH Date: Wed, 12 Feb 2025 10:53:23 +0530 Subject: [PATCH 26/28] redme file added --- tests/__init__.py | 0 tests/__pycache__/__init__.cpython-312.pyc | Bin 164 -> 0 bytes .../test_auth.cpython-312-pytest-7.4.4.pyc | Bin 8561 -> 0 bytes .../test_auth.cpython-312-pytest-8.3.4.pyc | Bin 8826 -> 0 bytes ...i_integration.cpython-312-pytest-7.4.4.pyc | Bin 3121 -> 0 bytes ...k_integration.cpython-312-pytest-7.4.4.pyc | Bin 4876 -> 0 bytes ...k_integration.cpython-312-pytest-8.3.4.pyc | Bin 5000 -> 0 bytes tests/test_auth.py | 99 ------------------ tests/test_fastapi_integration.py | 38 ------- tests/test_flask_integration.py | 56 ---------- 10 files changed, 193 deletions(-) delete mode 100644 tests/__init__.py delete mode 100644 tests/__pycache__/__init__.cpython-312.pyc delete mode 100644 tests/__pycache__/test_auth.cpython-312-pytest-7.4.4.pyc delete mode 100644 tests/__pycache__/test_auth.cpython-312-pytest-8.3.4.pyc delete mode 100644 tests/__pycache__/test_fastapi_integration.cpython-312-pytest-7.4.4.pyc delete mode 100644 tests/__pycache__/test_flask_integration.cpython-312-pytest-7.4.4.pyc delete mode 100644 tests/__pycache__/test_flask_integration.cpython-312-pytest-8.3.4.pyc delete mode 100644 tests/test_auth.py delete mode 100644 tests/test_fastapi_integration.py delete mode 100644 tests/test_flask_integration.py diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/__pycache__/__init__.cpython-312.pyc b/tests/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index bb6907639b12a56ec88caf00256483a921cf34d8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 164 zcmX@j%ge<81R`@5ri19mAOanHW&w&!XQ*V*Wb|9fP{ah}eFmxd<*c8PpPQ7bg9zQ^{GwFdw4B7^Z2gkd;*w(h`1s7c%#!$cy@JYL95%W6 cDWy57c15f}GZ}%n7{vI<%*e=C#0+Es03Wm|R{#J2 diff --git a/tests/__pycache__/test_auth.cpython-312-pytest-7.4.4.pyc b/tests/__pycache__/test_auth.cpython-312-pytest-7.4.4.pyc deleted file mode 100644 index 3f97ccf9b32e4e64319db18c8812d927c2ad8780..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8561 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 YP6On=mPGc2uUjQ{?^1h|?We2w7hLmke*gdg diff --git a/tests/__pycache__/test_auth.cpython-312-pytest-8.3.4.pyc b/tests/__pycache__/test_auth.cpython-312-pytest-8.3.4.pyc deleted file mode 100644 index dd5875099ae3a8ab1126010f18b17d3a943f23c1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8826 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 W{a%szAN;;o;ZJP!$M{J)i~j@^^3o&# diff --git a/tests/__pycache__/test_fastapi_integration.cpython-312-pytest-7.4.4.pyc b/tests/__pycache__/test_fastapi_integration.cpython-312-pytest-7.4.4.pyc deleted file mode 100644 index dc86c1b8c2eb95f5c8a6bf8b32bc0e36313e5476..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3121 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 U+OZHW2yYDFI8!siQ7)qH-?Va&L zytWitBtZS3l`4T$D&?nA5PVnrS*bsj`ePGlyjLrMRP|&3vP$~HSI@cQnGD&i*pJ;_ z*=No@_uO;OJ@=e5=gvQ4u?T@SvHq9YZjg|FW5;dK=WYG3@OVgcq6;OG75Gyu2lApo z%2GBcI3sdah7qw8Dl1u~9L|Qzk!*yI1xji;nvIrY*;qNAjh7SIgg``cp6JpYq6dxm z(}1&3w$G4@`yJd5b@>i?+9PE9zlA5{;~CJEJ0!bvK}dza!g({P01N)2l(XhpsAyFy zW?_MasPReNu`|OJX@v4g`4)-YNcS#vcyNaQL#;X;j%$LHS-2jTteQj zpVaHD?*-%`F}RVkqAnN#T{NVEqzCSRp%$c6kR>iv^Yd3NgL)$C(5d9g29v2_*J&j$ z`efr7pmo;!xoBO`MLnQPvw|LcEL|okd4>sNOtE1jD2J)FoMqjtQhh8l&cf8l=^D~u zlcK59C6Os~{WErIbXRvVnWb%!co~xFPbfL76&)NC8G)SHIdK#K6 zjo@e=Om?(@#@#ZCN?B=y8XwloifzD>wppzlt1Yl_soGPoF|x%f$Go8sC}Rx*SR~z{ zk)_7xt@bs&{VSuBD72NrIt;++%1qHTw4tW4h^CdRdcA~rRMS4G=SsbifMrt}h6!{8 z!4QJo2zDV*T_6KuA?XfT6NTWcuykOJ;B)!#27fw}kpC?&Ku!5kIpJap(C5we4)ke{ z)*`kWTo6?JNCjPb7JS;Hwgelc@m5q^Z1dS9HZISb8yFZQP2pMSY0n!4(j>*0%eDE` z6;}&Aycseru}Q$4A~RQ*QeLo(Joq9LGjokFKjd`rrVY+384c5JFt&F4ziDL@=D9-)O_P2KbM}g7M)`SpAWzU7Db5~{CNcT z!_Ih6=So_B&dAS0&;($2ZvyGE@z!~w9l6p;Kpv9K^TZP~1<#6sf(VwAW|)M2_9lD< z96os!*8A!oFfk=i6tu8w%+gek3oc?cpOU ziKEL_JCSKC8Sam%5Jl&sdk~=WQ1oM_m=)93Ksxtt7Npg>ZKMM92o6SI-&&rhIywOa z3ne6zNhctXh4Z1SSq1>mKqhM6AC=#w7temVV}Ca>&`peV`-i%HJG+V98>-wdFJ9Q_ zBf;?9OSdm|qlqsEf4nky=!-p5-RRJN!@;1u7AJD}?zP+3?$uXC^d25q`vF1kDf&l&hRqFw%Y=ewf!UeLGf%!`LJEgd!0lK#(b*o8 zoe}yoARWT71b{^XIy^Xi|MZHww=M1crY>HPC&9emri+`UABzyuz2I*NO`+JsVIi-z zL|twLnqo765z&&GlJ)n^~v|H&!+6kaG$?{I_H+q65%hw`3OaPU2JJZ49hJ|kGm3D zp=QYX%(w1#x+h$2y^rg@L+}6I&N1Z5e&;!QoN$i)J=-bY*>>Iw#a$0on_VOBKHq6qj}z=V?DK^GZ+z@>_0@m4_=s#5 zANmL$drQ@KLtayx63=kaX4ESAY_WZYi$aFmgL5`BoNvx{_Vcp9JNP8e8ham$JWIs% z_w;?uSkLd{-`VfINOvXna)t^yBifP{NNRMZk?`uLbg4RPRw!lxx&uH;9dwWBlOZ+CnB}MLz2x$>}6w$5YlZ%g#Z?4{vYXh_sT^&cH;@Ep zN@L#@d?ZaPSvD=ptjs10v|3KOYitn$>d_BiA1ugcvlw2is?==o%D)l%xsjs=P157e z#zS*(OM#k?DW1I88NPZF3N_QvX$BbSIOlHkqXEWJmb>K>DGQ26(0L5OiALPFo0HCN zPNrf^s#PtUsXnW*$Op9rULxcu+o3^CSpuWgXuevmfqpCs!`h9yS+dQF#bl@IWV4GRc}BY$!~9A&B_%FlmOBSRa(wL-Nh|iwq{xU3`gPSbB@v~Wdkk?RM4ed zgwmc3 zRYsPxfBSVOd3h~BhJLh8grTd#;>?;%qPthsgB|r?Tbf!P0MJ$sE)R6nBdgLB{9g75 zPS1lKY3kl7r?;|+zTnIb`8kU7_?Igl>LIY!Ygjxw+KG;?Mh|qN2bN}5qT`F_zEJyC zrOdyiOuIh=i2wIO%G{f92rHYY$MHI`Z|Nt?2bYK2$+31kvv|In7<|yU-*^P1dySRE z*y4w;)qz!Yw4;u;rOBmn0Bv=2X}qHzT9qc@_p(QDdLHaZllR_tdMlgg3(oA2pQAXB ze>vfy9s+B1)qw}o_op8Xb<|{AN-up3psgmCKJKXLRSADDdjzMq!g~(>9Qhs84_`=W z=T0)n6gPtCFVWpbG$-bcIWC7E0;qA5nzq5h9trP~oJMDGj_gcfLH-fJx#dVOK(XFr zG57m~<7xOdajriKV*Iv`_ZJp|J1Kl!G59^ei7xcYE%!3GY&stgPSJT7#p{A~9l%;Z z5QJCwN#M7!S0vRTsaNFU3c0u;i^8n%`|1Y4=URk_%H4gp_kAr63yJjr5&PBwCO9Wf;)9Ost$A6@iD;s5{u diff --git a/tests/test_auth.py b/tests/test_auth.py deleted file mode 100644 index a3dcd09..0000000 --- a/tests/test_auth.py +++ /dev/null @@ -1,99 +0,0 @@ -import pytest -from flask import Flask, request, jsonify -from flycatch_auth import auth, AuthCoreJwtConfig, IdentityService, Identity - - -class MockUserService(IdentityService): - def load_user(self, username: str)-> Identity: - return { - "id": "1", - "username": "testuser", - "password": "password123", - "grants": ["read_user"], - } - - -@pytest.fixture -def app(): - app = Flask(__name__) - app.config["TESTING"] = True - - jwt_config = AuthCoreJwtConfig( - enable=True, - secret="mysecret", - expiresIn="2h", - refresh=True, - prefix="/auth/jwt", - ) - - auth.init_app( - app=app, - user_service=MockUserService(), - credential_checker=lambda input, user: input == user, - jwt=jwt_config, - ) - - @app.post("/auth/jwt/login") - def login(): - username = request.json.get("username") - password = request.json.get("password") - user = auth.authenticate(username, password) - if user: - tokens = auth.generate_tokens(user) - return jsonify(tokens), 200 - return jsonify({"message": "Invalid credentials"}), 401 - - @app.post("/auth/jwt/refresh") - def refresh(): - refresh_token = request.json.get("refresh_token") - new_token = auth.refresh_access_token(refresh_token) - if new_token: - return jsonify({"access_token": new_token}), 200 - return jsonify({"message": "Invalid refresh token"}), 401 - - @app.get("/me") - # @auth.verify() - def protected(): - return jsonify({"message": "Access granted"}), 200 - - return app - - -@pytest.fixture -def client(app): - return app.test_client() - - -def test_jwt_token_generation(client): - response = client.post( - "/auth/jwt/login", json={"username": "testuser", "password": "password123"} - ) - assert response.status_code == 200 - assert "access_token" in response.json - assert "refresh_token" in response.json - - -def test_protected_route_access(client): - login_response = client.post( - "/auth/jwt/login", json={"username": "testuser", "password": "password123"} - ) - token = login_response.json["access_token"] - - response = client.get("/me", headers={"Authorization": f"Bearer {token}"}) - assert response.status_code == 200 - assert response.json["message"] == "Access granted" - - -def test_refresh_token(client): - login_response = client.post( - "/auth/jwt/login", json={"username": "testuser", "password": "password123"} - ) - refresh_token = login_response.json.get("refresh_token") - print(refresh_token) - - refresh_response = client.post( - "/auth/jwt/refresh", - headers={"Authorization": f"Bearer {refresh_token}"}, - ) - assert refresh_response.status_code == 200 - assert "access_token" in refresh_response.json diff --git a/tests/test_fastapi_integration.py b/tests/test_fastapi_integration.py deleted file mode 100644 index d2766f2..0000000 --- a/tests/test_fastapi_integration.py +++ /dev/null @@ -1,38 +0,0 @@ -import pytest -from fastapi import FastAPI -from fastapi.testclient import TestClient -from flycatch_auth import Auth, AuthCoreJwtConfig, IdentityService - -class MockIdentityService(IdentityService): - def load_user(self, username: str): - if username == "test_user": - return {"id": "1", "username": "test_user", "password": "securepass", "grants": ["read_user"]} - return None - -def mock_credential_checker(input_password, stored_password): - return input_password == stored_password - -@pytest.fixture -def fastapi_app(): - app = FastAPI() - auth = Auth(MockIdentityService(), mock_credential_checker, AuthCoreJwtConfig( - enable=True, - secret="test-secret", - expiresIn="1h", - refresh=True, - prefix="/auth/jwt", - ),) - - @app.get("/protected") - async def protected_route(user=auth.verify): - return {"message": "Access granted", "user": user} - return app - -@pytest.fixture -def client(fastapi_app): - return TestClient(fastapi_app) - -def test_fastapi_protected_route(client): - response = client.get("/protected") - print(response.json) - assert response.status_code == 401 # Should be unauthorized without token diff --git a/tests/test_flask_integration.py b/tests/test_flask_integration.py deleted file mode 100644 index 156af51..0000000 --- a/tests/test_flask_integration.py +++ /dev/null @@ -1,56 +0,0 @@ -import pytest -from flask import Flask, jsonify, request -from flycatch_auth import auth, AuthCoreJwtConfig, IdentityService, Identity - - -class MockUserService(IdentityService): - def load_user(self, username: str) -> Identity: - return {"id": "1", "username": "testuser", "password": "password123", "grants": ["read_user"]} - - -@pytest.fixture -def app(): - - app = Flask(__name__) - jwt_config = AuthCoreJwtConfig( - enable=True, - secret="mysecret", - expiresIn="2h", - refresh=True, - prefix="/auth/jwt", - ) - - auth.init_app( - app=app, - user_service=MockUserService(), - credential_checker=lambda input, user: input == user, - jwt=jwt_config, - ) - - @app.route("/me") - # @auth.verify() - # @auth.has_grants(["read_user"])["flask"] - def get_curr_user(): - return jsonify({"name": "Test User"}), 200 - return app - - -@pytest.fixture -def client(app): - return app.test_client() - - -def test_me_route_with_auth(client): - login_response = client.post( - "/auth/jwt/login", json={"username": "testuser", "password": "password123"}) - - assert login_response.status_code == 200, "Login failed" - - token = login_response.json.get("access_token") - assert token, "Token is missing from response" - - headers = {"Authorization": f"Bearer {token}"} - response = client.get("/me", headers=headers) - - assert response.status_code == 200, "Access denied" - assert response.json["name"] == "Test User" From b2ee955701bca2b7e6d7e782250bc226072a5bce Mon Sep 17 00:00:00 2001 From: ABINFLYCATCH Date: Wed, 12 Feb 2025 10:53:46 +0530 Subject: [PATCH 27/28] changes files --- README.md | 13 ++++ flycatch_auth/__init__.py | 4 +- flycatch_auth/auth.py | 5 ++ flycatch_auth/middleware.py | 41 +++++++++++++ tests/__init__.py | 0 tests/test_auth.py | 99 +++++++++++++++++++++++++++++++ tests/test_fastapi_integration.py | 38 ++++++++++++ tests/test_flask_integration.py | 56 +++++++++++++++++ 8 files changed, 254 insertions(+), 2 deletions(-) create mode 100644 flycatch_auth/middleware.py create mode 100644 tests/__init__.py create mode 100644 tests/test_auth.py create mode 100644 tests/test_fastapi_integration.py create mode 100644 tests/test_flask_integration.py diff --git a/README.md b/README.md index 2bbd2db..ee94ecd 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,19 @@ jwt_config = { "prefix": "/auth/jwt", # Prefix for JWT-related routes } ``` +### User Service + +```python +class UserService(IdentityService): + def load_user(self, username: str)-> Identity: + users = users.get_db() + return { + "id": "1", + "username": "testuser", + "password": "password123", + "grants": ["read_user"], + } +``` ### Password Checker diff --git a/flycatch_auth/__init__.py b/flycatch_auth/__init__.py index 1a4365f..12a7a5a 100644 --- a/flycatch_auth/__init__.py +++ b/flycatch_auth/__init__.py @@ -1,8 +1,8 @@ from .auth import AuthCore -from .model_types import Identity, IdentityService +from .model_types import Identity, IdentityService, CredentialChecker from .services import AuthCoreJwtConfig auth = AuthCore() # It enables top-level imports like `from flycatch_auth import auth, AuthCoreJwtConfig, JwtService` -__all__ = ["auth", "Identity", "IdentityService", "AuthCoreJwtConfig"] \ No newline at end of file +__all__ = ["auth", "Identity", "IdentityService", "AuthCoreJwtConfig","CredentialChecker"] \ No newline at end of file diff --git a/flycatch_auth/auth.py b/flycatch_auth/auth.py index 7f55522..36ecffb 100644 --- a/flycatch_auth/auth.py +++ b/flycatch_auth/auth.py @@ -4,6 +4,7 @@ from .model_types import IdentityService, CredentialChecker from .services import AuthCoreJwtConfig, JwtAuthService from .routes import create_jwt_routes +from .middleware import verify_request logger = logging.getLogger("auth_core") @@ -38,5 +39,9 @@ def init_app( return self.auth_service + def verify(self): + """Middleware to verify authentication.""" + return verify_request(self.auth_service) + auth = AuthCore() diff --git a/flycatch_auth/middleware.py b/flycatch_auth/middleware.py new file mode 100644 index 0000000..09f2526 --- /dev/null +++ b/flycatch_auth/middleware.py @@ -0,0 +1,41 @@ +import jwt +import logging +from functools import wraps +from flask import request, jsonify + +logger = logging.getLogger("auth_core") + + +def verify_request(auth_service): + """Middleware function to verify authentication.""" + + def decorator(f): + @wraps(f) + def wrapper(*args, **kwargs): + auth_header = request.headers.get("Authorization") + + if not auth_header: + logger.warning( + "Unauthorized access attempt: Missing Authorization header") + return jsonify({"error": "Unauthorized"}), 401 + if auth_service.jwt_config.enable: + try: + token = auth_header.split(" ")[1] # Extract the token + decoded_token = jwt.decode( + token, auth_service.jwt_config.secret, algorithms=["HS256"]) + + logger.info( + f"User {decoded_token['username']} authenticated successfully") + request.user = decoded_token # Attach user info to request + return f(*args, **kwargs) + + except jwt.ExpiredSignatureError: + logger.warning("Token expired") + return jsonify({"error": "Token expired"}), 403 + except jwt.InvalidTokenError: + logger.warning("Invalid token") + return jsonify({"error": "Invalid token"}), 403 + + return wrapper + + return decorator diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_auth.py b/tests/test_auth.py new file mode 100644 index 0000000..2d13141 --- /dev/null +++ b/tests/test_auth.py @@ -0,0 +1,99 @@ +import pytest +from flask import Flask, request, jsonify +from flycatch_auth import auth, AuthCoreJwtConfig, IdentityService, Identity + + +class MockUserService(IdentityService): + def load_user(self, username: str)-> Identity: + return { + "id": "1", + "username": "testuser", + "password": "password123", + "grants": ["read_user"], + } + + +@pytest.fixture +def app(): + app = Flask(__name__) + app.config["TESTING"] = True + + jwt_config = AuthCoreJwtConfig( + enable=True, + secret="mysecret", + expiresIn="2h", + refresh=True, + prefix="/auth/jwt", + ) + + auth.init_app( + app=app, + user_service=MockUserService(), + credential_checker=lambda input, user: input == user, + jwt=jwt_config, + ) + + @app.post("/auth/jwt/login") + def login(): + username = request.json.get("username") + password = request.json.get("password") + user = auth.authenticate(username, password) + if user: + tokens = auth.generate_tokens(user) + return jsonify(tokens), 200 + return jsonify({"message": "Invalid credentials"}), 401 + + @app.post("/auth/jwt/refresh") + def refresh(): + refresh_token = request.json.get("refresh_token") + new_token = auth.refresh_access_token(refresh_token) + if new_token: + return jsonify({"access_token": new_token}), 200 + return jsonify({"message": "Invalid refresh token"}), 401 + + @app.get("/me") + @auth.verify() + def protected(): + return jsonify({"message": "Access granted"}), 200 + + return app + + +@pytest.fixture +def client(app): + return app.test_client() + + +def test_jwt_token_generation(client): + response = client.post( + "/auth/jwt/login", json={"username": "testuser", "password": "password123"} + ) + assert response.status_code == 200 + assert "access_token" in response.json + assert "refresh_token" in response.json + + +def test_protected_route_access(client): + login_response = client.post( + "/auth/jwt/login", json={"username": "testuser", "password": "password123"} + ) + token = login_response.json["access_token"] + + response = client.get("/me", headers={"Authorization": f"Bearer {token}"}) + assert response.status_code == 200 + assert response.json["message"] == "Access granted" + + +def test_refresh_token(client): + login_response = client.post( + "/auth/jwt/login", json={"username": "testuser", "password": "password123"} + ) + refresh_token = login_response.json.get("refresh_token") + print(refresh_token) + + refresh_response = client.post( + "/auth/jwt/refresh", + headers={"Authorization": f"Bearer {refresh_token}"}, + ) + assert refresh_response.status_code == 200 + assert "access_token" in refresh_response.json diff --git a/tests/test_fastapi_integration.py b/tests/test_fastapi_integration.py new file mode 100644 index 0000000..d2766f2 --- /dev/null +++ b/tests/test_fastapi_integration.py @@ -0,0 +1,38 @@ +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient +from flycatch_auth import Auth, AuthCoreJwtConfig, IdentityService + +class MockIdentityService(IdentityService): + def load_user(self, username: str): + if username == "test_user": + return {"id": "1", "username": "test_user", "password": "securepass", "grants": ["read_user"]} + return None + +def mock_credential_checker(input_password, stored_password): + return input_password == stored_password + +@pytest.fixture +def fastapi_app(): + app = FastAPI() + auth = Auth(MockIdentityService(), mock_credential_checker, AuthCoreJwtConfig( + enable=True, + secret="test-secret", + expiresIn="1h", + refresh=True, + prefix="/auth/jwt", + ),) + + @app.get("/protected") + async def protected_route(user=auth.verify): + return {"message": "Access granted", "user": user} + return app + +@pytest.fixture +def client(fastapi_app): + return TestClient(fastapi_app) + +def test_fastapi_protected_route(client): + response = client.get("/protected") + print(response.json) + assert response.status_code == 401 # Should be unauthorized without token diff --git a/tests/test_flask_integration.py b/tests/test_flask_integration.py new file mode 100644 index 0000000..6087cab --- /dev/null +++ b/tests/test_flask_integration.py @@ -0,0 +1,56 @@ +import pytest +from flask import Flask, jsonify, request +from flycatch_auth import auth, AuthCoreJwtConfig, IdentityService, Identity + + +class MockUserService(IdentityService): + def load_user(self, username: str) -> Identity: + return {"id": "1", "username": "testuser", "password": "password123", "grants": ["read_user"]} + + +@pytest.fixture +def app(): + + app = Flask(__name__) + jwt_config = AuthCoreJwtConfig( + enable=True, + secret="mysecret", + expiresIn="2h", + refresh=True, + prefix="/auth/jwt", + ) + + auth.init_app( + app=app, + user_service=MockUserService(), + credential_checker=lambda input, user: input == user, + jwt=jwt_config, + ) + + @app.route("/me") + @auth.verify() + # @auth.has_grants(["read_user"])["flask"] + def get_curr_user(): + return jsonify({"name": "Test User"}), 200 + return app + + +@pytest.fixture +def client(app): + return app.test_client() + + +def test_me_route_with_auth(client): + login_response = client.post( + "/auth/jwt/login", json={"username": "testuser", "password": "password123"}) + + assert login_response.status_code == 200, "Login failed" + + token = login_response.json.get("access_token") + assert token, "Token is missing from response" + + headers = {"Authorization": f"Bearer {token}"} + response = client.get("/me", headers=headers) + + assert response.status_code == 200, "Access denied" + assert response.json["name"] == "Test User" From 6afda35dd85bfe8aae3a2b1a0f9b66caad7967ad Mon Sep 17 00:00:00 2001 From: ABINFLYCATCH Date: Wed, 12 Feb 2025 15:02:42 +0530 Subject: [PATCH 28/28] added response data and refactor files --- README.md | 3 +- flycatch_auth/auth.py | 8 +++- flycatch_auth/middleware.py | 12 +++--- flycatch_auth/model_types.py | 31 ++++++++++++++ flycatch_auth/routes/jwt_routes.py | 3 +- flycatch_auth/services/flask_auth.py | 49 --------------------- flycatch_auth/services/jwt_services.py | 59 +++++++++++--------------- tests/test_auth.py | 14 +++--- tests/test_flask_integration.py | 7 +-- 9 files changed, 83 insertions(+), 103 deletions(-) delete mode 100644 flycatch_auth/services/flask_auth.py diff --git a/README.md b/README.md index ee94ecd..caef8f5 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,8 @@ jwt_config = { ```python class UserService(IdentityService): def load_user(self, username: str)-> Identity: - users = users.get_db() + # users = users.get_db() + # your logic for implemention return { "id": "1", "username": "testuser", diff --git a/flycatch_auth/auth.py b/flycatch_auth/auth.py index 36ecffb..318220f 100644 --- a/flycatch_auth/auth.py +++ b/flycatch_auth/auth.py @@ -12,6 +12,7 @@ class AuthCore: def __init__(self): self.app = None self.jwt = None + self.session = None self.user_service = None self.credential_checker = None self.auth_service = None @@ -29,7 +30,7 @@ def init_app( self.user_service = user_service self.credential_checker = credential_checker - if jwt and jwt.enable: + if jwt and jwt.get("enable"): # Initialize JWT authentication service self.auth_service = JwtAuthService( user_service, credential_checker, jwt) @@ -37,6 +38,11 @@ def init_app( # Set up JWT authentication routes create_jwt_routes(app, jwt, user_service, credential_checker) + # if session and session.enable: + # # Initialize session-based authentication service + # self.auth_service = SessionAuthService( + # user_service, credential_checker, session) + return self.auth_service def verify(self): diff --git a/flycatch_auth/middleware.py b/flycatch_auth/middleware.py index 09f2526..f0fec84 100644 --- a/flycatch_auth/middleware.py +++ b/flycatch_auth/middleware.py @@ -2,7 +2,7 @@ import logging from functools import wraps from flask import request, jsonify - +from .model_types import api_response logger = logging.getLogger("auth_core") @@ -17,12 +17,12 @@ def wrapper(*args, **kwargs): if not auth_header: logger.warning( "Unauthorized access attempt: Missing Authorization header") - return jsonify({"error": "Unauthorized"}), 401 - if auth_service.jwt_config.enable: + return api_response(401, "Unauthorized access attempt",False) + if auth_service.jwt_config.get("enable"): try: token = auth_header.split(" ")[1] # Extract the token decoded_token = jwt.decode( - token, auth_service.jwt_config.secret, algorithms=["HS256"]) + token, auth_service.jwt_config.get('secret'), algorithms=["HS256"]) logger.info( f"User {decoded_token['username']} authenticated successfully") @@ -31,10 +31,10 @@ def wrapper(*args, **kwargs): except jwt.ExpiredSignatureError: logger.warning("Token expired") - return jsonify({"error": "Token expired"}), 403 + return api_response(403, "Token expired", False) except jwt.InvalidTokenError: logger.warning("Invalid token") - return jsonify({"error": "Invalid token"}), 403 + return api_response(401, "Invalid token", False) return wrapper diff --git a/flycatch_auth/model_types.py b/flycatch_auth/model_types.py index 62edc7c..b856f54 100644 --- a/flycatch_auth/model_types.py +++ b/flycatch_auth/model_types.py @@ -1,5 +1,6 @@ from abc import ABC, abstractmethod from typing import List, Optional, TypedDict +from datetime import datetime class Identity: @@ -27,6 +28,18 @@ class UserType(TypedDict): username: str password: str + +def api_response(code: int, message: str, status: str, data=None): + """Format API response.""" + return { + "code": code, + "timestamp": datetime.utcnow().isoformat() + "Z", + "message": message, + "status": status, + "data": data or {}, + } + + class CredentialChecker: def __init__(self): pass @@ -35,3 +48,21 @@ def verify(self, input_password: str, user_password: str) -> bool: """Checks if the provided password matches the stored password.""" return input_password == user_password + +class SessionConfig(TypedDict, total=False): + session_key: str + session_expiration: int + enabled: bool + prefix: str + secret: str + resave: bool + saveUninitialized: bool + cookie: dict + + +class AuthCoreJwtConfig(TypedDict, total=False): + enable: bool + secret: str + expiresIn: str + refresh: bool + prefix: str diff --git a/flycatch_auth/routes/jwt_routes.py b/flycatch_auth/routes/jwt_routes.py index 9b7a8f4..b78a2c0 100644 --- a/flycatch_auth/routes/jwt_routes.py +++ b/flycatch_auth/routes/jwt_routes.py @@ -2,12 +2,13 @@ import logging from ..services import JwtAuthService + logger = logging.getLogger(__name__) def create_jwt_routes(app, config, user_service, credential_checker): router = Blueprint("jwt_auth", __name__) - prefix = config.prefix or "/auth/jwt" + prefix = config.get("prefix") or "/auth/jwt" jwt_auth = JwtAuthService(user_service, credential_checker, config) diff --git a/flycatch_auth/services/flask_auth.py b/flycatch_auth/services/flask_auth.py deleted file mode 100644 index 5c81eb4..0000000 --- a/flycatch_auth/services/flask_auth.py +++ /dev/null @@ -1,49 +0,0 @@ -from flask import request, jsonify -from functools import wraps -from .flask_middleware import authenticate_request # Import the middleware function - -class FlaskAuth: - def __init__(self, user_service, credential_checker, jwt): - self.user_service = user_service - self.credential_checker = credential_checker - self.jwt = jwt - - def flask_verify(self, func): - """Decorator to verify JWT token in Flask""" - @wraps(func) - def wrapper(*args, **kwargs): - token = request.headers.get("Authorization") - if not token: - return jsonify({"error": "Token missing"}), 401 - - token = token.replace("Bearer ", "") - try: - # Decode the token and attach the user data to the request - user = self.jwt.decode_token(token) - if not user: - return jsonify({"error": "Invalid token"}), 401 - request.user = user # Attach user to the request - except Exception as e: - return jsonify({"error": str(e)}), 401 - - return func(*args, **kwargs) - return wrapper - - def flask_has_grants(self, required_grants): - """Flask decorator for grant-based access control""" - def decorator(func): - @wraps(func) - def wrapper(*args, **kwargs): - # Check if the user is available in request - if not hasattr(request, "user") or not request.user: - return jsonify({"error": "Unauthorized"}), 401 - - user_grants = request.user.get("grants", []) - print(user_grants) - # Check if the user has all the required grants - if not all(grant in user_grants for grant in required_grants): - return jsonify({"error": "Forbidden"}), 403 - - return func(*args, **kwargs) - return wrapper - return decorator diff --git a/flycatch_auth/services/jwt_services.py b/flycatch_auth/services/jwt_services.py index c99ea16..6c9083f 100644 --- a/flycatch_auth/services/jwt_services.py +++ b/flycatch_auth/services/jwt_services.py @@ -1,34 +1,8 @@ import jwt import datetime from typing import Optional, Dict -from dataclasses import dataclass from .base import AuthService -from flycatch_auth.model_types import IdentityService -from .base import AuthService - - -@dataclass -class AuthCoreJwtConfig: - enable: bool - secret: str - expiresIn: str - refresh: bool - prefix: str - - def decode_token(self, token: str) -> Optional[dict]: - """Decode JWT token and return user data""" - try: - return jwt.decode(token, self.secret, algorithms=["HS256"]) - except (jwt.ExpiredSignatureError, jwt.InvalidTokenError): - return None - - def verify_token(self, token: str) -> bool: - """Verify JWT token""" - return self.decode_token(token) is not None - - def verify_refresh_token(self, token: str) -> bool: - """Verify JWT refresh token""" - return self.decode_token(token) is not None +from flycatch_auth.model_types import IdentityService, AuthCoreJwtConfig, api_response class JwtAuthService(AuthService): @@ -60,30 +34,45 @@ def generate_token(self, user, token_type="access") -> str: "type": token_type, "exp": expiration, } - return jwt.encode(payload, self.jwt_config.secret, algorithm="HS256") + return jwt.encode(payload, self.jwt_config.get('secret'), algorithm="HS256") + + def decode_token(self, token: str) -> Optional[dict]: + """Decode JWT token and return user data""" + try: + return jwt.decode(token, self.secret, algorithms=["HS256"]) + except (jwt.ExpiredSignatureError, jwt.InvalidTokenError): + return None + + def verify_token(self, token: str) -> bool: + """Verify JWT token""" + return self.decode_token(token) is not None + + def verify_refresh_token(self, token: str) -> bool: + """Verify JWT refresh token""" + return self.decode_token(token) is not None def login(self, username: str, password: str) -> Dict: """Authenticate and return JWT access & refresh tokens.""" user = self.authenticate(username, password) if user: - return { + return api_response(200, "Login successful", True, { "access_token": self.generate_token(user, "access"), "refresh_token": self.generate_token(user, "refresh"), - } - return {"error": "Invalid credentials"} + }) + return api_response(401, "Invalid credentials", False) def refresh(self, refresh_token: str) -> Dict: """Refresh JWT access token.""" try: decoded_token = jwt.decode( - refresh_token, self.jwt_config.secret, algorithms=["HS256"]) + refresh_token, self.jwt_config.get('secret'), algorithms=["HS256"]) if decoded_token.get("type") != "refresh": return {"error": "Invalid token type"} user = {"id": decoded_token["id"], "username": decoded_token["username"]} - return {"access_token": self.generate_token(user, "access")} + return api_response(200, "Token refreshed", True, {"access_token": self.generate_token(user, "access"), "refresh_token": self.generate_token(user, "refresh")}) except jwt.ExpiredSignatureError: - return {"error": "Refresh token expired"} + return api_response(403, "Refresh token expired", False) except jwt.InvalidTokenError: - return {"error": "Invalid refresh token"} + return api_response(403, "Invalid refresh token", False) diff --git a/tests/test_auth.py b/tests/test_auth.py index 2d13141..3126821 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -4,7 +4,7 @@ class MockUserService(IdentityService): - def load_user(self, username: str)-> Identity: + def load_user(self, username: str) -> Identity: return { "id": "1", "username": "testuser", @@ -69,15 +69,15 @@ def test_jwt_token_generation(client): "/auth/jwt/login", json={"username": "testuser", "password": "password123"} ) assert response.status_code == 200 - assert "access_token" in response.json - assert "refresh_token" in response.json + assert "access_token" in response.json.get("data") + assert "refresh_token" in response.json.get("data") def test_protected_route_access(client): login_response = client.post( "/auth/jwt/login", json={"username": "testuser", "password": "password123"} ) - token = login_response.json["access_token"] + token = login_response.json["data"]["access_token"] response = client.get("/me", headers={"Authorization": f"Bearer {token}"}) assert response.status_code == 200 @@ -88,12 +88,12 @@ def test_refresh_token(client): login_response = client.post( "/auth/jwt/login", json={"username": "testuser", "password": "password123"} ) - refresh_token = login_response.json.get("refresh_token") + refresh_token = login_response.json["data"]["refresh_token"] print(refresh_token) refresh_response = client.post( "/auth/jwt/refresh", headers={"Authorization": f"Bearer {refresh_token}"}, ) - assert refresh_response.status_code == 200 - assert "access_token" in refresh_response.json + assert refresh_response.json['code'] == 200 + assert "access_token" in refresh_response.json['data'] diff --git a/tests/test_flask_integration.py b/tests/test_flask_integration.py index 6087cab..fbc682f 100644 --- a/tests/test_flask_integration.py +++ b/tests/test_flask_integration.py @@ -43,14 +43,15 @@ def client(app): def test_me_route_with_auth(client): login_response = client.post( "/auth/jwt/login", json={"username": "testuser", "password": "password123"}) + print(login_response.json) - assert login_response.status_code == 200, "Login failed" + assert login_response.status_code == 200, "Login successful" - token = login_response.json.get("access_token") + token = login_response.json['data']['access_token'] assert token, "Token is missing from response" headers = {"Authorization": f"Bearer {token}"} response = client.get("/me", headers=headers) - assert response.status_code == 200, "Access denied" + assert response.status_code == 200, "Access granted" assert response.json["name"] == "Test User"