Commit 667b5b56 authored by Patrick Ade's avatar Patrick Ade
Browse files

Implement user authentication and sensor data APIs

- Added JWT authentication with login and registration endpoints.
- Created sensor data retrieval endpoints for room and sensor availability.
- Integrated InfluxDB for data handling and added necessary routes.
- Updated project dependencies to include django-ninja and pyjwt.
parent 819d86b6
No related merge requests found
Showing with 421 additions and 36 deletions
+421 -36
from ninja import NinjaAPI
from .routes.user import user_router
from app.routes.sensor import sensor_router
api = NinjaAPI()
api.add_router("/api", user_router)
api.add_router("/api", sensor_router)
from ninja.security import HttpBearer
from django.contrib.auth.models import User
from .auth_utils import decode_jwt_token
class JWTAuth(HttpBearer):
def authenticate(self, request, token):
payload = decode_jwt_token(token)
if not payload:
return None
try:
return User.objects.get(id=payload["user_id"])
except User.DoesNotExist:
return None
import jwt
import datetime
from django.conf import settings
def create_jwt_token(user_id: int):
payload = {
"user_id": user_id,
"exp": datetime.datetime.utcnow() + datetime.timedelta(seconds=settings.JWT_EXP_DELTA_SECONDS),
"iat": datetime.datetime.utcnow(),
}
token = jwt.encode(payload, settings.JWT_SECRET, algorithm=settings.JWT_ALGORITHM)
return token
def decode_jwt_token(token: str):
try:
payload = jwt.decode(token, settings.JWT_SECRET, algorithms=[settings.JWT_ALGORITHM])
return payload
except jwt.ExpiredSignatureError:
return None
except jwt.InvalidTokenError:
return None
import csv
from io import StringIO
from ninja import Router
from django.http import JsonResponse
from utils.influx import InfluxDBHelper
from dotenv import load_dotenv
import os
sensor_router = Router(tags=["sensor"])
@sensor_router.get("/room_data_range")
def room_data_range(request, room: str, start: str = "-30d", stop: str = "now()"):
if not room:
return JsonResponse({"error": "Missing 'room' parameter"}, status=400)
load_dotenv()
client = InfluxDBHelper(
url=os.getenv("INFLUXDB_URL"),
token=os.getenv("INFLUXDB_TOKEN"),
org=os.getenv("INFLUXDB_ORG"),
bucket=os.getenv("INFLUXDB_BUCKET"),
)
tables = client.get_room_data_in_range(room, start, stop)
data_by_time = {}
for table in tables:
for record in table.records:
timestamp = str(record.get_time())
field = record.get_field()
value = record.get_value()
if timestamp not in data_by_time:
data_by_time[timestamp] = {}
data_by_time[timestamp][field] = value
return {"room": room, "data": data_by_time}
@sensor_router.get("/get_rooms")
def get_rooms(request):
client = InfluxDBHelper(
url=os.getenv("INFLUXDB_URL"),
token=os.getenv("INFLUXDB_TOKEN"),
org=os.getenv("INFLUXDB_ORG"),
bucket=os.getenv("INFLUXDB_BUCKET"),
)
tables = client.list_rooms()
rooms = set()
for table in tables:
for record in table.records:
rooms.add(record.get_value())
return {"rooms": sorted(rooms)}
@sensor_router.get("/room_data_csv")
def room_data_csv_view(request, room: str, start: str = "-30d", stop: str = "now()"):
if not room:
return JsonResponse({"error": "Missing 'room' parameter"}, status=400)
client = InfluxDBHelper(
url=os.getenv("INFLUXDB_URL"),
token=os.getenv("INFLUXDB_TOKEN"),
org=os.getenv("INFLUXDB_ORG"),
bucket=os.getenv("INFLUXDB_BUCKET"),
)
csv_stream = client.get_room_data_csv(room_id=room, start=start, stop=stop)
decoded = csv_stream.decode("utf-8")
results = []
for row in csv.DictReader(StringIO(decoded)):
if row.get("_field") and row.get("_value"):
results.append(
{
"time": row["_time"],
"field": row["_field"],
"value": row["_value"],
}
)
return {"room": room, "data": results}
@sensor_router.get("/room_data_csv_download")
def room_data_csv_download(request, room: str, start: str = "-30d", stop: str = "now()"):
if not room:
return JsonResponse({"error": "Missing 'room' parameter"}, status=400)
client = InfluxDBHelper(
url=os.getenv("INFLUXDB_URL"),
token=os.getenv("INFLUXDB_TOKEN"),
org=os.getenv("INFLUXDB_ORG"),
bucket=os.getenv("INFLUXDB_BUCKET"),
)
csv_data = client.get_room_data_csv(room, start, stop)
response = JsonResponse(csv_data, content_type="text/csv")
response["Content-Disposition"] = f'attachment; filename="{room}_data.csv"'
return response
@sensor_router.get("/get_rooms_from_building")
def get_rooms_from_building(request, building: str):
if not building:
return JsonResponse({"error": "Missing 'building' parameter"}, status=400)
client = InfluxDBHelper(
url=os.getenv("INFLUXDB_URL"),
token=os.getenv("INFLUXDB_TOKEN"),
org=os.getenv("INFLUXDB_ORG"),
bucket=os.getenv("INFLUXDB_BUCKET"),
)
tables = client.list_rooms()
rooms = set()
for table in tables:
for record in table.records:
room = record.get_value()
if isinstance(room, str) and room.startswith(f"{building}/"):
rooms.add(room)
return {"building": building, "rooms": sorted(rooms)}
@sensor_router.get("/get_bau")
def get_bau(request):
client = InfluxDBHelper(
url=os.getenv("INFLUXDB_URL"),
token=os.getenv("INFLUXDB_TOKEN"),
org=os.getenv("INFLUXDB_ORG"),
bucket=os.getenv("INFLUXDB_BUCKET"),
)
tables = client.list_rooms()
baus = set()
for table in tables:
for record in table.records:
room = record.get_value()
if isinstance(room, str) and "/" in room:
bau = room.split("/")[0]
baus.add(bau)
return {"baus": sorted(baus)}
@sensor_router.get("/room_availability")
def room_availability(request, room: str, start: str = "-30d", stop: str = "now()"):
if not room:
return JsonResponse({"error": "Missing 'room' parameter"}, status=400)
client = InfluxDBHelper(
url=os.getenv("INFLUXDB_URL"),
token=os.getenv("INFLUXDB_TOKEN"),
org=os.getenv("INFLUXDB_ORG"),
bucket=os.getenv("INFLUXDB_BUCKET"),
)
availability = client.has_room_sent_data(room, start, stop)
return {"room": room, "available": availability}
@sensor_router.get("/sensor_availability")
def sensor_availability(request, mac: str, start: str = "-30d", stop: str = "now()"):
if not mac:
return JsonResponse({"error": "Missing 'mac' parameter"}, status=400)
client = InfluxDBHelper(
url=os.getenv("INFLUXDB_URL"),
token=os.getenv("INFLUXDB_TOKEN"),
org=os.getenv("INFLUXDB_ORG"),
bucket=os.getenv("INFLUXDB_BUCKET"),
)
availability = client.has_sensor_sent_data(mac, start, stop)
return {"sensor_mac": mac, "available": availability}
@sensor_router.get("/all_rooms_availability")
def all_rooms_availability(request, start: str = "-30d", stop: str = "now()"):
client = InfluxDBHelper(
url=os.getenv("INFLUXDB_URL"),
token=os.getenv("INFLUXDB_TOKEN"),
org=os.getenv("INFLUXDB_ORG"),
bucket=os.getenv("INFLUXDB_BUCKET"),
)
rooms_available = client.rooms_with_available_data(start, stop)
return {"available_rooms": rooms_available}
@sensor_router.get("/all_sensors_availability")
def all_sensors_availability(request, start: str = "-30d", stop: str = "now()"):
client = InfluxDBHelper(
url=os.getenv("INFLUXDB_URL"),
token=os.getenv("INFLUXDB_TOKEN"),
org=os.getenv("INFLUXDB_ORG"),
bucket=os.getenv("INFLUXDB_BUCKET"),
)
sensors_available = client.sensors_with_available_data(start, stop)
return {"available_sensors": sensors_available}
from django.contrib.auth import authenticate
from django.contrib.auth.models import User
from ninja import Router
from pydantic import BaseModel
from ninja.errors import HttpError
from app.auth_utils import create_jwt_token
user_router = Router(tags=["user"])
class LoginSchema(BaseModel):
username: str
password: str
class TokenOut(BaseModel):
token: str
@user_router.post("/login", response=TokenOut)
def login_view(request, data: LoginSchema):
user = authenticate(username=data.username, password=data.password)
if user is None:
raise HttpError(401, "Invalid credentials")
token = create_jwt_token(user.id)
return {"token": token}
@user_router.get("/user")
def user_view(request):
if request.user.is_authenticated:
return {
"username": request.user.username,
"email": request.user.email,
}
raise HttpError(401, "Not logged in")
@user_router.post("/register")
def register_view(request, data: LoginSchema):
if User.objects.filter(username=data.username).exists():
raise HttpError(400, "Username already exists")
user = User.objects.create_user(username=data.username, password=data.password)
token = create_jwt_token(user.id)
return {"token": token}
from django.urls import path
from . import views
urlpatterns = [
path("api/set_csrf_token/", views.set_csrf_token, name="set_csrf_token"),
path("api/login", views.login_view, name="login"),
......@@ -9,10 +10,10 @@ urlpatterns = [
path("api/register", views.register, name="register"),
path("api/room_data_range", views.room_data_range, name="room_data_range"),
path("api/get_rooms", views.get_rooms, name="get_rooms"),
path("api/room_data_csv", views.room_data_csv_view, name="room_data_csv_view"),
path("api/room_data_csv_download", views.room_data_csv_download, name="room_data_csv_download"),
path("api/get_rooms_from_building", views.get_rooms_from_building, name="get_rooms_from_building"),
path("api/get_bau", views.get_bau, name="get_bau"),
path("api/room_data_csv", views.room_data_csv_view, name="room_data_csv_view"),
path("api/room_data_csv_download", views.room_data_csv_download, name="room_data_csv_download"),
path("api/get_rooms_from_building", views.get_rooms_from_building, name="get_rooms_from_building"),
path("api/get_bau", views.get_bau, name="get_bau"),
path("api/room_availability", views.room_availability, name="room_availability"),
path("api/sensor_availability", views.sensor_availability, name="sensor_availability"),
path("api/all_rooms_availability", views.all_rooms_availability, name="all_rooms_availability"),
......
"""
Django settings for core project.
Generated by 'django-admin startproject' using Django 5.2.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.2/ref/settings/
"""
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
......@@ -20,9 +8,7 @@ BASE_DIR = Path(__file__).resolve().parent.parent
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = (
"django-insecure-_nz-bhos!d#67%ki&#=$5g&=cjj0(nt3j&!^9loqyvr^lm-va&"
)
SECRET_KEY = "django-insecure-_nz-bhos!d#67%ki&#=$5g&=cjj0(nt3j&!^9loqyvr^lm-va&"
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
......@@ -142,3 +128,8 @@ STATIC_URL = "static/"
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# JWT settings
JWT_SECRET = "your-very-secret-key"
JWT_ALGORITHM = "HS256"
JWT_EXP_DELTA_SECONDS = 3600 # 1 hour
"""
URL configuration for core project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from app.api import api
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("app.urls")),
path("api/", api.urls),
]
......@@ -18,7 +18,9 @@ requires-python = ">=3.12"
dependencies = [
"django>=5.2",
"django-cors-headers>=4.7.0",
"django-ninja>=1.4.3",
"influxdb-client>=1.40",
"pyjwt>=2.10.1",
"python-dotenv>=1.1",
]
......
......@@ -2,6 +2,15 @@ version = 1
revision = 1
requires-python = ">=3.12"
[[package]]
name = "annotated-types"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
]
[[package]]
name = "asgiref"
version = "3.8.1"
......@@ -18,7 +27,9 @@ source = { virtual = "." }
dependencies = [
{ name = "django" },
{ name = "django-cors-headers" },
{ name = "django-ninja" },
{ name = "influxdb-client" },
{ name = "pyjwt" },
{ name = "python-dotenv" },
]
......@@ -31,7 +42,9 @@ dev = [
requires-dist = [
{ name = "django", specifier = ">=5.2" },
{ name = "django-cors-headers", specifier = ">=4.7.0" },
{ name = "django-ninja", specifier = ">=1.4.3" },
{ name = "influxdb-client", specifier = ">=1.40" },
{ name = "pyjwt", specifier = ">=2.10.1" },
{ name = "python-dotenv", specifier = ">=1.1" },
]
......@@ -74,6 +87,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/a2/7bcfff86314bd9dd698180e31ba00604001606efb518a06cca6833a54285/django_cors_headers-4.7.0-py3-none-any.whl", hash = "sha256:f1c125dcd58479fe7a67fe2499c16ee38b81b397463cf025f0e2c42937421070", size = 12794 },
]
[[package]]
name = "django-ninja"
version = "1.4.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
{ name = "pydantic" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cd/1a/f0d051f54375cfd2310f803925993fdc4172e41e627d63ece48194b07892/django_ninja-1.4.3.tar.gz", hash = "sha256:e46d477ca60c228d2a5eb3cc912094928ea830d364501f966661eeada67cb038", size = 3709571 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/08/ec/0cfa9b817f048cdec354354ae0569d7c0fd63907e5b1f927a7ee04a18635/django_ninja-1.4.3-py3-none-any.whl", hash = "sha256:f3204137a059437b95677049474220611f1cf9efedba9213556474b75168fa01", size = 2426185 },
]
[[package]]
name = "influxdb-client"
version = "1.48.0"
......@@ -90,6 +116,72 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/b3/1edc89584b8d1bc5226cf508b67ab64da3ba83041cab348861e6f4392326/influxdb_client-1.48.0-py3-none-any.whl", hash = "sha256:410db15db761df7ea98adb333c7a03f05bcc2ceef4830cefb7071b888be2b827", size = 746177 },
]
[[package]]
name = "pydantic"
version = "2.11.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
{ name = "pydantic-core" },
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782 },
]
[[package]]
name = "pydantic-core"
version = "2.33.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000 },
{ url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996 },
{ url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957 },
{ url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199 },
{ url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296 },
{ url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109 },
{ url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028 },
{ url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044 },
{ url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881 },
{ url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034 },
{ url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187 },
{ url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628 },
{ url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866 },
{ url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894 },
{ url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688 },
{ url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808 },
{ url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580 },
{ url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859 },
{ url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810 },
{ url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498 },
{ url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611 },
{ url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924 },
{ url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196 },
{ url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389 },
{ url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223 },
{ url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473 },
{ url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269 },
{ url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921 },
{ url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162 },
{ url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560 },
{ url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777 },
]
[[package]]
name = "pyjwt"
version = "2.10.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997 },
]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
......@@ -184,6 +276,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 },
]
[[package]]
name = "typing-inspection"
version = "0.4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552 },
]
[[package]]
name = "tzdata"
version = "2025.2"
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment