Skip to content

Commit 8bb24a1

Browse files
committedDec 8, 2024
subido codigo documentado
1 parent 5be4860 commit 8bb24a1

File tree

5 files changed

+285
-77
lines changed

5 files changed

+285
-77
lines changed
 

‎app/fastf1.py

+30-29
Original file line numberDiff line numberDiff line change
@@ -5,52 +5,60 @@
55

66

77
class sesion():
8+
"""Clase que representa una sesión de F1 y permite cargar, filtrar y exportar datos."""
89

910
def __init__(self, year, circuit, session, drivers):
11+
"""Inicializa la sesión con el año, circuito, tipo de sesión y pilotos.
12+
13+
Args:
14+
year (int): Año de la sesión.
15+
circuit (str): Nombre del circuito.
16+
session (str): Tipo de sesión (FP1, FP2, FP3, Q, R).
17+
drivers (list): Lista de códigos de los pilotos.
18+
"""
1019
self.year: int = year
1120
self.circuit: str = circuit
1221
self.session: str = session
1322
self.drivers: list = drivers
14-
self.session_data: pd.DataFrame = None
15-
self.data_filtered_pilots: pd.DataFrame = None
23+
self.session_data: pd.DataFrame = None # Datos completos de la sesión
24+
self.data_filtered_pilots: pd.DataFrame = None # Datos filtrados por piloto
1625

1726
def __str__(self):
18-
return f'Cargando la sesion {self.session} del año {self.year}'
27+
"""Representación en cadena de la sesión."""
28+
return f'Cargando la sesión {self.session} del año {self.year}'
1929

2030
async def load_sesion(self):
21-
"""
22-
Carga la sesión especificicada por el usuario
23-
"""
31+
"""Carga la sesión especificada por el usuario utilizando la biblioteca fastf1."""
2432
carga_sesion = fastf1.get_session(
2533
self.year,
2634
self.circuit,
2735
self.session
2836
)
29-
carga_sesion.load()
37+
carga_sesion.load() # Carga los datos de la sesión
3038

3139
if carga_sesion.laps is not None:
32-
self.session_data = carga_sesion.laps.reset_index() # Si hay vueltas, las asignamos
40+
# Si hay vueltas registradas, las almacenamos en session_data
41+
self.session_data = carga_sesion.laps.reset_index()
3342
else:
3443
# Si no hay vueltas, asignamos un DataFrame vacío
3544
self.session_data = pd.DataFrame()
36-
print("Contenido de `SesionState.session_data`:", self.session_data)
45+
print("Contenido de `session_data`:", self.session_data)
3746

3847
async def filter_by_driver(self):
48+
"""Filtra las vueltas por los nombres de los pilotos especificados."""
3949
if self.session_data is not None and not self.session_data.empty:
40-
# Filtrar las vueltas por los nombres de los pilotos
50+
# Filtrar las vueltas por los pilotos
4151
self.data_filtered_pilots = self.session_data[self.session_data['Driver'].isin(
4252
self.drivers)]
43-
# Reseteo del índice
53+
# Resetear el índice
4454
self.data_filtered_pilots.reset_index(drop=True, inplace=True)
45-
print("Contenido de `SesionState.data_filtered_pilots`:",
55+
print("Contenido de `data_filtered_pilots`:",
4656
self.data_filtered_pilots)
4757
else:
4858
print("Error: Los datos de la sesión no están disponibles. Asegúrate de ejecutar `load_sesion` primero.")
4959

5060
async def _drop_tables(self):
51-
"""
52-
Elimina las columnas especificadas en `variables_to_change` del DataFrame `data_filtered_pilots`.
53-
"""
61+
"""Elimina columnas innecesarias del DataFrame `data_filtered_pilots`."""
5462
variables_to_drop = [
5563
'Sector1SessionTime',
5664
'Sector2SessionTime',
@@ -67,9 +75,7 @@ async def _drop_tables(self):
6775
print("Error: No hay datos filtrados disponibles para modificar. Asegúrate de ejecutar `filter_by_driver` primero.")
6876

6977
async def _change_units(self):
70-
"""
71-
Cambia las unidades de tiempo de milisegundos a minutos y segundos.
72-
"""
78+
"""Convierte las unidades de tiempo de milisegundos a minutos y segundos."""
7379
variables_to_change = [
7480
'Time', 'LapTime',
7581
'Sector1Time', 'Sector2Time', 'Sector3Time'
@@ -87,12 +93,11 @@ async def _change_units(self):
8793
print("Error: No hay datos filtrados disponibles para modificar. Asegúrate de ejecutar `filter_by_driver` primero.")
8894

8995
async def data_to_json(self):
90-
"""
91-
Convierte `data_filtered_pilots` a JSON y lo guarda en un archivo.
92-
"""
96+
"""Convierte `data_filtered_pilots` a JSON y lo guarda en un archivo."""
9397
if self.data_filtered_pilots is not None and not self.data_filtered_pilots.empty:
94-
# Eliminar los índices llamando a la función _drop_tables
98+
# Eliminar columnas innecesarias
9599
await self._drop_tables()
100+
# Cambiar las unidades de tiempo
96101
await self._change_units()
97102

98103
# Convertir a JSON con la estructura especificada
@@ -123,9 +128,7 @@ async def data_to_json(self):
123128

124129

125130
def get_user_input():
126-
"""
127-
Solicita al usuario que introduzca los detalles de la sesión.
128-
"""
131+
"""Solicita al usuario que introduzca los detalles de la sesión."""
129132
year = int(input("Introduce el año de la sesión: "))
130133
circuit = input("Introduce el circuito de la sesión: ")
131134
session = input("Introduce el tipo de sesión (FP1, FP2, FP3, Q, R): ")
@@ -136,14 +139,12 @@ def get_user_input():
136139

137140

138141
async def main():
139-
"""
140-
Función principal para cargar, filtrar y guardar datos de la sesión.
141-
"""
142+
"""Función principal para cargar, filtrar y guardar datos de la sesión."""
142143
year, circuit, session, drivers = get_user_input()
143144
f1 = sesion(year, circuit, session, drivers)
144145
await f1.load_sesion()
145146
await f1.filter_by_driver()
146147
await f1.data_to_json()
147148

148-
# Run the main function
149+
# Ejecutar la función principal
149150
asyncio.run(main())

‎app/main.py

+111-20
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@
1313
load_dotenv()
1414

1515
# Configuración de Supabase
16-
SUPABASE_URL = os.getenv("SUPABASE_URL")
17-
SUPABASE_KEY = os.getenv("SUPABASE_KEY")
16+
SUPABASE_URL = os.getenv("SUPABASE_URL") # URL de Supabase
17+
SUPABASE_KEY = os.getenv("SUPABASE_KEY") # Clave de la API de Supabase
18+
# Crear cliente de Supabase
1819
supabase = create_client(SUPABASE_URL, SUPABASE_KEY)
1920

2021
# Crear la aplicación FastAPI
@@ -31,7 +32,10 @@ def read_root():
3132
return {"message": "Bienvenido a la API de gestión de usuarios"}
3233

3334
# Clases Pydantic para ítems y usuarios
35+
36+
3437
class Description(BaseModel):
38+
"""Modelo para la descripción detallada de un ítem."""
3539
DriverNumber: str
3640
LapTime: Optional[str] = None
3741
Sector1Time: Optional[str] = None
@@ -44,25 +48,36 @@ class Description(BaseModel):
4448

4549

4650
class Item(BaseModel):
51+
"""Modelo para un ítem que incluye id, nombre y descripción."""
4752
id: int
4853
name: str
4954
description: Description
5055

5156

5257
class ItemCreate(BaseModel):
58+
"""Modelo para la creación de un nuevo ítem."""
5359
name: str
5460
description: Description
5561

5662

5763
class UserUpdate(BaseModel):
64+
"""Modelo para actualizar los datos de un usuario."""
5865
nick: Optional[str] = None
5966
name: Optional[str] = None
6067
surname: Optional[str] = None
6168
gender: Optional[str] = None
6269
email: Optional[str] = None
6370

6471
# Funciones auxiliares para manejo de JSON
72+
73+
6574
def read_data():
75+
"""
76+
Lee los datos desde un archivo JSON y los procesa.
77+
78+
Returns:
79+
list: Lista de ítems con id y nombre asignados si no existen.
80+
"""
6681
try:
6782
with open("app/data/data_filtered_pilots.json", "r", encoding="utf-8") as file:
6883
data = json.load(file)
@@ -75,10 +90,18 @@ def read_data():
7590
except FileNotFoundError:
7691
return []
7792

93+
7894
def write_data(data):
95+
"""
96+
Escribe los datos en un archivo JSON.
97+
98+
Args:
99+
data (list): Lista de ítems a escribir.
100+
"""
79101
with open("app/data/data_filtered_pilots.json", "w", encoding="utf-8") as file:
80102
json.dump(data, file, indent=4)
81103

104+
82105
@app.post("/register")
83106
def register_user(
84107
nick: str,
@@ -89,9 +112,21 @@ def register_user(
89112
password: str
90113
):
91114
"""
92-
endpoint registro usuarios
115+
Endpoint para registrar nuevos usuarios.
116+
117+
Args:
118+
nick (str): Apodo del usuario.
119+
name (str): Nombre del usuario.
120+
surname (str): Apellido del usuario.
121+
gender (str): Género del usuario.
122+
email (str): Correo electrónico del usuario.
123+
password (str): Contraseña del usuario.
124+
125+
Returns:
126+
dict: Mensaje de confirmación.
93127
"""
94-
hashed_password = get_password_hash(password)
128+
hashed_password = get_password_hash(
129+
password) # Generar hash de la contraseña
95130
response = supabase.table("users").insert({
96131
"nick": nick,
97132
"name": name,
@@ -101,16 +136,26 @@ def register_user(
101136
"password": hashed_password,
102137
}).execute()
103138

104-
print(response) # Depurar la estructura de la respuesta
139+
print(response) # Imprimir respuesta para depuración
105140
return {"message": "¡USUARIO CREADO EXITOSAMENTE!"}
106141

107142

108143
@app.post("/token", response_model=None)
109144
def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
110145
"""
111-
Crea token para la autenticación
146+
Crea un token de acceso para autenticación.
147+
148+
Args:
149+
form_data (OAuth2PasswordRequestForm): Datos del formulario de inicio de sesión.
150+
151+
Raises:
152+
HTTPException: Si las credenciales son inválidas.
153+
154+
Returns:
155+
dict: Token de acceso y tipo de token.
112156
"""
113-
response = supabase.table("users").select("*").eq("nick", form_data.username).execute()
157+
response = supabase.table("users").select(
158+
"*").eq("nick", form_data.username).execute()
114159
user = response.data[0] if response.data else None
115160
if not user or not verify_password(form_data.password, user["password"]):
116161
raise HTTPException(status_code=400, detail="Credenciales inválidas")
@@ -121,23 +166,40 @@ def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
121166

122167
@app.get("/users/me")
123168
def read_users_me(current_user: str = Depends(get_current_user)):
169+
"""
170+
Obtiene los datos del usuario actual autenticado.
171+
172+
Args:
173+
current_user (str): Correo electrónico del usuario actual.
124174
125-
response = supabase.table("users").select("*").eq("email", current_user).execute()
175+
Raises:
176+
HTTPException: Si el usuario no es encontrado.
126177
178+
Returns:
179+
dict: Datos del usuario actual.
180+
"""
181+
response = supabase.table("users").select(
182+
"*").eq("email", current_user).execute()
127183
if not response.data:
128184
raise HTTPException(status_code=404, detail="Usuario no encontrado")
129185
return response.data[0]
130186

187+
# Operaciones relacionadas con usuarios desde Supabase
131188

132189

133-
##################################################################################################
134-
135-
136-
# Operaciones relacionadas con usuarios desde Supabase
137190
@app.get("/users/supabase", tags=["Usuarios"])
138191
async def get_users_from_supabase(current_user: str = Depends(get_current_user)):
139192
"""
140-
Devuelve todos los usuarios de la base de datos
193+
Devuelve todos los usuarios almacenados en Supabase.
194+
195+
Args:
196+
current_user (str): Correo electrónico del usuario actual.
197+
198+
Raises:
199+
HTTPException: Si ocurre un error al obtener los datos.
200+
201+
Returns:
202+
dict: Mensaje y lista de usuarios.
141203
"""
142204
try:
143205
supabase_client = SupabaseAPI("users", "*")
@@ -146,28 +208,57 @@ async def get_users_from_supabase(current_user: str = Depends(get_current_user))
146208
return {"message": "No se encontraron usuarios", "data": []}
147209
return {"message": "Usuarios obtenidos exitosamente", "data": users}
148210
except Exception as e:
149-
raise HTTPException(status_code=500, detail=f"Error al obtener datos de Supabase: {str(e)}")
211+
raise HTTPException(
212+
status_code=500, detail=f"Error al obtener datos de Supabase: {str(e)}")
150213

151-
# TODO cambiar filtro a por nick
152214

153215
@app.put("/users/{email}", tags=["Usuarios"])
154216
async def update_user(email: str, user_update: UserUpdate, current_user: str = Depends(get_current_user)):
217+
"""
218+
Actualiza un usuario existente.
219+
220+
Args:
221+
email (str): Correo electrónico del usuario a actualizar.
222+
user_update (UserUpdate): Datos a actualizar.
223+
current_user (str): Correo electrónico del usuario actual.
224+
225+
Raises:
226+
HTTPException: Si no se proporcionan datos o si ocurre un error.
227+
228+
Returns:
229+
dict: Mensaje y datos actualizados.
230+
"""
155231
try:
156-
update_data = {k: v for k, v in user_update.dict().items() if v is not None}
232+
update_data = {k: v for k, v in user_update.dict().items()
233+
if v is not None}
157234
if not update_data:
158-
raise HTTPException(status_code=400, detail="No se proporcionaron datos para actualizar")
159-
supabase_client = SupabaseAPI(tabla="users", select="*", data=update_data)
235+
raise HTTPException(
236+
status_code=400, detail="No se proporcionaron datos para actualizar")
237+
supabase_client = SupabaseAPI(
238+
tabla="users", select="*", data=update_data)
160239
response = supabase_client.update_user(email, update_data)
161240
return {"message": "Usuario actualizado exitosamente", "data": response.data}
162241
except ValueError as ve:
163242
raise HTTPException(status_code=404, detail=str(ve))
164243
except Exception as e:
165-
raise HTTPException(status_code=500, detail=f"Error actualizando usuario: {str(e)}")
244+
raise HTTPException(
245+
status_code=500, detail=f"Error actualizando usuario: {str(e)}")
246+
166247

167248
@app.delete("/users/{nick}", tags=["Usuarios"])
168249
async def delete_user(nick: str, current_user: str = Depends(get_current_user)):
169250
"""
170-
Borra usuario solo si está autenticado
251+
Elimina un usuario especificado por su nick.
252+
253+
Args:
254+
nick (str): Apodo del usuario a eliminar.
255+
current_user (str): Correo electrónico del usuario actual.
256+
257+
Raises:
258+
HTTPException: Si ocurre un error durante la eliminación.
259+
260+
Returns:
261+
dict: Mensaje de confirmación y datos de la operación.
171262
"""
172263
try:
173264
supabase_client = SupabaseAPI(tabla="users", select="*")

‎app/routes/oauth.py

+58-8
Original file line numberDiff line numberDiff line change
@@ -7,39 +7,89 @@
77
import os
88
from dotenv import load_dotenv
99

10+
# Cargar variables de entorno desde un archivo .env
1011
load_dotenv()
1112

1213
# Configuración de JWT
13-
SECRET_KEY = os.getenv("SECRET_KEY")
14-
ALGORITHM = os.getenv("ALGORITHM")
14+
SECRET_KEY = os.getenv("SECRET_KEY") # Clave secreta para firmar el JWT
15+
ALGORITHM = os.getenv("ALGORITHM") # Algoritmo utilizado para firmar el JWT
16+
# Tiempo de expiración del token
1517
ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES"))
1618

1719
# Configuración de OAuth2
1820
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
1921
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
2022

2123
# Métodos auxiliares
24+
25+
2226
def verify_password(plain_password, hashed_password):
27+
"""Verifica si la contraseña en texto plano coincide con la contraseña encriptada.
28+
29+
Args:
30+
plain_password (str): Contraseña proporcionada por el usuario.
31+
hashed_password (str): Contraseña almacenada en la base de datos.
32+
33+
Returns:
34+
bool: True si las contraseñas coinciden, False en caso contrario.
35+
"""
2336
return pwd_context.verify(plain_password, hashed_password)
2437

38+
2539
def get_password_hash(password):
40+
"""Genera el hash de una contraseña.
41+
42+
Args:
43+
password (str): Contraseña en texto plano.
44+
45+
Returns:
46+
str: Hash de la contraseña.
47+
"""
2648
return pwd_context.hash(password)
2749

50+
2851
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
52+
"""Crea un token de acceso JWT.
53+
54+
Args:
55+
data (dict): Información a incluir en el token.
56+
expires_delta (timedelta, optional): Tiempo hasta la expiración del token. Por defecto es 15 minutos.
57+
58+
Returns:
59+
str: Token JWT codificado.
60+
"""
2961
to_encode = data.copy()
30-
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15))
31-
to_encode.update({"exp": expire})
32-
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
62+
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15)
63+
) # Calcula la fecha de expiración
64+
to_encode.update({"exp": expire}) # Agrega la fecha de expiración al token
65+
encoded_jwt = jwt.encode(to_encode, SECRET_KEY,
66+
algorithm=ALGORITHM) # Codifica el token
3367
return encoded_jwt
3468

3569
# Dependencia para obtener al usuario actual
70+
71+
3672
def get_current_user(token: str = Depends(oauth2_scheme)):
73+
"""Obtiene el usuario actual a partir del token proporcionado.
74+
75+
Args:
76+
token (str): Token de acceso JWT.
77+
78+
Raises:
79+
HTTPException: Si el token es inválido o ha expirado.
80+
81+
Returns:
82+
str: Nombre de usuario extraído del token.
83+
"""
3784
try:
38-
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
85+
payload = jwt.decode(token, SECRET_KEY, algorithms=[
86+
ALGORITHM]) # Decodifica el token
87+
# Obtiene el nombre de usuario del token
3988
username: str = payload.get("sub")
4089
if username is None:
4190
raise HTTPException(status_code=401, detail="Token inválido")
4291
return username
4392
except JWTError:
44-
raise HTTPException(status_code=401, detail="Token inválido o expirado")
45-
93+
raise HTTPException(
94+
status_code=401, detail="Token inválido o expirado"
95+
)

‎app/supabase_data.py

+51-15
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,15 @@
66
class SupabaseAPI():
77
def __init__(self, tabla, select, data=None):
88
"""
9-
Si no recibe datos para hacer un post data debe estar en None
9+
Inicializa la instancia de SupabaseAPI.
10+
11+
Args:
12+
tabla (str): Nombre de la tabla en Supabase.
13+
select (str): Campos a seleccionar en las consultas.
14+
data (dict, optional): Datos a insertar o actualizar. Por defecto es None.
15+
16+
Nota:
17+
Si no se reciben datos para una operación de inserción o actualización, 'data' debe estar en None.
1018
"""
1119
# Cargar las variables de entorno desde el archivo .env
1220
load_dotenv()
@@ -19,41 +27,57 @@ def __init__(self, tabla, select, data=None):
1927

2028
# Verificar si las variables de entorno están definidas
2129
if not SUPABASE_URL or not SUPABASE_KEY:
22-
raise ValueError("SUPABASE_URL and SUPABASE_KEY must be set")
30+
raise ValueError(
31+
"SUPABASE_URL y SUPABASE_KEY deben estar configuradas")
2332

2433
# Crear el cliente de Supabase utilizando la URL y la clave
2534
self.supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
2635

27-
self.tabla = tabla
28-
self.select = select
29-
self.data = data
36+
self.tabla = tabla # Nombre de la tabla a interactuar
37+
self.select = select # Campos a seleccionar en las consultas
38+
self.data = data # Datos para operaciones de inserción o actualización
3039

3140
def fetch_data(self):
41+
"""
42+
Obtiene datos de la tabla especificada.
43+
44+
Returns:
45+
Response: Respuesta de Supabase con los datos obtenidos.
46+
"""
3247
return self.supabase.table(self.tabla).select(self.select).execute()
3348

3449
def post_data(self):
50+
"""
51+
Inserta datos en la tabla especificada.
52+
53+
Returns:
54+
Response: Respuesta de Supabase después de la inserción.
55+
"""
3556
response = (
3657
self.supabase.table(self.tabla)
3758
.insert(self.data)
3859
.execute()
3960
)
4061
return response
4162

42-
# TODO:
43-
# Crear metodos update y delete para users
4463
def update_user(self, email: str, updated_data: dict):
4564
"""
46-
Actualiza la información de un usuario basándose en su email.
65+
Actualiza la información de un usuario basado en su email.
66+
4767
Args:
48-
email (str): Email del usuario a actualizar
49-
updated_data (dict): Datos a actualizar
68+
email (str): Email del usuario a actualizar.
69+
updated_data (dict): Diccionario con los datos a actualizar.
70+
5071
Returns:
51-
Response: Respuesta de Supabase con los datos actualizados
72+
Response: Respuesta de Supabase con los datos actualizados.
73+
74+
Raises:
75+
ValueError: Si no se encuentra el usuario o ocurre un error en la actualización.
5276
"""
5377
try:
5478
print(f"Iniciando actualización para usuario con email: {email}")
5579

56-
# Verificar usuario existente
80+
# Verificar si el usuario existe
5781
check_user = (
5882
self.supabase.table(self.tabla)
5983
.select("*")
@@ -68,19 +92,19 @@ def update_user(self, email: str, updated_data: dict):
6892
print(f"Usuario encontrado: {usuario_actual}")
6993
print(f"Aplicando actualización: {updated_data}")
7094

71-
# Combinar datos actuales con nuevos datos
95+
# Combinar datos actuales con los nuevos datos
7296
datos_actualizados = {**usuario_actual}
7397
datos_actualizados.update(updated_data)
7498

75-
# Realizar actualización usando datos combinados
99+
# Realizar la actualización usando los datos combinados
76100
response = (
77101
self.supabase.table(self.tabla)
78102
.update(datos_actualizados)
79103
.eq("email", email)
80104
.execute()
81105
)
82106

83-
# Verificar actualización
107+
# Verificar si la actualización fue exitosa
84108
if not response or not response.data:
85109
verify = (
86110
self.supabase.table(self.tabla)
@@ -100,6 +124,18 @@ def update_user(self, email: str, updated_data: dict):
100124
raise ValueError(f"Error actualizando usuario: {str(e)}")
101125

102126
def delete_user(self, nick: str):
127+
"""
128+
Elimina un usuario basado en su nick.
129+
130+
Args:
131+
nick (str): Nick del usuario a eliminar.
132+
133+
Returns:
134+
Response: Respuesta de Supabase después de la eliminación.
135+
136+
Raises:
137+
ValueError: Si no se encuentra el usuario o ocurre un error en la eliminación.
138+
"""
103139
try:
104140
response = (
105141
self.supabase.table('users')

‎requirements.txt

+35-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,35 @@
1-
fastapi
2-
uvicorn
3-
pydantic
4-
supabase-py
5-
python-dotenv
1+
annotated-types==0.7.0
2+
anyio==4.6.2.post1
3+
bcrypt==4.2.1
4+
certifi==2024.8.30
5+
cffi==1.17.1
6+
charset-normalizer==3.4.0
7+
click==8.1.7
8+
cryptography==44.0.0
9+
defusedxml==0.8.0rc2
10+
ecdsa==0.19.0
11+
exceptiongroup==1.2.2
12+
fastapi==0.115.5
13+
h11==0.14.0
14+
idna==3.10
15+
oauthlib==3.2.2
16+
passlib==1.7.4
17+
pyasn1==0.6.1
18+
pycparser==2.22
19+
pydantic==2.10.2
20+
pydantic_core==2.27.1
21+
PyJWT==2.10.1
22+
python-jose==3.3.0
23+
python-multipart==0.0.17
24+
python3-openid==3.2.0
25+
requests==2.32.3
26+
requests-oauthlib==2.0.0
27+
rsa==4.9
28+
six==1.16.0
29+
sniffio==1.3.1
30+
social-auth-core==4.5.4
31+
SQLAlchemy==2.0.36
32+
starlette==0.41.3
33+
typing_extensions==4.12.2
34+
urllib3==2.2.3
35+
uvicorn==0.32.1

0 commit comments

Comments
 (0)
Please sign in to comment.