Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
b81b6954ef
|
|||
|
61cce57401
|
|||
|
7f9df9db91
|
|||
|
0e8f1562c8
|
|||
|
a8b5bea8cb
|
|||
|
f77b96ebcc
|
|||
|
a4d047452a
|
|||
|
c30516c2bc
|
|||
|
8b4c3cdec9
|
|||
|
9409ebacf3
|
|||
|
831a653706
|
|||
|
c9f1b1833d
|
|||
|
a2d8ccfd7a
|
|||
|
357d0d0d65
|
|||
| fe91adad08 | |||
|
0d31b9c146
|
|||
|
3246081b81
|
|||
|
3e86fe223e
|
|||
|
56c8d38cde
|
|||
|
1caffff30d
|
|||
|
46e883200e
|
|||
|
6daf2345be
|
|||
| e4b405cdbd | |||
|
a820431707
|
|||
|
5c2e58d5d0
|
|||
|
0337a90f15
|
|||
|
235420bc3e
|
|||
|
aafcdcc6de
|
|||
|
4e2467c45b
|
|||
|
436f27ef09
|
|||
|
5941f38d2a
|
|||
|
cbc2526c14
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,4 @@
|
|||||||
__pycache__
|
__pycache__
|
||||||
gatekeeper.db
|
gatekeeper.db
|
||||||
|
.env
|
||||||
|
.coverage
|
||||||
|
|||||||
7
.vscode/settings.json
vendored
Normal file
7
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"python.testing.pytestArgs": [
|
||||||
|
"test"
|
||||||
|
],
|
||||||
|
"python.testing.unittestEnabled": false,
|
||||||
|
"python.testing.pytestEnabled": true
|
||||||
|
}
|
||||||
@@ -2,13 +2,15 @@
|
|||||||
Status: WIP - not prod ready
|
Status: WIP - not prod ready
|
||||||
|
|
||||||
Dev with `nix develop`
|
Dev with `nix develop`
|
||||||
|
sync python deps with `uv sync`
|
||||||
start dev server `uv run fastapi dev`
|
start dev server `uv run fastapi dev`
|
||||||
Swagger UI @ http://127.0.0.1:8000/docs
|
Swagger UI @ http://127.0.0.1:8000/docs
|
||||||
start prod server `uv run fastapi run`
|
start prod server `uv run fastapi run`
|
||||||
|
|
||||||
|
You need to set services.pcscd.enable = true; for the smartcard reader to work
|
||||||
|
|
||||||
Issues:
|
Issues:
|
||||||
- `nix run` currently broken
|
- `nix run` currently broken
|
||||||
- no auth
|
|
||||||
- no door state
|
- no door state
|
||||||
- no door operations
|
- no door operations
|
||||||
- card system is dummy until I get hardware
|
- card system is dummy until I get hardware
|
||||||
|
|||||||
0
app/controllers/__init__.py
Normal file
0
app/controllers/__init__.py
Normal file
@@ -5,12 +5,13 @@ from typing import List
|
|||||||
|
|
||||||
from ..model.models import *
|
from ..model.models import *
|
||||||
from ..services.database import engine, get_session, add_and_refresh
|
from ..services.database import engine, get_session, add_and_refresh
|
||||||
|
from ..services.auth import auth_is_admin
|
||||||
import uuid as gen_uuid
|
import uuid as gen_uuid
|
||||||
|
|
||||||
aa_router = APIRouter(prefix="/aa", tags=["AccessAuth"])
|
aa_router = APIRouter(prefix="/aa", tags=["AccessAuth"])
|
||||||
|
|
||||||
@aa_router.post("/", response_model=AccessAuthorizationResponse)
|
@aa_router.post("/", response_model=AccessAuthorizationResponse)
|
||||||
def add_accessauth(*, db: Session = Depends(get_session), aa: AccessAuthorizationCreate):
|
def add_accessauth(*, db: Session = Depends(get_session), aa: AccessAuthorizationCreate, admin: bool = Depends(auth_is_admin)):
|
||||||
print("Creating accessauth with data: ", aa)
|
print("Creating accessauth with data: ", aa)
|
||||||
timetables = [Timetable.model_validate(t) for t in aa.timetables]
|
timetables = [Timetable.model_validate(t) for t in aa.timetables]
|
||||||
db_aa = AccessAuthorizationDB(
|
db_aa = AccessAuthorizationDB(
|
||||||
@@ -21,21 +22,21 @@ def add_accessauth(*, db: Session = Depends(get_session), aa: AccessAuthorizatio
|
|||||||
return add_and_refresh(db, db_aa)
|
return add_and_refresh(db, db_aa)
|
||||||
|
|
||||||
@aa_router.get("/", response_model=List[AccessAuthorizationResponse])
|
@aa_router.get("/", response_model=List[AccessAuthorizationResponse])
|
||||||
def get_all_accessauths(db: Session = Depends(get_session)):
|
def get_all_accessauths(db: Session = Depends(get_session), admin: bool = Depends(auth_is_admin)):
|
||||||
return db.exec(
|
return db.exec(
|
||||||
select(AccessAuthorizationDB)
|
select(AccessAuthorizationDB)
|
||||||
.options(selectinload(AccessAuthorizationDB.timetables))
|
.options(selectinload(AccessAuthorizationDB.timetables))
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
@aa_router.get("/{aa_id}", response_model=AccessAuthorizationResponse)
|
@aa_router.get("/{aa_id}", response_model=AccessAuthorizationResponse)
|
||||||
def get_one_accessauth(*, db: Session = Depends(get_session), aa_id: int):
|
def get_one_accessauth(*, db: Session = Depends(get_session), aa_id: int, admin: bool = Depends(auth_is_admin)):
|
||||||
db_aa = db.get(AccessAuthorizationDB, aa_id)
|
db_aa = db.get(AccessAuthorizationDB, aa_id)
|
||||||
if db_aa is None:
|
if db_aa is None:
|
||||||
raise HTTPException(status_code=404, detail="AA not found")
|
raise HTTPException(status_code=404, detail="AA not found")
|
||||||
return db_aa
|
return db_aa
|
||||||
|
|
||||||
@aa_router.put("/assign/{group_id}/{aa_id}", response_model=GroupResponse)
|
@aa_router.put("/assign/{group_id}/{aa_id}", response_model=GroupResponse)
|
||||||
def assign_accessauth(*, db: Session = Depends(get_session), group_id: int, aa_id: int):
|
def assign_accessauth(*, db: Session = Depends(get_session), group_id: int, aa_id: int, admin: bool = Depends(auth_is_admin)):
|
||||||
db_group = db.get(GroupDB, group_id)
|
db_group = db.get(GroupDB, group_id)
|
||||||
if db_group is None:
|
if db_group is None:
|
||||||
raise HTTPException(status_code=404, detail="Group not found")
|
raise HTTPException(status_code=404, detail="Group not found")
|
||||||
@@ -48,7 +49,7 @@ def assign_accessauth(*, db: Session = Depends(get_session), group_id: int, aa_i
|
|||||||
return add_and_refresh(db, db_group)
|
return add_and_refresh(db, db_group)
|
||||||
|
|
||||||
@aa_router.put("/unassign/{group_id}/{aa_id}", response_model=GroupResponse)
|
@aa_router.put("/unassign/{group_id}/{aa_id}", response_model=GroupResponse)
|
||||||
def unassign_accessauth(*, db: Session = Depends(get_session), group_id: int, aa_id: int):
|
def unassign_accessauth(*, db: Session = Depends(get_session), group_id: int, aa_id: int, admin: bool = Depends(auth_is_admin)):
|
||||||
db_group = db.get(GroupDB, group_id)
|
db_group = db.get(GroupDB, group_id)
|
||||||
if db_group is None:
|
if db_group is None:
|
||||||
raise HTTPException(status_code=404, detail="Group not found")
|
raise HTTPException(status_code=404, detail="Group not found")
|
||||||
@@ -61,7 +62,7 @@ def unassign_accessauth(*, db: Session = Depends(get_session), group_id: int, aa
|
|||||||
return add_and_refresh(db, db_group)
|
return add_and_refresh(db, db_group)
|
||||||
|
|
||||||
@aa_router.patch("/{aa_id}", response_model=AccessAuthorizationResponse)
|
@aa_router.patch("/{aa_id}", response_model=AccessAuthorizationResponse)
|
||||||
def change_accessauth(*, db: Session = Depends(get_session), aa_id: int, aa: AccessAuthorizationUpdate):
|
def change_accessauth(*, db: Session = Depends(get_session), aa_id: int, aa: AccessAuthorizationUpdate, admin: bool = Depends(auth_is_admin)):
|
||||||
db_aa = db.get(AccessAuthorizationDB, aa_id)
|
db_aa = db.get(AccessAuthorizationDB, aa_id)
|
||||||
if db_aa is None:
|
if db_aa is None:
|
||||||
raise HTTPException(status_code=404, detail="AccessAuthorization not found")
|
raise HTTPException(status_code=404, detail="AccessAuthorization not found")
|
||||||
@@ -70,7 +71,7 @@ def change_accessauth(*, db: Session = Depends(get_session), aa_id: int, aa: Acc
|
|||||||
return add_and_refresh(db, db_aa)
|
return add_and_refresh(db, db_aa)
|
||||||
|
|
||||||
@aa_router.delete("/{aa_id}")
|
@aa_router.delete("/{aa_id}")
|
||||||
def delete_accessauth(*, db: Session = Depends(get_session), aa_id: int):
|
def delete_accessauth(*, db: Session = Depends(get_session), aa_id: int, admin: bool = Depends(auth_is_admin)):
|
||||||
db_aa = db.get(AccessAuthorizationDB, aa_id)
|
db_aa = db.get(AccessAuthorizationDB, aa_id)
|
||||||
if db_aa is None:
|
if db_aa is None:
|
||||||
raise HTTPException(status_code=404, detail="AccessAuthorization not found")
|
raise HTTPException(status_code=404, detail="AccessAuthorization not found")
|
||||||
|
|||||||
@@ -1,34 +1,40 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from sqlmodel import Session, select
|
from sqlmodel import Session, select
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from ..model.models import Card
|
from ..model.models import Card
|
||||||
from ..services.database import engine, get_session, add_and_refresh
|
from ..services.database import engine, get_session, add_and_refresh
|
||||||
|
from ..services.auth import auth_is_admin
|
||||||
import uuid as gen_uuid
|
import uuid as gen_uuid
|
||||||
|
from app.services.scanner import WriteNewCard, DeleteCard
|
||||||
|
|
||||||
card_router = APIRouter(prefix="/cards", tags=["Card"])
|
card_router = APIRouter(prefix="/cards", tags=["Card"])
|
||||||
|
|
||||||
def register_card(group_id: int):
|
def register_card(group_id: int):
|
||||||
uuid = str(gen_uuid.uuid4()) #hier code für mifare registrierung
|
key = WriteNewCard()
|
||||||
card = Card(group_id=group_id, uuid=uuid)
|
if key == None:
|
||||||
|
print("No card registered. Check logs!")
|
||||||
|
raise HTTPException(status.HTTP_417_EXPECTATION_FAILED, detail="No card registered. Check logs!")
|
||||||
|
card = Card(group_id=group_id, uuid=key)
|
||||||
return card
|
return card
|
||||||
|
|
||||||
@card_router.post("/{group_id}", response_model=Card)
|
@card_router.post("/{group_id}", response_model=Card)
|
||||||
def add_card(*, db: Session = Depends(get_session), group_id: int):
|
def add_card(*, db: Session = Depends(get_session), group_id: int, admin: bool = Depends(auth_is_admin)):
|
||||||
card = register_card(group_id)
|
card = register_card(group_id)
|
||||||
return add_and_refresh(db, card)
|
return add_and_refresh(db, card)
|
||||||
|
|
||||||
@card_router.delete("/{card_id}")
|
@card_router.get("/delete")
|
||||||
def del_card(*, db: Session = Depends(get_session), card_id: int):
|
def del_card(*, db: Session = Depends(get_session), admin: bool = Depends(auth_is_admin)):
|
||||||
card = db.get(Card, card_id)
|
key = DeleteCard()
|
||||||
if card is None:
|
# card = db.get(Card, card_id)
|
||||||
raise HTTPException(status_code=404, detail="Card not found")
|
# if card is None:
|
||||||
db.delete(card)
|
# raise HTTPException(status_code=404, detail="Card not found")
|
||||||
db.commit()
|
# db.delete(card)
|
||||||
return {"message": "Card deleted successfully"}
|
# db.commit()
|
||||||
|
# return {"message": "Card deleted successfully"}
|
||||||
##TBH not a big fan of having creation using group_id but deletion using card_id
|
##TBH not a big fan of having creation using group_id but deletion using card_id
|
||||||
@card_router.get("/{group_id}", response_model=List[Card])
|
@card_router.get("/{group_id}", response_model=List[Card])
|
||||||
def get_cards(*, db: Session = Depends(get_session), group_id: int):
|
def get_cards(*, db: Session = Depends(get_session), group_id: int, admin: bool = Depends(auth_is_admin)):
|
||||||
cards = db.exec(select(Card).where(Card.group_id == group_id)).all()
|
cards = db.exec(select(Card).where(Card.group_id == group_id)).all()
|
||||||
return cards
|
return cards
|
||||||
|
|
||||||
|
|||||||
20
app/controllers/doorManager.py
Normal file
20
app/controllers/doorManager.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from sqlmodel import Session
|
||||||
|
|
||||||
|
from app.services.database import get_session
|
||||||
|
from app.services.auth import auth_is_admin
|
||||||
|
import app.services.door as doorService
|
||||||
|
|
||||||
|
door_router = APIRouter(prefix="/door",tags=["Door"])
|
||||||
|
|
||||||
|
@door_router.put("/open")
|
||||||
|
def open_door(db: Session = Depends(get_session), admin: bool = Depends(auth_is_admin)):
|
||||||
|
doorService.opendoor()
|
||||||
|
|
||||||
|
@door_router.put("/close")
|
||||||
|
def open_door(db: Session = Depends(get_session), admin: bool = Depends(auth_is_admin)):
|
||||||
|
doorService.closedoor()
|
||||||
|
|
||||||
|
@door_router.post("/test")
|
||||||
|
def test_access(input: str, db: Session = Depends(get_session)):
|
||||||
|
return doorService.checkAccess(input, db=db)
|
||||||
@@ -1,24 +1,28 @@
|
|||||||
from fastapi import APIRouter, HTTPException, Depends
|
from fastapi import APIRouter, HTTPException, Depends, status
|
||||||
from sqlmodel import Session, select
|
from sqlmodel import Session, select
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from ..model.models import GroupDB, GroupResponse, GroupCreate
|
from ..model.models import GroupDB, GroupResponse, GroupCreate
|
||||||
from ..services.database import engine, get_session, add_and_refresh
|
from ..services.database import engine, get_session, add_and_refresh
|
||||||
|
from ..services.auth import auth_is_admin
|
||||||
|
|
||||||
group_router = APIRouter(prefix="/groups", tags=["Group"])
|
group_router = APIRouter(prefix="/groups", tags=["Group"])
|
||||||
|
|
||||||
@group_router.get("/", response_model=List[GroupResponse])
|
@group_router.get("/", response_model=List[GroupResponse])
|
||||||
def get_groups(*, db: Session = Depends(get_session)):
|
def get_groups(*, db: Session = Depends(get_session), admin: bool = Depends(auth_is_admin)):
|
||||||
groups = db.exec(select(GroupDB)).all()
|
groups = db.exec(select(GroupDB)).all()
|
||||||
return groups
|
return groups
|
||||||
|
|
||||||
@group_router.post("/", response_model=GroupResponse)
|
@group_router.post("/", response_model=GroupResponse)
|
||||||
def create_group(*, db: Session = Depends(get_session), group: GroupCreate):
|
def create_group(*, db: Session = Depends(get_session), group: GroupCreate, admin: bool = Depends(auth_is_admin)):
|
||||||
db_group = GroupDB.model_validate(group)
|
db_group = GroupDB.model_validate(group)
|
||||||
|
group = db.exec(select(GroupDB).where(GroupDB.name == db_group.name)).first()
|
||||||
|
if group is not None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Group already exists!")
|
||||||
return add_and_refresh(db, db_group)
|
return add_and_refresh(db, db_group)
|
||||||
|
|
||||||
@group_router.delete("/{group_id}")
|
@group_router.delete("/{group_id}")
|
||||||
def delete_group(*, db: Session = Depends(get_session), group_id: int):
|
def delete_group(*, db: Session = Depends(get_session), group_id: int, admin: bool = Depends(auth_is_admin)):
|
||||||
db_group = db.get(GroupDB, group_id)
|
db_group = db.get(GroupDB, group_id)
|
||||||
if db_group is None:
|
if db_group is None:
|
||||||
raise HTTPException(status_code=404, detail="Group not found")
|
raise HTTPException(status_code=404, detail="Group not found")
|
||||||
|
|||||||
@@ -4,41 +4,44 @@ from typing import List
|
|||||||
|
|
||||||
from ..model.models import UserResponse, UserCreate, UserDB, UserUpdate
|
from ..model.models import UserResponse, UserCreate, UserDB, UserUpdate
|
||||||
from ..services.database import engine, get_session, add_and_refresh
|
from ..services.database import engine, get_session, add_and_refresh
|
||||||
|
from ..services.auth import get_password_hash, get_current_user, auth_is_admin
|
||||||
|
|
||||||
user_router = APIRouter(tags=["Users"])
|
user_router = APIRouter(tags=["Users"])
|
||||||
|
|
||||||
@user_router.post("/users/", response_model=UserResponse)
|
@user_router.post("/users/", response_model=UserResponse)
|
||||||
def create_user(*, db: Session = Depends(get_session), user: UserCreate):
|
def create_user(*, db: Session = Depends(get_session), user: UserCreate, admin: bool = Depends(auth_is_admin)):
|
||||||
print("creating user with data ", user)
|
print("creating user with data ", user)
|
||||||
db_user = UserDB.model_validate(user)
|
hashed_password = {"passwordhash": get_password_hash(user.password)}
|
||||||
|
db_user = UserDB.model_validate(user, update=hashed_password)
|
||||||
return add_and_refresh(db, db_user)
|
return add_and_refresh(db, db_user)
|
||||||
|
|
||||||
@user_router.get("/users/", response_model=List[UserResponse])
|
@user_router.get("/users/", response_model=List[UserResponse])
|
||||||
def read_users(*, db: Session = Depends(get_session)):
|
def read_users(*, db: Session = Depends(get_session), admin: bool = Depends(auth_is_admin)):
|
||||||
users = db.exec(select(UserDB)).all()
|
users = db.exec(select(UserDB)).all()
|
||||||
return users
|
return users
|
||||||
|
|
||||||
@user_router.get("/users/{user_id}", response_model=UserResponse)
|
@user_router.get("/users/{user_id}", response_model=UserResponse)
|
||||||
def read_user(*, db: Session = Depends(get_session), user_id: int):
|
def read_user(*, db: Session = Depends(get_session), user_id: int, admin: bool = Depends(auth_is_admin)):
|
||||||
db_user = db.get(UserDB, user_id)
|
db_user = db.get(UserDB, user_id)
|
||||||
if db_user is None:
|
if db_user is None:
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
return db_user
|
return db_user
|
||||||
|
|
||||||
@user_router.patch("/users/{user_id}", response_model=UserResponse)
|
@user_router.patch("/users/{user_id}", response_model=UserResponse)
|
||||||
def update_user(*, db: Session = Depends(get_session), user_id: int, user: UserUpdate):
|
def update_user(*, db: Session = Depends(get_session), user_id: int, user: UserUpdate, admin: bool = Depends(auth_is_admin)):
|
||||||
db_user = db.get(UserDB, user_id)
|
db_user = db.get(UserDB, user_id)
|
||||||
if db_user is None:
|
if db_user is None:
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
user_data = user.model_dump(exclude_unset=True)
|
user_data = user.model_dump(exclude_unset=True)
|
||||||
db_user.sqlmodel_update(user_data)
|
hashed_password = {}
|
||||||
db.add(db_user)
|
if "password" in user_data:
|
||||||
db.commit()
|
password = user_data["password"]
|
||||||
db.refresh(db_user)
|
hashed_password = {"passwordhash": get_password_hash(password)}
|
||||||
return db_user
|
db_user.sqlmodel_update(user_data, update=hashed_password)
|
||||||
|
return add_and_refresh(db, db_user)
|
||||||
|
|
||||||
@user_router.delete("/users/{user_id}")
|
@user_router.delete("/users/{user_id}")
|
||||||
def delete_user(*, db: Session = Depends(get_session), user_id: int):
|
def delete_user(*, db: Session = Depends(get_session), user_id: int, admin: bool = Depends(auth_is_admin)):
|
||||||
db_user = db.get(UserDB, user_id)
|
db_user = db.get(UserDB, user_id)
|
||||||
if db_user is None:
|
if db_user is None:
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
|
|||||||
29
app/main.py
29
app/main.py
@@ -1,14 +1,33 @@
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from .controllers import userManager, cardManager, groupManager, aaManager
|
from fastapi.security import OAuth2PasswordBearer
|
||||||
from .services.database import create_db_and_tables
|
from contextlib import asynccontextmanager
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
app = FastAPI()
|
from .controllers import userManager, cardManager, groupManager, aaManager, doorManager
|
||||||
@app.on_event("startup")
|
from .services.database import create_db_and_tables, get_session
|
||||||
def on_startup():
|
from .services.auth import token_router, create_first_user
|
||||||
|
from app.services.scanner import BackgroundScanner
|
||||||
|
|
||||||
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
||||||
|
scanner = BackgroundScanner(db=get_session())
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
load_dotenv()
|
||||||
create_db_and_tables()
|
create_db_and_tables()
|
||||||
|
create_first_user()
|
||||||
print("Database created and tables initialized.")
|
print("Database created and tables initialized.")
|
||||||
|
scanner.start()
|
||||||
|
yield
|
||||||
|
#scanner.stop()
|
||||||
|
|
||||||
|
app = FastAPI(lifespan=lifespan)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
app.include_router(token_router)
|
||||||
app.include_router(userManager.user_router)
|
app.include_router(userManager.user_router)
|
||||||
app.include_router(groupManager.group_router)
|
app.include_router(groupManager.group_router)
|
||||||
app.include_router(cardManager.card_router)
|
app.include_router(cardManager.card_router)
|
||||||
app.include_router(aaManager.aa_router)
|
app.include_router(aaManager.aa_router)
|
||||||
|
app.include_router(doorManager.door_router)
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
from sqlmodel import Field, Relationship, Session, SQLModel
|
from sqlmodel import Field, Relationship, Session, SQLModel
|
||||||
from typing import List
|
from typing import List
|
||||||
|
from datetime import time
|
||||||
|
|
||||||
class Base(SQLModel):
|
class Base(SQLModel):
|
||||||
pass
|
pass
|
||||||
@@ -18,7 +19,7 @@ class UserCreate(UserBase):
|
|||||||
|
|
||||||
class UserDB(UserBase, table=True):
|
class UserDB(UserBase, table=True):
|
||||||
id: int | None = Field(default=None, primary_key=True)
|
id: int | None = Field(default=None, primary_key=True)
|
||||||
password: str
|
passwordhash: str
|
||||||
|
|
||||||
class UserUpdate(Base):
|
class UserUpdate(Base):
|
||||||
name: str | None = None
|
name: str | None = None
|
||||||
@@ -31,6 +32,14 @@ class AaGroupLink(Base, table=True):
|
|||||||
group_id: int | None = Field(default=None, foreign_key="groupdb.id", primary_key=True)
|
group_id: int | None = Field(default=None, foreign_key="groupdb.id", primary_key=True)
|
||||||
accessauth_id: int | None = Field(default=None, foreign_key="accessauthorizationdb.id", primary_key=True)
|
accessauth_id: int | None = Field(default=None, foreign_key="accessauthorizationdb.id", primary_key=True)
|
||||||
|
|
||||||
|
#### Token
|
||||||
|
class Token(Base):
|
||||||
|
access_token: str
|
||||||
|
token_type: str
|
||||||
|
|
||||||
|
class TokenData(Base):
|
||||||
|
username: str | None = None
|
||||||
|
|
||||||
#### Group
|
#### Group
|
||||||
class GroupBase(Base):
|
class GroupBase(Base):
|
||||||
name: str = Field(index=True, unique=True)
|
name: str = Field(index=True, unique=True)
|
||||||
@@ -80,8 +89,8 @@ class Card(Base, table=True):
|
|||||||
group: GroupDB | None = Relationship(back_populates="cards")
|
group: GroupDB | None = Relationship(back_populates="cards")
|
||||||
|
|
||||||
class TimetableBase(Base):
|
class TimetableBase(Base):
|
||||||
weekday: int = Field(le=7, ge=1)
|
weekday: int = Field(le=6, ge=0)
|
||||||
starttime: str
|
starttime: time
|
||||||
duration: int = Field(gt=0, lt=1440)
|
duration: int = Field(gt=0, lt=1440)
|
||||||
|
|
||||||
class Timetable(TimetableBase, table=True):
|
class Timetable(TimetableBase, table=True):
|
||||||
|
|||||||
0
app/services/__init__.py
Normal file
0
app/services/__init__.py
Normal file
124
app/services/auth.py
Normal file
124
app/services/auth.py
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
from typing import Annotated
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from fastapi import APIRouter, HTTPException, Depends, status
|
||||||
|
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||||
|
from sqlmodel import Session, select
|
||||||
|
from pwdlib import PasswordHash
|
||||||
|
import jwt
|
||||||
|
from jwt.exceptions import InvalidTokenError
|
||||||
|
from ..model.models import UserDB, Token, TokenData, UserCreate
|
||||||
|
from ..services.database import *
|
||||||
|
import secrets, string
|
||||||
|
|
||||||
|
SECRET_KEY = "8b14d0b447bff7efa24d5019cc59a999786e31f6f865173bbd642bf18de5ad85" #Encrypt and change later or store in env file or somehthing
|
||||||
|
ALGORITHM = "HS256"
|
||||||
|
ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
||||||
|
|
||||||
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
||||||
|
|
||||||
|
token_router = APIRouter(tags=["Token"])
|
||||||
|
|
||||||
|
password_hash = PasswordHash.recommended()
|
||||||
|
|
||||||
|
def verify_password(plain_password, hashed_password):
|
||||||
|
return password_hash.verify(plain_password, hashed_password)
|
||||||
|
|
||||||
|
def get_password_hash(password):
|
||||||
|
return password_hash.hash(password)
|
||||||
|
|
||||||
|
def get_user(db, username: str):
|
||||||
|
user = db.exec(select(UserDB).where(UserDB.name == username)).first()
|
||||||
|
return user
|
||||||
|
|
||||||
|
def authenticate_user(db, username: str, password: str):
|
||||||
|
user = get_user(db, username)
|
||||||
|
if not user:
|
||||||
|
return False
|
||||||
|
if not verify_password(password, user.passwordhash):
|
||||||
|
return False
|
||||||
|
return user
|
||||||
|
|
||||||
|
def create_access_token(data: dict, expires_delta: timedelta | None = None):
|
||||||
|
to_encode = data.copy()
|
||||||
|
if expires_delta:
|
||||||
|
expire = datetime.now(timezone.utc) + expires_delta
|
||||||
|
else:
|
||||||
|
expire = datetime.now(timezone.utc) + timedelta(minutes=15)
|
||||||
|
to_encode.update({"exp": expire})
|
||||||
|
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||||
|
return encoded_jwt
|
||||||
|
|
||||||
|
def get_current_user(
|
||||||
|
token: Annotated[str, Depends(oauth2_scheme)],
|
||||||
|
db: Session = Depends(get_session),
|
||||||
|
):
|
||||||
|
credentials_exception = HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Could not validate credentials",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"}
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||||
|
username = payload.get("sub")
|
||||||
|
if username is None:
|
||||||
|
raise credentials_exception
|
||||||
|
token_data = TokenData(username=username)
|
||||||
|
except InvalidTokenError:
|
||||||
|
raise credentials_exception
|
||||||
|
user = get_user(db, username=token_data.username)
|
||||||
|
if user is None:
|
||||||
|
raise credentials_exception
|
||||||
|
return user
|
||||||
|
|
||||||
|
def auth_is_admin(
|
||||||
|
token: str = Depends(oauth2_scheme),
|
||||||
|
db: Session = Depends(get_session),
|
||||||
|
):
|
||||||
|
user = get_current_user(token=token, db=db)
|
||||||
|
if not user.is_admin:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Not authorized to perform this action",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"}
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def create_first_user():
|
||||||
|
print("Checking for admin user")
|
||||||
|
with Session(engine) as db:
|
||||||
|
admin_user = db.exec(select(UserDB)).first()
|
||||||
|
if admin_user is None:
|
||||||
|
password = ''.join(secrets.choice(string.digits) for i in range(8))
|
||||||
|
print("Creating first admin user with password", password)
|
||||||
|
user = UserDB(
|
||||||
|
name="admin",
|
||||||
|
passwordhash=get_password_hash(password),
|
||||||
|
is_admin=True
|
||||||
|
)
|
||||||
|
return add_and_refresh(db, user)
|
||||||
|
print(f"Admin user already exists: {admin_user.name}")
|
||||||
|
|
||||||
|
|
||||||
|
@token_router.post("/token")
|
||||||
|
def login_for_access_token(
|
||||||
|
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
|
||||||
|
db: Session = Depends(get_session)
|
||||||
|
) -> Token:
|
||||||
|
user = authenticate_user(db, form_data.username, form_data.password)
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Incorrect username or pw",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"}
|
||||||
|
)
|
||||||
|
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||||
|
access_token = create_access_token(
|
||||||
|
data={"sub": user.name}, expires_delta=access_token_expires
|
||||||
|
)
|
||||||
|
return Token(access_token=access_token, token_type="bearer")
|
||||||
|
|
||||||
|
@token_router.get("/test/login")
|
||||||
|
def test_login(
|
||||||
|
current_user: Annotated[UserDB, Depends(get_current_user)]
|
||||||
|
) -> UserDB:
|
||||||
|
return current_user
|
||||||
54
app/services/door.py
Normal file
54
app/services/door.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
|
||||||
|
import paho.mqtt.client as mqttClient
|
||||||
|
import paho.mqtt.publish as publish
|
||||||
|
|
||||||
|
from sqlmodel import select
|
||||||
|
from fastapi import Depends, HTTPException, status
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
|
import sqlalchemy.exc as exc
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from app.services.database import Session, get_session
|
||||||
|
from app.model.models import *
|
||||||
|
|
||||||
|
doorIsOpen = True
|
||||||
|
client = mqttClient.Client(client_id="", userdata=None, protocol=mqttClient.MQTTv5)
|
||||||
|
client.tls_set(tls_version=mqttClient.ssl.PROTOCOL_TLS)
|
||||||
|
client.username_pw_set("username", "passwort")
|
||||||
|
#client.connect("host", port=8883)
|
||||||
|
# I think this could also be gpio controlled
|
||||||
|
#See: https://github.com/technyon/nuki_hub#gpio-lock-control-optional
|
||||||
|
|
||||||
|
def openDoor():
|
||||||
|
doorIsOpen = True
|
||||||
|
publish.single(topic="/lock/action", payload="unlock")
|
||||||
|
pass
|
||||||
|
|
||||||
|
def closeDoor():
|
||||||
|
doorIsOpen = False
|
||||||
|
publish.single(topic="/lock/action", payload="lock")
|
||||||
|
pass
|
||||||
|
|
||||||
|
def isDoorOpen():
|
||||||
|
return doorIsOpen
|
||||||
|
|
||||||
|
def checkAccess(uuid: str, db: Session = Depends(get_session)):
|
||||||
|
try:
|
||||||
|
current_weekday = datetime.datetime.weekday(datetime.date.today())
|
||||||
|
current_time = datetime.datetime.now()
|
||||||
|
card = db.exec(select(Card).where(Card.uuid == uuid)).one()
|
||||||
|
for auth in card.group.accessauths:
|
||||||
|
print(f"checking auth: {auth.name}")
|
||||||
|
for timetable in auth.timetables:
|
||||||
|
print(f" checking timetable {timetable.id}")
|
||||||
|
print(f" comparing weekday: CUR:{current_weekday} TT:{timetable.weekday}")
|
||||||
|
if current_weekday == timetable.weekday:
|
||||||
|
starttime = datetime.datetime.combine(datetime.date.today(), timetable.starttime)
|
||||||
|
endtime = starttime + datetime.timedelta(minutes=timetable.duration)
|
||||||
|
print(f" comparing time: Start:{starttime} Current:{current_time} End:{endtime}")
|
||||||
|
if starttime < current_time < endtime:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
except exc.NoResultFound:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
303
app/services/scanner.py
Normal file
303
app/services/scanner.py
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
import secrets
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
from sqlmodel import Session
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from fastapi import HTTPException, status
|
||||||
|
|
||||||
|
from smartcard.CardRequest import CardRequest
|
||||||
|
from smartcard.CardType import AnyCardType
|
||||||
|
from smartcard.Exceptions import CardRequestTimeoutException
|
||||||
|
|
||||||
|
from desfire import DESFire, DESFireKey, PCSCDevice, diversify_key, get_list, to_hex_string
|
||||||
|
from desfire.enums import DESFireCommunicationMode, DESFireFileType, DESFireKeySettings, DESFireKeyType
|
||||||
|
from desfire.schemas import FilePermissions, FileSettings, KeySettings
|
||||||
|
import desfire.exceptions as desExceptions
|
||||||
|
|
||||||
|
|
||||||
|
#ENV vars
|
||||||
|
load_dotenv()
|
||||||
|
MIFARE_APP_MASTER_KEY = os.getenv('MIFARE_APP_MASTER_KEY')
|
||||||
|
MIFARE_ACL_READ_BASE_KEY = os.getenv('MIFARE_ACL_READ_BASE_KEY')
|
||||||
|
MIFARE_ACL_WRITE_BASE_KEY = os.getenv('MIFARE_ACL_WRITE_BASE_KEY')
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
MIFARE_APP_ID = "DEAFFE" # 7 bytes
|
||||||
|
MIFARE_ACL_READ_BASE_KEY_ID = 0x1
|
||||||
|
MIFARE_ACL_WRITE_BASE_KEY_ID = 0x2
|
||||||
|
MIFARE_SYS_ID = "FF0000" # 3 bytes, can essentially be anything
|
||||||
|
MIFARE_ENCRYPTED_FILE_ID = 0x1
|
||||||
|
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def checkForKey():
|
||||||
|
if MIFARE_APP_MASTER_KEY == None:
|
||||||
|
logger.critical("NO MASTER KEY LOADED")
|
||||||
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="No key loaded! Check application.")
|
||||||
|
|
||||||
|
def getCardService(timeout: int = 10):
|
||||||
|
cardtype = AnyCardType()
|
||||||
|
cardrequest = CardRequest(timeout=timeout, cardType=cardtype)
|
||||||
|
print("Please present DESfire tag...")
|
||||||
|
try:
|
||||||
|
cardservice = cardrequest.waitforcard()
|
||||||
|
except CardRequestTimeoutException:
|
||||||
|
logger.error("No tag detected within the timeout.")
|
||||||
|
raise Exception
|
||||||
|
return cardservice
|
||||||
|
|
||||||
|
def DeleteCard():
|
||||||
|
try:
|
||||||
|
checkForKey()
|
||||||
|
from app.main import scanner as scannerThread
|
||||||
|
scannerThread.stop()
|
||||||
|
cardservice = getCardService(15)
|
||||||
|
cardservice.connection.connect()
|
||||||
|
|
||||||
|
# Create Desfire object
|
||||||
|
desfire = DESFire(PCSCDevice(cardservice.connection.component))
|
||||||
|
|
||||||
|
# Create Key objects
|
||||||
|
AES_NULL_KEY_DATA = "00" * 8
|
||||||
|
aes_keysettings = KeySettings(
|
||||||
|
key_type=DESFireKeyType.DF_KEY_AES,
|
||||||
|
)
|
||||||
|
key_settings = desfire.get_key_setting()
|
||||||
|
aes_null_key = DESFireKey(key_settings, AES_NULL_KEY_DATA)
|
||||||
|
aes_master_key = DESFireKey(aes_keysettings, MIFARE_APP_MASTER_KEY)
|
||||||
|
desfire.authenticate(0x0, aes_null_key)
|
||||||
|
applications = desfire.get_application_ids()
|
||||||
|
logger.debug(f"Applications: {applications}")
|
||||||
|
if len(applications) == 0:
|
||||||
|
raise HTTPException(status_code=status.HTTP_410_GONE, detail="No applications on card")
|
||||||
|
|
||||||
|
desfire.select_application(MIFARE_APP_ID)
|
||||||
|
desfire.authenticate(0x0, aes_master_key)
|
||||||
|
try:
|
||||||
|
desfire.delete_application(MIFARE_APP_ID)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
scannerThread.start()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in deletion function: {e}", exc_info=True)
|
||||||
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error: {e}")
|
||||||
|
|
||||||
|
def WriteNewCard():
|
||||||
|
try:
|
||||||
|
checkForKey()
|
||||||
|
from app.main import scanner as scannerThread
|
||||||
|
scannerThread.stop()
|
||||||
|
|
||||||
|
cardtype = AnyCardType()
|
||||||
|
cardrequest = CardRequest(timeout=10, cardType=cardtype)
|
||||||
|
print("Please present DESfire tag...")
|
||||||
|
try:
|
||||||
|
cardservice = cardrequest.waitforcard()
|
||||||
|
except CardRequestTimeoutException:
|
||||||
|
logger.error("No tag detected within the timeout.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
cardservice.connection.connect()
|
||||||
|
|
||||||
|
# Create Desfire object
|
||||||
|
desfire = DESFire(PCSCDevice(cardservice.connection.component))
|
||||||
|
|
||||||
|
# Create Key objects
|
||||||
|
AES_NULL_KEY_DATA = "00" * 16
|
||||||
|
aes_keysettings = KeySettings(
|
||||||
|
key_type=DESFireKeyType.DF_KEY_AES,
|
||||||
|
)
|
||||||
|
aes_null_key = DESFireKey(aes_keysettings, AES_NULL_KEY_DATA)
|
||||||
|
aes_master_key = DESFireKey(aes_keysettings, MIFARE_APP_MASTER_KEY)
|
||||||
|
keysetting = desfire.get_key_setting()
|
||||||
|
desKey = DESFireKey(keysetting, "00" * 8)
|
||||||
|
|
||||||
|
# Authenticate with default DES key
|
||||||
|
print("Authenticating with default DES key...")
|
||||||
|
desfire.authenticate(0x0, desKey)
|
||||||
|
|
||||||
|
#get uid
|
||||||
|
uid = desfire.get_real_uid()
|
||||||
|
|
||||||
|
# Set default key
|
||||||
|
print("Setting default key...")
|
||||||
|
desfire.change_default_key(aes_null_key, 0x0)
|
||||||
|
|
||||||
|
# Create application
|
||||||
|
print("Creating application...")
|
||||||
|
app_settings = KeySettings(
|
||||||
|
settings=[
|
||||||
|
DESFireKeySettings.KS_ALLOW_CHANGE_MK,
|
||||||
|
DESFireKeySettings.KS_LISTING_WITHOUT_MK,
|
||||||
|
DESFireKeySettings.KS_CREATE_DELETE_WITHOUT_MK,
|
||||||
|
DESFireKeySettings.KS_CONFIGURATION_CHANGEABLE,
|
||||||
|
],
|
||||||
|
key_type=DESFireKeyType.DF_KEY_AES,
|
||||||
|
)
|
||||||
|
desfire.create_application(MIFARE_APP_ID, app_settings, 4)
|
||||||
|
|
||||||
|
# Verify application creation
|
||||||
|
applications = desfire.get_application_ids()
|
||||||
|
assert len(applications) == 1
|
||||||
|
assert applications[0] == get_list(MIFARE_APP_ID)
|
||||||
|
print(" - Application created successfully.")
|
||||||
|
|
||||||
|
# Select application
|
||||||
|
desfire.select_application(MIFARE_APP_ID)
|
||||||
|
#Auth again as 0key
|
||||||
|
aes_null_auth_key = DESFireKey(aes_keysettings, AES_NULL_KEY_DATA)
|
||||||
|
desfire.authenticate(0x0, aes_null_auth_key)
|
||||||
|
# Authenticate with AES key, as this has been set as the default key
|
||||||
|
aes_app_mk = DESFireKey(aes_keysettings, MIFARE_APP_MASTER_KEY)
|
||||||
|
desfire.change_key(0x0, aes_null_key, aes_app_mk, 0x1)
|
||||||
|
|
||||||
|
print("new key auth")
|
||||||
|
desfire.authenticate(0x0, aes_app_mk)
|
||||||
|
|
||||||
|
#generate div data
|
||||||
|
diversification_data = [0x01] + uid + get_list(MIFARE_APP_ID) + get_list(MIFARE_SYS_ID)
|
||||||
|
read_div_key_bytes = diversify_key(get_list(MIFARE_ACL_READ_BASE_KEY), diversification_data, pad_to_32=False)
|
||||||
|
write_div_key_bytes = diversify_key(get_list(MIFARE_ACL_WRITE_BASE_KEY), diversification_data, pad_to_32=False)
|
||||||
|
|
||||||
|
print("Changing file read key...")
|
||||||
|
aes_file_read_key = DESFireKey(aes_keysettings, read_div_key_bytes)
|
||||||
|
desfire.change_key(MIFARE_ACL_READ_BASE_KEY_ID, aes_null_key, aes_file_read_key, 0x1)
|
||||||
|
|
||||||
|
print("Changing file write key...")
|
||||||
|
aes_file_write_key = DESFireKey(aes_keysettings, write_div_key_bytes)
|
||||||
|
desfire.change_key(MIFARE_ACL_WRITE_BASE_KEY_ID, aes_null_key, aes_file_write_key, 0x1)
|
||||||
|
|
||||||
|
print("Create encrypted file containing UUID...")
|
||||||
|
file_settings = FileSettings(
|
||||||
|
file_size=16,
|
||||||
|
encryption=DESFireCommunicationMode.ENCRYPTED,
|
||||||
|
permissions=FilePermissions(
|
||||||
|
read_key=MIFARE_ACL_READ_BASE_KEY_ID,
|
||||||
|
write_key=MIFARE_ACL_WRITE_BASE_KEY_ID,
|
||||||
|
),
|
||||||
|
file_type=DESFireFileType.MDFT_STANDARD_DATA_FILE,
|
||||||
|
)
|
||||||
|
desfire.create_standard_file(MIFARE_ENCRYPTED_FILE_ID, file_settings)
|
||||||
|
|
||||||
|
print("Read and verify file settings again...")
|
||||||
|
file_data = desfire.get_file_settings(MIFARE_ENCRYPTED_FILE_ID)
|
||||||
|
assert file_data.file_size == 16
|
||||||
|
assert file_data.encryption == DESFireCommunicationMode.ENCRYPTED
|
||||||
|
assert file_data.permissions is not None
|
||||||
|
assert file_data.permissions.read_access == MIFARE_ACL_READ_BASE_KEY_ID
|
||||||
|
assert file_data.permissions.write_access == MIFARE_ACL_WRITE_BASE_KEY_ID
|
||||||
|
assert file_data.file_type == DESFireFileType.MDFT_STANDARD_DATA_FILE
|
||||||
|
print(" - File created successfully.")
|
||||||
|
|
||||||
|
print("Writing UID to encrypted file...")
|
||||||
|
key = secrets.token_hex(16)
|
||||||
|
desfire.write_file_data(MIFARE_ENCRYPTED_FILE_ID, 0x0, file_data.encryption, get_list(key))
|
||||||
|
|
||||||
|
print("Reading from encrypted file...")
|
||||||
|
rdata = desfire.read_file_data(MIFARE_ENCRYPTED_FILE_ID, file_data)
|
||||||
|
assert rdata == get_list(key)
|
||||||
|
print(" - Data written successfully.")
|
||||||
|
scannerThread.start()
|
||||||
|
return key
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in write function: {e}", exc_info=True)
|
||||||
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
class BackgroundScanner:
|
||||||
|
def __init__(self, db):
|
||||||
|
self.db = db
|
||||||
|
self.is_running = False
|
||||||
|
self.thread: Optional[threading.Thread] = None
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
if self.is_running:
|
||||||
|
logger.info("Scanner already running")
|
||||||
|
return
|
||||||
|
self.is_running = True
|
||||||
|
self.thread = threading.Thread(target=self._scan_loop, daemon=True)
|
||||||
|
self.thread.start()
|
||||||
|
logger.info("Scanner started")
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.is_running = False
|
||||||
|
if self.thread:
|
||||||
|
self.thread.join()
|
||||||
|
logger.info("Scanner stopped")
|
||||||
|
|
||||||
|
def _scan_loop(self):
|
||||||
|
while self.is_running:
|
||||||
|
try:
|
||||||
|
card_content = self._read_card()
|
||||||
|
if card_content:
|
||||||
|
logger.info(to_hex_string(card_content))
|
||||||
|
time.sleep(5)
|
||||||
|
logger.debug("READY after success")
|
||||||
|
#self._check_db(card_content)
|
||||||
|
else:
|
||||||
|
time.sleep(0.5)
|
||||||
|
logger.debug("READY after timout")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in scan function: {e}", exc_info=True)
|
||||||
|
time.sleep(6)
|
||||||
|
|
||||||
|
def _read_card(self):
|
||||||
|
cardtype = AnyCardType()
|
||||||
|
cardrequest = CardRequest(timeout=5, cardType=cardtype)
|
||||||
|
try:
|
||||||
|
cardservice = cardrequest.waitforcard()
|
||||||
|
except CardRequestTimeoutException:
|
||||||
|
logger.debug("No tag detected within the timeout.")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
cardservice.connection.connect()
|
||||||
|
|
||||||
|
# Create Desfire object
|
||||||
|
desfire = DESFire(PCSCDevice(cardservice.connection.component))
|
||||||
|
aes_keysettings = KeySettings(
|
||||||
|
key_type=DESFireKeyType.DF_KEY_AES,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get real UID
|
||||||
|
mk = DESFireKey(desfire.get_key_setting(), "00" * 8)
|
||||||
|
desfire.authenticate(0x0, mk)
|
||||||
|
#To get the uid you have to auth with an empty (default) key
|
||||||
|
uid = desfire.get_real_uid()
|
||||||
|
applications = desfire.get_application_ids()
|
||||||
|
try:
|
||||||
|
assert len(applications) == 1
|
||||||
|
assert applications[0] == get_list(MIFARE_APP_ID)
|
||||||
|
except AssertionError:
|
||||||
|
logger.error("No application found!")
|
||||||
|
time.sleep(4)
|
||||||
|
return None
|
||||||
|
#Then use the key derivation with that uid, the appid, the sysid
|
||||||
|
diversification_data = [0x01] + uid + get_list(MIFARE_APP_ID) + get_list(MIFARE_SYS_ID)
|
||||||
|
read_div_key_bytes = diversify_key(get_list(MIFARE_ACL_READ_BASE_KEY), diversification_data, pad_to_32=False)
|
||||||
|
|
||||||
|
#Log in with derived read key
|
||||||
|
logger.info("Start auth")
|
||||||
|
aes_app_read_key = DESFireKey(aes_keysettings, read_div_key_bytes)
|
||||||
|
desfire.select_application(MIFARE_APP_ID)
|
||||||
|
|
||||||
|
desfire.authenticate(MIFARE_ACL_READ_BASE_KEY_ID, aes_app_read_key)
|
||||||
|
|
||||||
|
logger.info("Read data")
|
||||||
|
file_data = desfire.get_file_settings(MIFARE_ENCRYPTED_FILE_ID)
|
||||||
|
logger.info(f"File settings: {file_data}")
|
||||||
|
rdata = desfire.read_file_data(MIFARE_ENCRYPTED_FILE_ID, file_data)
|
||||||
|
return rdata
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"something went wrong: {e}")
|
||||||
|
time.sleep(5)
|
||||||
24
flake.lock
generated
24
flake.lock
generated
@@ -2,11 +2,11 @@
|
|||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1775710090,
|
"lastModified": 1778869304,
|
||||||
"narHash": "sha256-ar3rofg+awPB8QXDaFJhJ2jJhu+KqN/PRCXeyuXR76E=",
|
"narHash": "sha256-30sZNZoA1cqF5JNO9fVX+wgiQYjB7HJqqJ4ztCDeBZE=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "4c1018dae018162ec878d42fec712642d214fdfa",
|
"rev": "d233902339c02a9c334e7e593de68855ad26c4cb",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -29,11 +29,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1773870109,
|
"lastModified": 1776659114,
|
||||||
"narHash": "sha256-ZoTdqZP03DcdoyxvpFHCAek4bkPUTUPUF3oCCgc3dP4=",
|
"narHash": "sha256-qapCOQmR++yZSY43dzrp3wCrkOTLpod+ONtJWBk6iKU=",
|
||||||
"owner": "pyproject-nix",
|
"owner": "pyproject-nix",
|
||||||
"repo": "build-system-pkgs",
|
"repo": "build-system-pkgs",
|
||||||
"rev": "b6e74f433b02fa4b8a7965ee24680f4867e2926f",
|
"rev": "ffaa2161dd5d63e0e94591f86b54fc239660fb2e",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -49,11 +49,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1776120154,
|
"lastModified": 1778901413,
|
||||||
"narHash": "sha256-mtIBTmVKzyoFYoAGdd8Cd7iswFna9YQVyjObZLXPO64=",
|
"narHash": "sha256-GSKXTAnFqRAMlZkJrIPcQMYf+lpMr66K3i60mB9STvc=",
|
||||||
"owner": "pyproject-nix",
|
"owner": "pyproject-nix",
|
||||||
"repo": "pyproject.nix",
|
"repo": "pyproject.nix",
|
||||||
"rev": "29dc4e9960d2b7f122b52b155e0e8f87cd5c5c08",
|
"rev": "a228447c3e179d477c1b6246ef3efa8cfe3c469a",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -80,11 +80,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1776114780,
|
"lastModified": 1779269674,
|
||||||
"narHash": "sha256-aYUgp40qkY7oNSm+G9A/woNYP+eDeFM0bYckfmxEUiY=",
|
"narHash": "sha256-P1LHCRdYpdtHAEzuEsNHrI6d9mVPl5a2fyFDZGHNVbI=",
|
||||||
"owner": "pyproject-nix",
|
"owner": "pyproject-nix",
|
||||||
"repo": "uv2nix",
|
"repo": "uv2nix",
|
||||||
"rev": "73ff87a3e489b07b9cf842f917963a9e40d49225",
|
"rev": "69aec536f6d1acc415ed2e20299312802aba98c6",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
12
flake.nix
12
flake.nix
@@ -58,6 +58,13 @@
|
|||||||
lib.composeManyExtensions [
|
lib.composeManyExtensions [
|
||||||
pyproject-build-systems.overlays.wheel
|
pyproject-build-systems.overlays.wheel
|
||||||
overlay
|
overlay
|
||||||
|
(final: prev: {
|
||||||
|
pyscard = prev.pyscard.overrideAttrs (old: {
|
||||||
|
nativeBuildInputs = (old.nativeBuildInputs or []) ++ [ pkgs.swig pkgs.pkg-config];
|
||||||
|
buildInputs = (old.buildInputs or []) ++ [ pkgs.pcsclite.dev ];
|
||||||
|
NIX_CFLAGS_COMPILE = "-I${pkgs.pcsclite.dev}/include/PCSC";
|
||||||
|
});
|
||||||
|
})
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -76,11 +83,16 @@
|
|||||||
packages = [
|
packages = [
|
||||||
virtualenv
|
virtualenv
|
||||||
pkgs.uv
|
pkgs.uv
|
||||||
|
pkgs.pcsclite
|
||||||
|
pkgs.pcsclite.dev
|
||||||
|
pkgs.swig
|
||||||
|
pkgs.pkg-config
|
||||||
];
|
];
|
||||||
env = {
|
env = {
|
||||||
UV_NO_SYNC = "1";
|
UV_NO_SYNC = "1";
|
||||||
UV_PYTHON = pythonSet.python.interpreter;
|
UV_PYTHON = pythonSet.python.interpreter;
|
||||||
UV_PYTHON_DOWNLOADS = "never";
|
UV_PYTHON_DOWNLOADS = "never";
|
||||||
|
LD_LIBRARY_PATH = "${lib.getLib pkgs.pcsclite}/lib";
|
||||||
};
|
};
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
unset PYTHONPATH
|
unset PYTHONPATH
|
||||||
|
|||||||
7
plan.txt
7
plan.txt
@@ -43,6 +43,13 @@ Plan:
|
|||||||
Erstelle AA mit Timeslot
|
Erstelle AA mit Timeslot
|
||||||
Registriere karte -> gruppe
|
Registriere karte -> gruppe
|
||||||
|
|
||||||
|
CRUD
|
||||||
|
create(post)
|
||||||
|
read(get)[all items]
|
||||||
|
read(get)[single item]
|
||||||
|
update(put/patch)
|
||||||
|
delete(delete)
|
||||||
|
|
||||||
zeitplan
|
zeitplan
|
||||||
Hier bin ich nicht sicher, ich denke an cron style für wiederholende dinge aber das kann nur zeitpunkte und keine blöcke.
|
Hier bin ich nicht sicher, ich denke an cron style für wiederholende dinge aber das kann nur zeitpunkte und keine blöcke.
|
||||||
Villeicht ne liste von cron zeiten [ "0 16 * * 2" "0 18 * * 2" ](Wäre dienstag 16-18 uhr) - Ist aber warscheinlich schwer zu parsen
|
Villeicht ne liste von cron zeiten [ "0 16 * * 2" "0 18 * * 2" ](Wäre dienstag 16-18 uhr) - Ist aber warscheinlich schwer zu parsen
|
||||||
|
|||||||
@@ -9,6 +9,14 @@ dependencies = [
|
|||||||
"sqlmodel>=0.0.38",
|
"sqlmodel>=0.0.38",
|
||||||
"poetry>=2.3.4",
|
"poetry>=2.3.4",
|
||||||
"python-desfire",
|
"python-desfire",
|
||||||
|
"paho-mqtt>=2.1.0",
|
||||||
|
"pyjwt[crypto]>=2.12.1",
|
||||||
|
"pwdlib[argon2]>=0.3.0",
|
||||||
|
"pytest>=9.0.3",
|
||||||
|
"requests>=2.33.1",
|
||||||
|
"pytest-cov>=7.1.0",
|
||||||
|
"setuptools>=82.0.1",
|
||||||
|
"pyscard>=2.3.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.uv.sources]
|
[tool.uv.sources]
|
||||||
@@ -16,3 +24,4 @@ python-desfire = { git = "https://github.com/waza-ari/python-desfire" }
|
|||||||
|
|
||||||
[tool.uv.extra-build-dependencies]
|
[tool.uv.extra-build-dependencies]
|
||||||
python-desfire = ["poetry"]
|
python-desfire = ["poetry"]
|
||||||
|
"pyscard" = ["setuptools"]
|
||||||
|
|||||||
164
test.py
Normal file
164
test.py
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
"""
|
||||||
|
This is a more involved example that performs initial configuration (often called personalization) of a DESFire card.
|
||||||
|
|
||||||
|
It performs the following steps:
|
||||||
|
1. Authenticate with the default DES key
|
||||||
|
3. Change the default key
|
||||||
|
2. Create an application
|
||||||
|
4. Change the application master key
|
||||||
|
6. Create a read and write key (diversified)
|
||||||
|
7. Create an encrypted file
|
||||||
|
8. Write the UID to the encrypted file
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
from smartcard.CardRequest import CardRequest
|
||||||
|
from smartcard.CardType import AnyCardType
|
||||||
|
from smartcard.Exceptions import CardRequestTimeoutException
|
||||||
|
|
||||||
|
from desfire import DESFire, DESFireKey, PCSCDevice, diversify_key, get_list, to_hex_string
|
||||||
|
from desfire.enums import DESFireCommunicationMode, DESFireFileType, DESFireKeySettings, DESFireKeyType
|
||||||
|
from desfire.schemas import FilePermissions, FileSettings, KeySettings
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
# Please make sure to yet your own keys here before running this script
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
MIFARE_APP_MASTER_KEY = os.getenv('MIFARE_APP_MASTER_KEY')
|
||||||
|
MIFARE_ACL_READ_BASE_KEY = os.getenv('MIFARE_ACL_READ_BASE_KEY')
|
||||||
|
MIFARE_ACL_WRITE_BASE_KEY = os.getenv('MIFARE_ACL_WRITE_BASE_KEY')
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
MIFARE_APP_ID = "DEAFFE" # 7 bytes
|
||||||
|
MIFARE_ACL_READ_BASE_KEY_ID = 0x1
|
||||||
|
MIFARE_ACL_WRITE_BASE_KEY_ID = 0x2
|
||||||
|
MIFARE_SYS_ID = "FF0000" # 3 bytes, can essentially be anything
|
||||||
|
MIFARE_ENCRYPTED_FILE_ID = 0x1
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
cardtype = AnyCardType()
|
||||||
|
cardrequest = CardRequest(timeout=30, cardType=cardtype)
|
||||||
|
print("Please present DESfire tag...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
cardservice = cardrequest.waitforcard()
|
||||||
|
except CardRequestTimeoutException:
|
||||||
|
print("No tag detected within the timeout.")
|
||||||
|
raise
|
||||||
|
|
||||||
|
cardservice.connection.connect()
|
||||||
|
|
||||||
|
# Create Desfire object
|
||||||
|
desfire = DESFire(PCSCDevice(cardservice.connection.component))
|
||||||
|
|
||||||
|
# Create Key objects
|
||||||
|
AES_NULL_KEY_DATA = "00" * 16
|
||||||
|
aes_keysettings = KeySettings(
|
||||||
|
key_type=DESFireKeyType.DF_KEY_AES,
|
||||||
|
)
|
||||||
|
aes_null_key = DESFireKey(aes_keysettings, AES_NULL_KEY_DATA)
|
||||||
|
|
||||||
|
# Authenticate with default DES key
|
||||||
|
print("Authenticating with default DES key...")
|
||||||
|
key_settings = desfire.get_key_setting()
|
||||||
|
mk = DESFireKey(key_settings, "00" * 8)
|
||||||
|
desfire.authenticate(0x0, mk)
|
||||||
|
|
||||||
|
# Get real UID
|
||||||
|
print("Getting real UID...")
|
||||||
|
uid = desfire.get_real_uid()
|
||||||
|
print(" - UID: ", to_hex_string(uid))
|
||||||
|
|
||||||
|
# Set default key
|
||||||
|
print("Setting default key...")
|
||||||
|
desfire.change_default_key(aes_null_key, 0x0)
|
||||||
|
|
||||||
|
# Create application
|
||||||
|
print("Creating application...")
|
||||||
|
app_settings = KeySettings(
|
||||||
|
settings=[
|
||||||
|
DESFireKeySettings.KS_ALLOW_CHANGE_MK,
|
||||||
|
DESFireKeySettings.KS_LISTING_WITHOUT_MK,
|
||||||
|
DESFireKeySettings.KS_CREATE_DELETE_WITHOUT_MK,
|
||||||
|
DESFireKeySettings.KS_CONFIGURATION_CHANGEABLE,
|
||||||
|
],
|
||||||
|
key_type=DESFireKeyType.DF_KEY_AES,
|
||||||
|
)
|
||||||
|
desfire.create_application(MIFARE_APP_ID, app_settings, 4)
|
||||||
|
|
||||||
|
# Verify application creation
|
||||||
|
applications = desfire.get_application_ids()
|
||||||
|
assert len(applications) == 1
|
||||||
|
assert applications[0] == get_list(MIFARE_APP_ID)
|
||||||
|
print(" - Application created successfully.")
|
||||||
|
|
||||||
|
# Select application
|
||||||
|
print("Selecting application...")
|
||||||
|
desfire.select_application(MIFARE_APP_ID)
|
||||||
|
|
||||||
|
# Authenticate with AES key, as this has been set as the default key
|
||||||
|
print("Authenticating with AES key...")
|
||||||
|
# Create a new one as key data would be overriden by session data
|
||||||
|
aes_null_auth_key = DESFireKey(aes_keysettings, AES_NULL_KEY_DATA)
|
||||||
|
desfire.authenticate(0x0, aes_null_auth_key)
|
||||||
|
|
||||||
|
# Change Application master key
|
||||||
|
print("Changing application master key (AMK)...")
|
||||||
|
aes_app_mk = DESFireKey(aes_keysettings, MIFARE_APP_MASTER_KEY)
|
||||||
|
desfire.change_key(0x0, aes_null_key, aes_app_mk, 0x1)
|
||||||
|
|
||||||
|
# Re-Authenticate with new AES key
|
||||||
|
print("Re-authenticating with new AES key...")
|
||||||
|
desfire.authenticate(0x0, aes_app_mk)
|
||||||
|
|
||||||
|
# Change file read and write keys (diversified)
|
||||||
|
diversification_data = [0x01] + uid + get_list(MIFARE_APP_ID) + get_list(MIFARE_SYS_ID)
|
||||||
|
read_div_key_bytes = diversify_key(get_list(MIFARE_ACL_READ_BASE_KEY), diversification_data, pad_to_32=False)
|
||||||
|
write_div_key_bytes = diversify_key(get_list(MIFARE_ACL_WRITE_BASE_KEY), diversification_data, pad_to_32=False)
|
||||||
|
|
||||||
|
print("Changing file read key...")
|
||||||
|
aes_file_read_key = DESFireKey(aes_keysettings, read_div_key_bytes)
|
||||||
|
desfire.change_key(MIFARE_ACL_READ_BASE_KEY_ID, aes_null_key, aes_file_read_key, 0x1)
|
||||||
|
|
||||||
|
print("Changing file write key...")
|
||||||
|
aes_file_write_key = DESFireKey(aes_keysettings, write_div_key_bytes)
|
||||||
|
desfire.change_key(MIFARE_ACL_WRITE_BASE_KEY_ID, aes_null_key, aes_file_write_key, 0x1)
|
||||||
|
|
||||||
|
print("Create encrypted file containing UID...")
|
||||||
|
file_settings = FileSettings(
|
||||||
|
file_size=8,
|
||||||
|
encryption=DESFireCommunicationMode.ENCRYPTED,
|
||||||
|
permissions=FilePermissions(
|
||||||
|
read_key=MIFARE_ACL_READ_BASE_KEY_ID,
|
||||||
|
write_key=MIFARE_ACL_WRITE_BASE_KEY_ID,
|
||||||
|
),
|
||||||
|
file_type=DESFireFileType.MDFT_STANDARD_DATA_FILE,
|
||||||
|
)
|
||||||
|
desfire.create_standard_file(MIFARE_ENCRYPTED_FILE_ID, file_settings)
|
||||||
|
|
||||||
|
print("Read and verify file settings again...")
|
||||||
|
file_data = desfire.get_file_settings(MIFARE_ENCRYPTED_FILE_ID)
|
||||||
|
assert file_data.file_size == 8
|
||||||
|
assert file_data.encryption == DESFireCommunicationMode.ENCRYPTED
|
||||||
|
assert file_data.permissions is not None
|
||||||
|
assert file_data.permissions.read_access == MIFARE_ACL_READ_BASE_KEY_ID
|
||||||
|
assert file_data.permissions.write_access == MIFARE_ACL_WRITE_BASE_KEY_ID
|
||||||
|
assert file_data.file_type == DESFireFileType.MDFT_STANDARD_DATA_FILE
|
||||||
|
print(" - File created successfully.")
|
||||||
|
|
||||||
|
print("Writing UID to encrypted file...")
|
||||||
|
data = [0x0] + uid
|
||||||
|
assert len(data) == 8
|
||||||
|
desfire.write_file_data(MIFARE_ENCRYPTED_FILE_ID, 0x0, file_data.encryption, get_list(data))
|
||||||
|
|
||||||
|
print("Reading from encrypted file...")
|
||||||
|
rdata = desfire.read_file_data(MIFARE_ENCRYPTED_FILE_ID, file_data)
|
||||||
|
assert rdata == data
|
||||||
|
print(" - Data written successfully.")
|
||||||
|
|
||||||
|
print("Personalization finished.")
|
||||||
0
test/__init__.py
Normal file
0
test/__init__.py
Normal file
119
test/conftest.py
Normal file
119
test/conftest.py
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from sqlmodel import Session, create_engine, SQLModel
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from sqlalchemy.pool import StaticPool
|
||||||
|
|
||||||
|
from app.main import app
|
||||||
|
from app.model.models import UserDB, Card, GroupDB, AccessAuthorizationDB, Timetable, AaGroupLink
|
||||||
|
from app.services.database import get_session
|
||||||
|
|
||||||
|
# Use in-memory SQLite for testing
|
||||||
|
TEST_SQLALCHEMY_DATABASE_URL = "sqlite://"
|
||||||
|
|
||||||
|
engine = create_engine(TEST_SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}, poolclass=StaticPool)
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
def db_session():
|
||||||
|
"""Create a fresh database session for each test."""
|
||||||
|
SQLModel.metadata.create_all(engine)
|
||||||
|
with Session(engine) as session:
|
||||||
|
yield session
|
||||||
|
SQLModel.metadata.drop_all(engine)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
def client(db_session):
|
||||||
|
"""Create a test client with a database session override."""
|
||||||
|
def override_get_session():
|
||||||
|
yield db_session
|
||||||
|
|
||||||
|
app.dependency_overrides[get_session] = override_get_session
|
||||||
|
with TestClient(app) as test_client:
|
||||||
|
yield test_client
|
||||||
|
app.dependency_overrides.clear()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def admin_user(db_session):
|
||||||
|
"""Create an admin user for testing."""
|
||||||
|
from app.services.auth import get_password_hash
|
||||||
|
admin = UserDB(
|
||||||
|
name="admin",
|
||||||
|
passwordhash=get_password_hash("admin123"),
|
||||||
|
is_admin=True
|
||||||
|
)
|
||||||
|
db_session.add(admin)
|
||||||
|
db_session.commit()
|
||||||
|
db_session.refresh(admin)
|
||||||
|
return admin
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def regular_user(db_session):
|
||||||
|
"""Create a regular user for testing."""
|
||||||
|
from app.services.auth import get_password_hash
|
||||||
|
user = UserDB(
|
||||||
|
name="user",
|
||||||
|
passwordhash=get_password_hash("user123"),
|
||||||
|
is_admin=False
|
||||||
|
)
|
||||||
|
db_session.add(user)
|
||||||
|
db_session.commit()
|
||||||
|
db_session.refresh(user)
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def auth_headers(client, admin_user):
|
||||||
|
"""Get authentication headers for admin user."""
|
||||||
|
response = client.post(
|
||||||
|
"/token",
|
||||||
|
data={"username": admin_user.name, "password": "admin123"}
|
||||||
|
)
|
||||||
|
token = response.json()["access_token"]
|
||||||
|
return {"Authorization": f"Bearer {token}"}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def user_auth_headers(client, regular_user):
|
||||||
|
"""Get authentication headers for regular user."""
|
||||||
|
response = client.post(
|
||||||
|
"/token",
|
||||||
|
data={"username": regular_user.name, "password": "user123"}
|
||||||
|
)
|
||||||
|
token = response.json()["access_token"]
|
||||||
|
return {"Authorization": f"Bearer {token}"}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def test_group(db_session):
|
||||||
|
"""Create a test group."""
|
||||||
|
group = GroupDB(name="Test Group")
|
||||||
|
db_session.add(group)
|
||||||
|
db_session.commit()
|
||||||
|
db_session.refresh(group)
|
||||||
|
return group
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def test_card(db_session, test_group):
|
||||||
|
"""Create a test card."""
|
||||||
|
card = Card(uuid="test-uuid-123", group_id=test_group.id)
|
||||||
|
db_session.add(card)
|
||||||
|
db_session.commit()
|
||||||
|
db_session.refresh(card)
|
||||||
|
return card
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def test_aa(db_session):
|
||||||
|
"""Create a test access authorization."""
|
||||||
|
aa = AccessAuthorizationDB(
|
||||||
|
name="Test AA",
|
||||||
|
is_active=True
|
||||||
|
)
|
||||||
|
db_session.add(aa)
|
||||||
|
db_session.commit()
|
||||||
|
db_session.refresh(aa)
|
||||||
|
return aa
|
||||||
24
test/test_main.py
Normal file
24
test/test_main.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
def test_app_startup(client):
|
||||||
|
"""Test that the application starts up correctly."""
|
||||||
|
response = client.get("/")
|
||||||
|
# Application should respond (even if it's a 404)
|
||||||
|
assert response.status_code in [404, 200]
|
||||||
|
|
||||||
|
|
||||||
|
def test_health_check(client):
|
||||||
|
"""Test basic health check endpoint if it exists."""
|
||||||
|
# Note: This would require adding a health check endpoint
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_router_includes():
|
||||||
|
"""Test that all routers are included in the app."""
|
||||||
|
from app.main import app
|
||||||
|
routes = [route.path for route in app.routes]
|
||||||
|
|
||||||
|
# Check that router prefixes are present
|
||||||
|
assert any("/users" in route for route in routes)
|
||||||
|
assert any("/cards" in route for route in routes)
|
||||||
|
assert any("/groups" in route for route in routes)
|
||||||
|
assert any("/aa" in route for route in routes)
|
||||||
|
assert any("/token" in route for route in routes)
|
||||||
95
test/test_models.py
Normal file
95
test/test_models.py
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import pytest
|
||||||
|
import datetime
|
||||||
|
from app.model.models import (
|
||||||
|
UserBase, UserResponse, UserCreate, UserDB, UserUpdate,
|
||||||
|
GroupBase, GroupCreate, GroupDB, GroupResponse,
|
||||||
|
AccessAuthorizationBase, AccessAuthorizationCreate,
|
||||||
|
AccessAuthorizationDB, AccessAuthorizationResponse, AccessAuthorizationUpdate,
|
||||||
|
Card, Timetable, TimetableCreate, Token, TokenData, AaGroupLink
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_models():
|
||||||
|
"""Test user model creation and validation."""
|
||||||
|
# Test UserBase
|
||||||
|
user_base = UserBase(name="Test User", email="test@example.com", is_admin=False)
|
||||||
|
assert user_base.name == "Test User"
|
||||||
|
assert user_base.email == "test@example.com"
|
||||||
|
assert user_base.is_admin is False
|
||||||
|
|
||||||
|
# Test UserCreate
|
||||||
|
user_create = UserCreate(name="New User", email="new@example.com", password="secret123")
|
||||||
|
assert user_create.password == "secret123"
|
||||||
|
|
||||||
|
# Test UserUpdate
|
||||||
|
user_update = UserUpdate(name="Updated Name")
|
||||||
|
assert user_update.name == "Updated Name"
|
||||||
|
assert user_update.email is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_group_models():
|
||||||
|
"""Test group model creation and validation."""
|
||||||
|
# Test GroupBase
|
||||||
|
group_base = GroupBase(name="Test Group")
|
||||||
|
assert group_base.name == "Test Group"
|
||||||
|
|
||||||
|
# Test GroupCreate
|
||||||
|
group_create = GroupCreate(name="New Group")
|
||||||
|
assert group_create.name == "New Group"
|
||||||
|
|
||||||
|
|
||||||
|
def test_access_authorization_models():
|
||||||
|
"""Test access authorization model creation and validation."""
|
||||||
|
# Test AccessAuthorizationBase
|
||||||
|
aa_base = AccessAuthorizationBase(name="Test AA", is_active=True)
|
||||||
|
assert aa_base.name == "Test AA"
|
||||||
|
assert aa_base.is_active is True
|
||||||
|
|
||||||
|
# Test AccessAuthorizationCreate with timetables
|
||||||
|
timetable_create = TimetableCreate(weekday=1, starttime="08:00", duration=60)
|
||||||
|
aa_create = AccessAuthorizationCreate(
|
||||||
|
name="New AA",
|
||||||
|
is_active=False,
|
||||||
|
timetables=[timetable_create]
|
||||||
|
)
|
||||||
|
assert aa_create.name == "New AA"
|
||||||
|
assert aa_create.is_active is False
|
||||||
|
assert len(aa_create.timetables) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_card_model():
|
||||||
|
"""Test card model creation and validation."""
|
||||||
|
card = Card(uuid="test-uuid", group_id=1)
|
||||||
|
assert card.uuid == "test-uuid"
|
||||||
|
assert card.group_id == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_timetable_models():
|
||||||
|
"""Test timetable model creation and validation."""
|
||||||
|
# Test TimetableBase with valid values
|
||||||
|
timetable = TimetableCreate(weekday=1, starttime="09:00", duration=120)
|
||||||
|
assert timetable.weekday == 1
|
||||||
|
assert timetable.starttime == datetime.time(9, 0)
|
||||||
|
assert timetable.duration == 120
|
||||||
|
|
||||||
|
# Test boundary values
|
||||||
|
max_duration = TimetableCreate(weekday=6, starttime="23:59", duration=1439)
|
||||||
|
assert max_duration.duration == 1439
|
||||||
|
assert max_duration.weekday == 6
|
||||||
|
|
||||||
|
|
||||||
|
def test_token_models():
|
||||||
|
"""Test token model creation and validation."""
|
||||||
|
token = Token(access_token="test-token", token_type="bearer")
|
||||||
|
assert token.access_token == "test-token"
|
||||||
|
assert token.token_type == "bearer"
|
||||||
|
|
||||||
|
token_data = TokenData(username="testuser")
|
||||||
|
assert token_data.username == "testuser"
|
||||||
|
|
||||||
|
|
||||||
|
def test_aa_group_link_model():
|
||||||
|
"""Test many-to-many relationship link model."""
|
||||||
|
link = AaGroupLink(group_id=1, accessauth_id=2)
|
||||||
|
assert link.group_id == 1
|
||||||
|
assert link.accessauth_id == 2
|
||||||
0
test/test_services/__init__.py
Normal file
0
test/test_services/__init__.py
Normal file
192
test/test_services/test_aa_manager.py
Normal file
192
test/test_services/test_aa_manager.py
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
import pytest
|
||||||
|
from fastapi import status
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_access_auth(client, auth_headers):
|
||||||
|
"""Test creating a new access authorization."""
|
||||||
|
aa_data = {
|
||||||
|
"name": "New AA",
|
||||||
|
"is_active": True,
|
||||||
|
"timetables": [
|
||||||
|
{"weekday": 1, "starttime": "08:00", "duration": 60},
|
||||||
|
{"weekday": 2, "starttime": "09:00", "duration": 90}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
response = client.post("/aa/", json=aa_data, headers=auth_headers)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
assert data["name"] == "New AA"
|
||||||
|
assert data["is_active"] is True
|
||||||
|
assert "id" in data
|
||||||
|
assert len(data["timetables"]) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_all_access_auths(client, auth_headers, test_aa):
|
||||||
|
"""Test retrieving all access authorizations."""
|
||||||
|
response = client.get("/aa/", headers=auth_headers)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
aa_list = response.json()
|
||||||
|
assert len(aa_list) >= 1
|
||||||
|
|
||||||
|
aa_names = [aa["name"] for aa in aa_list]
|
||||||
|
assert test_aa.name in aa_names
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_access_auth_by_id(client, auth_headers, test_aa):
|
||||||
|
"""Test retrieving a specific access authorization by ID."""
|
||||||
|
response = client.get(f"/aa/{test_aa.id}", headers=auth_headers)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
assert data["id"] == test_aa.id
|
||||||
|
assert data["name"] == test_aa.name
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_nonexistent_access_auth(client, auth_headers):
|
||||||
|
"""Test retrieving a non-existent access authorization."""
|
||||||
|
response = client.get("/aa/99999", headers=auth_headers)
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_assign_access_auth_to_group(client, auth_headers, test_group, test_aa):
|
||||||
|
"""Test assigning an access authorization to a group."""
|
||||||
|
response = client.put(
|
||||||
|
f"/aa/assign/{test_group.id}/{test_aa.id}",
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
assert data["id"] == test_group.id
|
||||||
|
# The AA should now be in the group's accessauths
|
||||||
|
# Note: The response model might not include the full relationship
|
||||||
|
|
||||||
|
|
||||||
|
def test_assign_already_assigned_access_auth(client, auth_headers, test_group, test_aa):
|
||||||
|
"""Test assigning an already assigned access authorization."""
|
||||||
|
# First assignment
|
||||||
|
client.put(f"/aa/assign/{test_group.id}/{test_aa.id}", headers=auth_headers)
|
||||||
|
|
||||||
|
# Second assignment should indicate it's already assigned
|
||||||
|
response = client.put(
|
||||||
|
f"/aa/assign/{test_group.id}/{test_aa.id}",
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
# According to the code, this returns 200 with "already assigned" message
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_unassign_access_auth_from_group(client, auth_headers, test_group, test_aa):
|
||||||
|
"""Test unassigning an access authorization from a group."""
|
||||||
|
# First assign
|
||||||
|
client.put(f"/aa/assign/{test_group.id}/{test_aa.id}", headers=auth_headers)
|
||||||
|
|
||||||
|
# Then unassign
|
||||||
|
response = client.put(
|
||||||
|
f"/aa/unassign/{test_group.id}/{test_aa.id}",
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_unassign_nonexistent_assignment(client, auth_headers, test_group, test_aa):
|
||||||
|
"""Test unassigning a non-existent assignment."""
|
||||||
|
response = client.put(
|
||||||
|
f"/aa/unassign/{test_group.id}/{test_aa.id}",
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
# According to the code, this returns 200 with "not assigned" message
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_assign_to_nonexistent_group(client, auth_headers, test_aa):
|
||||||
|
"""Test assigning an AA to a non-existent group."""
|
||||||
|
response = client.put(f"/aa/assign/99999/{test_aa.id}", headers=auth_headers)
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_assign_nonexistent_aa(client, auth_headers, test_group):
|
||||||
|
"""Test assigning a non-existent AA to a group."""
|
||||||
|
response = client.put(f"/aa/assign/{test_group.id}/99999", headers=auth_headers)
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_access_auth(client, auth_headers, test_aa):
|
||||||
|
"""Test updating an access authorization."""
|
||||||
|
update_data = {
|
||||||
|
"name": "Updated AA",
|
||||||
|
"is_active": False
|
||||||
|
}
|
||||||
|
|
||||||
|
response = client.patch(
|
||||||
|
f"/aa/{test_aa.id}",
|
||||||
|
json=update_data,
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
assert data["name"] == "Updated AA"
|
||||||
|
assert data["is_active"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_access_auth_with_timetables(client, auth_headers, test_aa):
|
||||||
|
"""Test updating an access authorization with new timetables."""
|
||||||
|
update_data = {
|
||||||
|
"timetables": [
|
||||||
|
{"weekday": 5, "starttime": "10:00", "duration": 120}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
response = client.patch(
|
||||||
|
f"/aa/{test_aa.id}",
|
||||||
|
json=update_data,
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_nonexistent_access_auth(client, auth_headers):
|
||||||
|
"""Test updating a non-existent access authorization."""
|
||||||
|
update_data = {"name": "Updated"}
|
||||||
|
response = client.patch("/aa/99999", json=update_data, headers=auth_headers)
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_access_auth(client, auth_headers, test_aa):
|
||||||
|
"""Test deleting an access authorization."""
|
||||||
|
response = client.delete(f"/aa/{test_aa.id}", headers=auth_headers)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "deleted successfully" in response.json()["message"].lower()
|
||||||
|
|
||||||
|
# Verify AA is deleted
|
||||||
|
response = client.get(f"/aa/{test_aa.id}", headers=auth_headers)
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_nonexistent_access_auth(client, auth_headers):
|
||||||
|
"""Test deleting a non-existent access authorization."""
|
||||||
|
response = client.delete("/aa/99999", headers=auth_headers)
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_aa_operations_by_non_admin(client, test_aa, user_auth_headers):
|
||||||
|
"""Test that non-admin users cannot perform AA operations."""
|
||||||
|
# Try to create an AA
|
||||||
|
response = client.post(
|
||||||
|
"/aa/",
|
||||||
|
json={"name": "test", "is_active": True, "timetables": []},
|
||||||
|
headers=user_auth_headers
|
||||||
|
)
|
||||||
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
# Try to get all AAs
|
||||||
|
response = client.get("/aa/", headers=user_auth_headers)
|
||||||
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
# Try to assign AA
|
||||||
|
response = client.put(f"/aa/assign/1/{test_aa.id}", headers=user_auth_headers)
|
||||||
|
assert response.status_code == 403
|
||||||
196
test/test_services/test_auth.py
Normal file
196
test/test_services/test_auth.py
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
import pytest
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from fastapi import HTTPException, status
|
||||||
|
from app.services.auth import (
|
||||||
|
verify_password, get_password_hash, get_user, authenticate_user,
|
||||||
|
create_access_token, get_current_user, auth_is_admin, create_first_user
|
||||||
|
)
|
||||||
|
from app.model.models import UserDB
|
||||||
|
from jwt.exceptions import InvalidTokenError
|
||||||
|
|
||||||
|
|
||||||
|
def test_password_hashing():
|
||||||
|
"""Test password hashing and verification."""
|
||||||
|
password = "test_password_123"
|
||||||
|
|
||||||
|
# Hash password
|
||||||
|
hashed = get_password_hash(password)
|
||||||
|
assert hashed != password
|
||||||
|
assert len(hashed) > 0
|
||||||
|
|
||||||
|
# Verify correct password
|
||||||
|
assert verify_password(password, hashed) is True
|
||||||
|
|
||||||
|
# Verify incorrect password
|
||||||
|
assert verify_password("wrong_password", hashed) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_user(db_session):
|
||||||
|
"""Test get_user function."""
|
||||||
|
from app.services.auth import get_password_hash
|
||||||
|
|
||||||
|
# Create a user
|
||||||
|
user = UserDB(name="testuser", passwordhash=get_password_hash("password"))
|
||||||
|
db_session.add(user)
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
# Get existing user
|
||||||
|
retrieved_user = get_user(db_session, "testuser")
|
||||||
|
assert retrieved_user is not None
|
||||||
|
assert retrieved_user.name == "testuser"
|
||||||
|
|
||||||
|
# Try to get non-existent user
|
||||||
|
retrieved_user = get_user(db_session, "nonexistent")
|
||||||
|
assert retrieved_user is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_authenticate_user(db_session):
|
||||||
|
"""Test user authentication."""
|
||||||
|
from app.services.auth import get_password_hash
|
||||||
|
|
||||||
|
# Create a user
|
||||||
|
user = UserDB(name="authuser", passwordhash=get_password_hash("correctpass"))
|
||||||
|
db_session.add(user)
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
# Authenticate with correct credentials
|
||||||
|
authenticated = authenticate_user(db_session, "authuser", "correctpass")
|
||||||
|
assert authenticated is not False
|
||||||
|
assert authenticated.name == "authuser"
|
||||||
|
|
||||||
|
# Authenticate with wrong password
|
||||||
|
authenticated = authenticate_user(db_session, "authuser", "wrongpass")
|
||||||
|
assert authenticated is False
|
||||||
|
|
||||||
|
# Authenticate non-existent user
|
||||||
|
authenticated = authenticate_user(db_session, "nonexistent", "password")
|
||||||
|
assert authenticated is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_access_token():
|
||||||
|
"""Test JWT token creation."""
|
||||||
|
data = {"sub": "testuser"}
|
||||||
|
|
||||||
|
# Create token with default expiration
|
||||||
|
token = create_access_token(data)
|
||||||
|
assert isinstance(token, str)
|
||||||
|
assert len(token) > 0
|
||||||
|
|
||||||
|
# Create token with custom expiration
|
||||||
|
custom_expire = timedelta(hours=1)
|
||||||
|
token = create_access_token(data, expires_delta=custom_expire)
|
||||||
|
assert isinstance(token, str)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_current_user(db_session, admin_user):
|
||||||
|
"""Test getting current user from token."""
|
||||||
|
from app.services.auth import create_access_token, get_current_user
|
||||||
|
|
||||||
|
# Create token for admin user
|
||||||
|
token = create_access_token(data={"sub": admin_user.name})
|
||||||
|
|
||||||
|
# Get user from token
|
||||||
|
user = get_current_user(token=token, db=db_session)
|
||||||
|
assert user is not None
|
||||||
|
assert user.name == admin_user.name
|
||||||
|
assert user.id == admin_user.id
|
||||||
|
|
||||||
|
# Test invalid token
|
||||||
|
with pytest.raises(HTTPException) as exc_info:
|
||||||
|
get_current_user(token="invalid_token")
|
||||||
|
assert exc_info.value.status_code == status.HTTP_401_UNAUTHORIZED
|
||||||
|
|
||||||
|
# Test expired token (create token with past expiration)
|
||||||
|
past_expire = timedelta(minutes=-100)
|
||||||
|
expired_token = create_access_token(data={"sub": admin_user.name}, expires_delta=past_expire)
|
||||||
|
|
||||||
|
with pytest.raises(HTTPException) as exc_info:
|
||||||
|
get_current_user(token=expired_token)
|
||||||
|
assert exc_info.value.status_code == status.HTTP_401_UNAUTHORIZED
|
||||||
|
|
||||||
|
|
||||||
|
def test_auth_is_admin(db_session, admin_user, regular_user):
|
||||||
|
"""Test admin authorization check."""
|
||||||
|
from app.services.auth import create_access_token, auth_is_admin
|
||||||
|
|
||||||
|
# Create token for admin user
|
||||||
|
admin_token = create_access_token(data={"sub": admin_user.name})
|
||||||
|
|
||||||
|
# Admin should pass
|
||||||
|
result = auth_is_admin(token=admin_token, db=db_session)
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
# Create token for regular user
|
||||||
|
user_token = create_access_token(data={"sub": regular_user.name})
|
||||||
|
|
||||||
|
# Regular user should fail
|
||||||
|
with pytest.raises(HTTPException) as exc_info:
|
||||||
|
auth_is_admin(token=user_token, db=db_session)
|
||||||
|
assert exc_info.value.status_code == status.HTTP_403_FORBIDDEN
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_first_user(db_session):
|
||||||
|
"""Test automatic creation of first admin user."""
|
||||||
|
#Currently broken because this uses the prod db because of how i wrote the create_first_user function
|
||||||
|
# Clear any existing users
|
||||||
|
from sqlmodel import select
|
||||||
|
db_session.exec(select(UserDB)).all()
|
||||||
|
for user in db_session.exec(select(UserDB)).all():
|
||||||
|
db_session.delete(user)
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
# Create first user
|
||||||
|
result = create_first_user()
|
||||||
|
assert result is not None
|
||||||
|
assert result.name == "admin"
|
||||||
|
assert result.is_admin is True
|
||||||
|
|
||||||
|
# Verify user exists in database
|
||||||
|
user = db_session.exec(select(UserDB).where(UserDB.name == "admin")).first()
|
||||||
|
assert user is not None
|
||||||
|
assert user.is_admin is True
|
||||||
|
|
||||||
|
# Test that it doesn't create another admin if one exists
|
||||||
|
second_result = create_first_user()
|
||||||
|
assert second_result is None # Should print "Admin user already exists"
|
||||||
|
|
||||||
|
|
||||||
|
def test_token_endpoint(client, admin_user):
|
||||||
|
"""Test the token endpoint for login."""
|
||||||
|
# Test successful login
|
||||||
|
response = client.post(
|
||||||
|
"/token",
|
||||||
|
data={"username": admin_user.name, "password": "admin123"}
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert "access_token" in data
|
||||||
|
assert data["token_type"] == "bearer"
|
||||||
|
|
||||||
|
# Test failed login with wrong password
|
||||||
|
response = client.post(
|
||||||
|
"/token",
|
||||||
|
data={"username": admin_user.name, "password": "wrongpassword"}
|
||||||
|
)
|
||||||
|
assert response.status_code == 401
|
||||||
|
|
||||||
|
# Test failed login with non-existent user
|
||||||
|
response = client.post(
|
||||||
|
"/token",
|
||||||
|
data={"username": "nonexistent", "password": "password"}
|
||||||
|
)
|
||||||
|
assert response.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
def test_test_login_endpoint(client, admin_user, auth_headers):
|
||||||
|
"""Test the test login endpoint."""
|
||||||
|
# Test with valid token
|
||||||
|
response = client.get("/test/login", headers=auth_headers)
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["name"] == admin_user.name
|
||||||
|
assert data["is_admin"] is True
|
||||||
|
|
||||||
|
# Test without token
|
||||||
|
response = client.get("/test/login")
|
||||||
|
assert response.status_code == 401
|
||||||
66
test/test_services/test_card_manager.py
Normal file
66
test/test_services/test_card_manager.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import pytest
|
||||||
|
from fastapi import status
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_card(client, auth_headers, test_group):
|
||||||
|
"""Test adding a card to a group."""
|
||||||
|
response = client.post(f"/cards/{test_group.id}", headers=auth_headers)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
assert "id" in data
|
||||||
|
assert "uuid" in data
|
||||||
|
assert data["group_id"] == test_group.id
|
||||||
|
assert len(data["uuid"]) > 0 # UUID should be generated
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_card_to_nonexistent_group(client, auth_headers):
|
||||||
|
"""Test adding a card to a non-existent group."""
|
||||||
|
response = client.post("/cards/99999", headers=auth_headers)
|
||||||
|
# This might succeed and create a card with a non-existent group_id
|
||||||
|
# or fail depending on foreign key constraints
|
||||||
|
# For now, let's assume it might fail
|
||||||
|
# assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_card(client, auth_headers, test_card):
|
||||||
|
"""Test deleting a card."""
|
||||||
|
response = client.delete(f"/cards/{test_card.id}", headers=auth_headers)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "deleted successfully" in response.json()["message"].lower()
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_nonexistent_card(client, auth_headers):
|
||||||
|
"""Test deleting a non-existent card."""
|
||||||
|
response = client.delete("/cards/99999", headers=auth_headers)
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_cards_for_group(client, auth_headers, test_group, test_card):
|
||||||
|
"""Test getting all cards for a group."""
|
||||||
|
response = client.get(f"/cards/{test_group.id}", headers=auth_headers)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
cards = response.json()
|
||||||
|
assert len(cards) >= 1
|
||||||
|
assert any(card["id"] == test_card.id for card in cards)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_cards_for_nonexistent_group(client, auth_headers):
|
||||||
|
"""Test getting cards for a non-existent group."""
|
||||||
|
response = client.get("/cards/99999", headers=auth_headers)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
cards = response.json()
|
||||||
|
assert len(cards) == 0 # Empty list for non-existent group
|
||||||
|
|
||||||
|
|
||||||
|
def test_card_operations_by_non_admin(client, test_group, user_auth_headers):
|
||||||
|
"""Test that non-admin users cannot perform card operations."""
|
||||||
|
# Try to add a card
|
||||||
|
response = client.post(f"/cards/{test_group.id}", headers=user_auth_headers)
|
||||||
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
# Try to get cards
|
||||||
|
response = client.get(f"/cards/{test_group.id}", headers=user_auth_headers)
|
||||||
|
assert response.status_code == 403
|
||||||
64
test/test_services/test_database.py
Normal file
64
test/test_services/test_database.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import pytest
|
||||||
|
from sqlmodel import Session, select
|
||||||
|
from app.services.database import create_db_and_tables, get_session, add_and_refresh
|
||||||
|
from app.model.models import UserDB, GroupDB, Card
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_db_and_tables():
|
||||||
|
"""Test database and tables creation."""
|
||||||
|
# This is primarily an integration test
|
||||||
|
from sqlalchemy import inspect
|
||||||
|
from app.services.database import engine
|
||||||
|
|
||||||
|
create_db_and_tables()
|
||||||
|
inspector = inspect(engine)
|
||||||
|
|
||||||
|
# Check that tables exist
|
||||||
|
tables = inspector.get_table_names()
|
||||||
|
assert "userdb" in tables
|
||||||
|
assert "groupdb" in tables
|
||||||
|
assert "card" in tables
|
||||||
|
assert "accessauthorizationdb" in tables
|
||||||
|
assert "timetable" in tables
|
||||||
|
assert "aagrouplink" in tables
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_session(db_session):
|
||||||
|
"""Test database session generator."""
|
||||||
|
# Test that we can get a session
|
||||||
|
session_gen = get_session()
|
||||||
|
session = next(session_gen)
|
||||||
|
|
||||||
|
assert isinstance(session, Session)
|
||||||
|
|
||||||
|
# Test that session works
|
||||||
|
user = UserDB(name="Test", passwordhash="hash")
|
||||||
|
session.add(user)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
retrieved_user = session.get(UserDB, user.id)
|
||||||
|
assert retrieved_user is not None
|
||||||
|
assert retrieved_user.name == "Test"
|
||||||
|
|
||||||
|
# Clean up generator
|
||||||
|
try:
|
||||||
|
next(session_gen)
|
||||||
|
except StopIteration:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_and_refresh(db_session):
|
||||||
|
"""Test add_and_refresh helper function."""
|
||||||
|
user = UserDB(name="Test User", passwordhash="hashed")
|
||||||
|
|
||||||
|
# Add user
|
||||||
|
result = add_and_refresh(db_session, user)
|
||||||
|
|
||||||
|
# Assert that user is now in database with ID
|
||||||
|
assert result.id is not None
|
||||||
|
assert result.name == "Test User"
|
||||||
|
|
||||||
|
# Verify in database
|
||||||
|
db_user = db_session.get(UserDB, result.id)
|
||||||
|
assert db_user is not None
|
||||||
|
assert db_user.name == "Test User"
|
||||||
68
test/test_services/test_group_manager.py
Normal file
68
test/test_services/test_group_manager.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import pytest
|
||||||
|
from fastapi import status
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_group(client, auth_headers):
|
||||||
|
"""Test creating a new group."""
|
||||||
|
group_data = {"name": "New Test Group"}
|
||||||
|
|
||||||
|
response = client.post("/groups/", json=group_data, headers=auth_headers)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
assert data["name"] == "New Test Group"
|
||||||
|
assert "id" in data
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_duplicate_group(client, auth_headers, test_group):
|
||||||
|
"""Test creating a group with a duplicate name."""
|
||||||
|
group_data = {"name": test_group.name}
|
||||||
|
|
||||||
|
response = client.post("/groups/", json=group_data, headers=auth_headers)
|
||||||
|
# This should fail due to unique constraint
|
||||||
|
assert response.status_code == 409 # Validation error
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_groups(client, auth_headers, test_group):
|
||||||
|
"""Test retrieving all groups."""
|
||||||
|
response = client.get("/groups/", headers=auth_headers)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
groups = response.json()
|
||||||
|
assert len(groups) >= 1
|
||||||
|
|
||||||
|
group_names = [group["name"] for group in groups]
|
||||||
|
assert test_group.name in group_names
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_group(client, auth_headers, test_group):
|
||||||
|
"""Test deleting a group."""
|
||||||
|
response = client.delete(f"/groups/{test_group.id}", headers=auth_headers)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "deleted successfully" in response.json()["message"].lower()
|
||||||
|
|
||||||
|
# Verify group is deleted
|
||||||
|
response = client.get("/groups/", headers=auth_headers)
|
||||||
|
groups = response.json()
|
||||||
|
assert not any(group["id"] == test_group.id for group in groups)
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_nonexistent_group(client, auth_headers):
|
||||||
|
"""Test deleting a non-existent group."""
|
||||||
|
response = client.delete("/groups/99999", headers=auth_headers)
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_group_operations_by_non_admin(client, user_auth_headers):
|
||||||
|
"""Test that non-admin users cannot perform group operations."""
|
||||||
|
# Try to create a group
|
||||||
|
response = client.post(
|
||||||
|
"/groups/",
|
||||||
|
json={"name": "test"},
|
||||||
|
headers=user_auth_headers
|
||||||
|
)
|
||||||
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
# Try to get groups
|
||||||
|
response = client.get("/groups/", headers=user_auth_headers)
|
||||||
|
assert response.status_code == 403
|
||||||
150
test/test_services/test_user_manager.py
Normal file
150
test/test_services/test_user_manager.py
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import pytest
|
||||||
|
from fastapi import status
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_user(client, auth_headers):
|
||||||
|
"""Test creating a new user."""
|
||||||
|
user_data = {
|
||||||
|
"name": "newuser",
|
||||||
|
"email": "newuser@example.com",
|
||||||
|
"is_admin": False,
|
||||||
|
"password": "newpassword123"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = client.post("/users/", json=user_data, headers=auth_headers)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
assert data["name"] == "newuser"
|
||||||
|
assert data["email"] == "newuser@example.com"
|
||||||
|
assert data["is_admin"] is False
|
||||||
|
assert "id" in data
|
||||||
|
assert "passwordhash" not in data # Password hash should not be in response
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_user_unauthorized(client):
|
||||||
|
"""Test creating a user without admin credentials."""
|
||||||
|
user_data = {
|
||||||
|
"name": "unauthorized_user",
|
||||||
|
"email": "unauthorized@example.com",
|
||||||
|
"password": "password123"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = client.post("/users/", json=user_data)
|
||||||
|
assert response.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_users(client, auth_headers, admin_user, regular_user):
|
||||||
|
"""Test retrieving all users."""
|
||||||
|
response = client.get("/users/", headers=auth_headers)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
users = response.json()
|
||||||
|
assert len(users) >= 2 # At least admin_user and regular_user
|
||||||
|
|
||||||
|
user_names = [user["name"] for user in users]
|
||||||
|
assert admin_user.name in user_names
|
||||||
|
assert regular_user.name in user_names
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_user_by_id(client, auth_headers, regular_user):
|
||||||
|
"""Test retrieving a specific user by ID."""
|
||||||
|
response = client.get(f"/users/{regular_user.id}", headers=auth_headers)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
assert data["id"] == regular_user.id
|
||||||
|
assert data["name"] == regular_user.name
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_nonexistent_user(client, auth_headers):
|
||||||
|
"""Test retrieving a non-existent user."""
|
||||||
|
response = client.get("/users/99999", headers=auth_headers)
|
||||||
|
assert response.status_code == 404
|
||||||
|
assert "not found" in response.json()["detail"].lower()
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_user(client, auth_headers, regular_user):
|
||||||
|
"""Test updating a user."""
|
||||||
|
update_data = {
|
||||||
|
"name": "updated_name",
|
||||||
|
"email": "updated@example.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = client.patch(
|
||||||
|
f"/users/{regular_user.id}",
|
||||||
|
json=update_data,
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
assert data["name"] == "updated_name"
|
||||||
|
assert data["email"] == "updated@example.com"
|
||||||
|
# Unchanged fields should remain the same
|
||||||
|
assert data["is_admin"] == regular_user.is_admin
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_user_password(client, auth_headers, regular_user):
|
||||||
|
"""Test updating a user's password."""
|
||||||
|
update_data = {
|
||||||
|
"password": "new_password_456"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = client.patch(
|
||||||
|
f"/users/{regular_user.id}",
|
||||||
|
json=update_data,
|
||||||
|
headers=auth_headers
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
# Verify password can be used for login
|
||||||
|
login_response = client.post(
|
||||||
|
"/token",
|
||||||
|
data={"username": regular_user.name, "password": "new_password_456"}
|
||||||
|
)
|
||||||
|
assert login_response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_nonexistent_user(client, auth_headers):
|
||||||
|
"""Test updating a non-existent user."""
|
||||||
|
update_data = {"name": "updated"}
|
||||||
|
response = client.patch("/users/99999", json=update_data, headers=auth_headers)
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_user(client, auth_headers, regular_user):
|
||||||
|
"""Test deleting a user."""
|
||||||
|
response = client.delete(f"/users/{regular_user.id}", headers=auth_headers)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "deleted successfully" in response.json()["message"].lower()
|
||||||
|
|
||||||
|
# Verify user is deleted
|
||||||
|
response = client.get(f"/users/{regular_user.id}", headers=auth_headers)
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_nonexistent_user(client, auth_headers):
|
||||||
|
"""Test deleting a non-existent user."""
|
||||||
|
response = client.delete("/users/99999", headers=auth_headers)
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_operations_by_non_admin(client, user_auth_headers):
|
||||||
|
"""Test that non-admin users cannot perform admin operations."""
|
||||||
|
# Try to create a user
|
||||||
|
response = client.post(
|
||||||
|
"/users/",
|
||||||
|
json={"name": "test", "password": "pass"},
|
||||||
|
headers=user_auth_headers
|
||||||
|
)
|
||||||
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
# Try to get users
|
||||||
|
response = client.get("/users/", headers=user_auth_headers)
|
||||||
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
# Try to delete the admin user (if ID is known)
|
||||||
|
# This would require knowing the admin user ID
|
||||||
|
# response = client.delete(f"/users/{admin_id}", headers=user_auth_headers)
|
||||||
|
# assert response.status_code == 403
|
||||||
253
uv.lock
generated
253
uv.lock
generated
@@ -36,6 +36,49 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
|
{ url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "argon2-cffi"
|
||||||
|
version = "25.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "argon2-cffi-bindings" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "argon2-cffi-bindings"
|
||||||
|
version = "25.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "cffi" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441, upload-time = "2025-07-30T10:02:05.147Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/97/3c0a35f46e52108d4707c44b95cfe2afcafc50800b5450c197454569b776/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3d3f05610594151994ca9ccb3c771115bdb4daef161976a266f0dd8aa9996b8f", size = 54393, upload-time = "2025-07-30T10:01:40.97Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/f4/98bbd6ee89febd4f212696f13c03ca302b8552e7dbf9c8efa11ea4a388c3/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8b8efee945193e667a396cbc7b4fb7d357297d6234d30a489905d96caabde56b", size = 29328, upload-time = "2025-07-30T10:01:41.916Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/43/24/90a01c0ef12ac91a6be05969f29944643bc1e5e461155ae6559befa8f00b/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3c6702abc36bf3ccba3f802b799505def420a1b7039862014a65db3205967f5a", size = 31269, upload-time = "2025-07-30T10:01:42.716Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d4/d3/942aa10782b2697eee7af5e12eeff5ebb325ccfb86dd8abda54174e377e4/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1c70058c6ab1e352304ac7e3b52554daadacd8d453c1752e547c76e9c99ac44", size = 86558, upload-time = "2025-07-30T10:01:43.943Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/82/b484f702fec5536e71836fc2dbc8c5267b3f6e78d2d539b4eaa6f0db8bf8/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb", size = 92364, upload-time = "2025-07-30T10:01:44.887Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c9/c1/a606ff83b3f1735f3759ad0f2cd9e038a0ad11a3de3b6c673aa41c24bb7b/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4f9665de60b1b0e99bcd6be4f17d90339698ce954cfd8d9cf4f91c995165a92", size = 85637, upload-time = "2025-07-30T10:01:46.225Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/b4/678503f12aceb0262f84fa201f6027ed77d71c5019ae03b399b97caa2f19/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ba92837e4a9aa6a508c8d2d7883ed5a8f6c308c89a4790e1e447a220deb79a85", size = 91934, upload-time = "2025-07-30T10:01:47.203Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/c7/f36bd08ef9bd9f0a9cff9428406651f5937ce27b6c5b07b92d41f91ae541/argon2_cffi_bindings-25.1.0-cp314-cp314t-win32.whl", hash = "sha256:84a461d4d84ae1295871329b346a97f68eade8c53b6ed9a7ca2d7467f3c8ff6f", size = 28158, upload-time = "2025-07-30T10:01:48.341Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/80/0106a7448abb24a2c467bf7d527fe5413b7fdfa4ad6d6a96a43a62ef3988/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b55aec3565b65f56455eebc9b9f34130440404f27fe21c3b375bf1ea4d8fbae6", size = 32597, upload-time = "2025-07-30T10:01:49.112Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/b8/d663c9caea07e9180b2cb662772865230715cbd573ba3b5e81793d580316/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:87c33a52407e4c41f3b70a9c2d3f6056d88b10dad7695be708c5021673f55623", size = 28231, upload-time = "2025-07-30T10:01:49.92Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1d/57/96b8b9f93166147826da5f90376e784a10582dd39a393c99bb62cfcf52f0/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500", size = 54121, upload-time = "2025-07-30T10:01:50.815Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44", size = 29177, upload-time = "2025-07-30T10:01:51.681Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0", size = 31090, upload-time = "2025-07-30T10:01:53.184Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6", size = 81246, upload-time = "2025-07-30T10:01:54.145Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a", size = 87126, upload-time = "2025-07-30T10:01:55.074Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/72/70/7a2993a12b0ffa2a9271259b79cc616e2389ed1a4d93842fac5a1f923ffd/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d", size = 80343, upload-time = "2025-07-30T10:01:56.007Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/9a/4e5157d893ffc712b74dbd868c7f62365618266982b64accab26bab01edc/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99", size = 86777, upload-time = "2025-07-30T10:01:56.943Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/cd/15777dfde1c29d96de7f18edf4cc94c385646852e7c7b0320aa91ccca583/argon2_cffi_bindings-25.1.0-cp39-abi3-win32.whl", hash = "sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2", size = 27180, upload-time = "2025-07-30T10:01:57.759Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl", hash = "sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98", size = 31715, upload-time = "2025-07-30T10:01:58.56Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/42/b9/f8d6fa329ab25128b7e98fd83a3cb34d9db5b059a9847eddb840a0af45dd/argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", size = 27149, upload-time = "2025-07-30T10:01:59.329Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "build"
|
name = "build"
|
||||||
version = "1.4.3"
|
version = "1.4.3"
|
||||||
@@ -95,6 +138,9 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
|
{ url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
|
{ url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
|
{ url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
|
{ url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
|
{ url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
|
{ url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
|
||||||
@@ -103,6 +149,9 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
|
{ url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
|
{ url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
|
{ url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
|
{ url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
|
{ url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
|
{ url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
|
||||||
@@ -111,6 +160,9 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
|
{ url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
|
{ url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
|
{ url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -204,6 +256,75 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "coverage"
|
||||||
|
version = "7.14.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/23/7f/d0720730a397a999ffc0fd3f5bebef347338e3a47b727da66fbb228e2ff2/coverage-7.14.0.tar.gz", hash = "sha256:057a6af2f160a85384cde4ab36f0d2777bae1057bae255f95413cdd382aa5c74", size = 919489, upload-time = "2026-05-10T18:02:31.397Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6b/76/b7c66ee3c66e1b0f9d894c8125983aa0c03fb2336f2fd16559f9c966157f/coverage-7.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f2bbb8254370eb4c628ff3d6fa8a7f74ddc40565394d4f7ab791d1fe568e37ef", size = 219990, upload-time = "2026-05-10T18:00:38.887Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/af/e567cbad5ba69c013a50146dfa886dc7193361fda77521f51274ff620e1b/coverage-7.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:23b81107f46d3f21d0cbce30664fcec0f5d9f585638a67081750f99738f6bf66", size = 220365, upload-time = "2026-05-10T18:00:40.864Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/6f/9ad575d505b4d805b254febc8a5b338a2efe278f8786e56ff1cb8413f9c3/coverage-7.14.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:22a7e06a5f11a757cdfe79018e9095f9f69ae283c5cd8123774c788deec8717b", size = 251363, upload-time = "2026-05-10T18:00:42.489Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/5f/b5370068b2f57787454592ed7dcd1002f0f1703b7db1fa30f6a325a4ca6e/coverage-7.14.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9d1aa57a1dc8e05bdc42e81c5d671d849577aeedf279f4c449d6d286f9ed88ca", size = 253961, upload-time = "2026-05-10T18:00:44.079Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/29/1e/51adf17738976e8f2b85ddef7b7aa12a0838b056c92f175941d8862767c1/coverage-7.14.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90c1a51bcfddf645b3bb7ec333d9e94393a8e94f55642380fa8a9a5a9e636cb7", size = 255193, upload-time = "2026-05-10T18:00:45.623Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/7b/5bfd7ac1df3b881c2ac7a5cbc99c7609e6296c402f5ef587cd81c6f355b3/coverage-7.14.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a841fae2fadcae4f438d43b6ccc4aac2ad609f47cdb6cfdce60cbb3fe5ca7bc2", size = 257326, upload-time = "2026-05-10T18:00:47.173Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7d/38/1d37d316b174fad3843a1d76dbdfe4398771c9ecd0515935dd9ece9cd627/coverage-7.14.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c79d2319cabef1fe8e86df73371126931550804738f78ad7d31e3aad85a67367", size = 251582, upload-time = "2026-05-10T18:00:49.152Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/34/46/746704f95980ba220214e1a41e18cec5aea80a898eaa53c51bf2d645ff36/coverage-7.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b23b0c6f0b1db6ad769b7050c8b641c0bf215ded26c1816955b17b7f26edfa9", size = 253325, upload-time = "2026-05-10T18:00:51.252Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e1/b9/bbe87206d9687b192352f893797825b5f5b15ecd3aa9c68fbff0c074d77b/coverage-7.14.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:55d3089079ce181a4566b1065ab28d2575eb76d8ac8f81f4fcda2bf037fee087", size = 251291, upload-time = "2026-05-10T18:00:52.816Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/57/b8cdb12ac0d73ef0243218bd5e22c9df8f92edab8018213a86aec67c5324/coverage-7.14.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:49c005cba1e2f9677fb2845dcdf9a2e72a52a17d63e8231aaaae35d9f50215ef", size = 255448, upload-time = "2026-05-10T18:00:54.548Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/d4/5002019538b2036ce3c84340f54d2fd5100d55b0a6b0894eee56128d03c7/coverage-7.14.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9117377b823daa28aa8635fbb08cda1cd6be3d7143257345459559aeef852d52", size = 251110, upload-time = "2026-05-10T18:00:56.122Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/37/53/20c5009477660f084e6ed60bc02a91894b8e234e617e86ecfd9aaf78e27b/coverage-7.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7b79d646cf46d5cf9a9f40281d4441df5849e445726e369006d2b117710b33fe", size = 252885, upload-time = "2026-05-10T18:00:57.967Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ae/ab/3cf6427ac9c1f1db747dbb1ce71dde47984876d4c2cfd018a3fef0a78d4d/coverage-7.14.0-cp313-cp313-win32.whl", hash = "sha256:fb609b3658479e33f9516d46f1a89dbb9b6c261366e3a11844a96ec487533dae", size = 222539, upload-time = "2026-05-10T18:00:59.581Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8f/b8/9228523e80321c2cb4880d1f589bc0171f2f71432c35118ad04dc01decce/coverage-7.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0773d8329cf32b6fd222e4b52622c61fe8d503eb966cfc8d3c3c10c96266d50e", size = 223344, upload-time = "2026-05-10T18:01:01.531Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a3/99/118daa192f95e3a6cb2740100fbf8797cda1734b4134ef0b5d501a7fa8f3/coverage-7.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:b4e26a0f1b696faf283bffe5b8569e44e336c582439df5d53281ab89ee0cba96", size = 221966, upload-time = "2026-05-10T18:01:03.16Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/f1/a46cc0c013be170216253184a32366d7cbdb9252feaec866b05c2d12a894/coverage-7.14.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:953f521ca9445300397e65fda3dca58b2dbd68fee983777420b57ac3c77e9f90", size = 220679, upload-time = "2026-05-10T18:01:05.058Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/8c/9c30a3d311a34177fa432995be7fbfc64477d8bac5630bd38055b1c9b424/coverage-7.14.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:98af83fd65ae24b1fdd03aaead967a9f523bcd2f1aab2d4f3ffda65bb568a6f1", size = 221033, upload-time = "2026-05-10T18:01:07.002Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9a/cd/3fb5e06c3badefd0c1b47e2044fdca67f8220a4ec2e7fcfb476aa0a67c6c/coverage-7.14.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:668b92e6958c4db7cf92e81caac328dfbbdbb215db2850ad28f0cbe1eea0bfbd", size = 262333, upload-time = "2026-05-10T18:01:08.903Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a8/e6/fbc322325c7294d3e22c1ad6b79e45d0806b25228c8e5842aed6d8169aa7/coverage-7.14.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9fbd898551762dea00d3fef2b1c4f99afd2c6a3ff952ea07d60a9bd5ed4f34bc", size = 264410, upload-time = "2026-05-10T18:01:10.531Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/08/92/c497b264bec1673c47cc77e26f760fcda4654cabf1f39546d1a23a3b8c35/coverage-7.14.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68af363c07ecd8d4b7d4043d85cb376d7d227eceb54e5323ee45da73dbd3e426", size = 266836, upload-time = "2026-05-10T18:01:12.19Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/fc/045da320987f401af5d2815d351e8aa799aec859f60e29f445e3089eeedb/coverage-7.14.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e57054a583da8ac55edf24117ea4c9133032cfc4cf72aa2d48c1e5d4b52f899", size = 267974, upload-time = "2026-05-10T18:01:13.926Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1b/ae/227b1e379497fb7a4fc3286e620f80c8a1e7cec66d45695a01639eb1af65/coverage-7.14.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc3499459bbcdd51a65b64c35ab7ed2764eaf3cba826e0df3f1d7fe2e102b70b", size = 261578, upload-time = "2026-05-10T18:01:15.564Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/f5/3570342900f2acea31d33ff1590c5d8bac1a8e1a2e1c6d34a5d5e61de681/coverage-7.14.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:45899ec2138a4346ed34d601dedf5076fb74edf2d1dd9dc76a78e82397edee90", size = 264394, upload-time = "2026-05-10T18:01:17.607Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/16/29/de1bbc01c935b28f89b1dc3db85b011c055e843a8e5e3b83141c3f80af7f/coverage-7.14.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8767486808c436f05b23ab98eb963fb29185e32a9357a166971685cb3459900f", size = 262022, upload-time = "2026-05-10T18:01:19.304Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/35/95/f53890b0bf2fc10ab168e05d38869215e73ca24c4cb521c3bb0eb62fe16b/coverage-7.14.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a3b5ddfd6aa7ddad53ee3edb231e88a2151507a43229b7d71b953916deca127d", size = 265732, upload-time = "2026-05-10T18:01:21.494Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/ea/c919e259081dd2bdf0e43b87209709ba7ec2e4117c2a7f5185379c43463c/coverage-7.14.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:63df0fe568e698e1045792399f8ab6da3a6c2dce3182813fb92afa2641087b47", size = 260921, upload-time = "2026-05-10T18:01:23.533Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/2c/c2831889705a81dc5d1c6ca12e4d8e9b95dfc146d153488a6c0ea685d28e/coverage-7.14.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:827d6397dbd95144939b18f89edf31f63e1f99633e8d5f32f22ba8bdda567477", size = 263109, upload-time = "2026-05-10T18:01:25.165Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/a9/2fcae5003cac3d63fe344d2166243c2756935f48420863c5272b240d550b/coverage-7.14.0-cp313-cp313t-win32.whl", hash = "sha256:7bf43e000d24012599b879791cff41589af90674722421ef11b11a5431920bab", size = 223212, upload-time = "2026-05-10T18:01:27.157Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3f/bb/18e94d7b14b9b398164197114a587a04ab7c9fdbe1d237eef57311c5e883/coverage-7.14.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3f5549365af25d770e06b1f8f5682d9a5637d06eb494db91c6fa75d3950cc917", size = 224272, upload-time = "2026-05-10T18:01:29.107Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/db/56/4f14fad782b035c81c4ffd09159e7103d42bb1d93ac8496d04b90a11b7da/coverage-7.14.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6d160217ec6fe890f16ad3a9531761589443749e448f91986c972714fad361c8", size = 222530, upload-time = "2026-05-10T18:01:31.151Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1c/18/b9a6586d73992807c26f9a5f274131be3d76b56b18a82b9392e2a25d2e45/coverage-7.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9aed9fa983514ca032790f3fe0d1c0e42ca7e16b42432af1706b50a9a46bef5d", size = 220036, upload-time = "2026-05-10T18:01:33.057Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/9b/4165a1d56ddc302a0e2d518fd9d412a4fd0b57562618c78c5f21c57194f5/coverage-7.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ba3b8390db29296dbbf49e91b6fe08f990743a90c8f447ba4c2ffc29670dfa63", size = 220368, upload-time = "2026-05-10T18:01:34.705Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/69/aa/c12e52a5ba148d9995229d557e3be6e554fe469addc0e9241b2f0956d8ea/coverage-7.14.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3a5d8e876dfa2f102e970b183863d6dedd023d3c0eeca1fe7a9787bc5f28b212", size = 251417, upload-time = "2026-05-10T18:01:36.949Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/51/ec641c26e6dca1b25a7d2035ba6ecb7c884ef1a100a9e42fbe4ce4405139/coverage-7.14.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ebb8f4614a3787d567e610bbfdf96a4798dd69a1afb1bd8ad228d4111fe6ff3", size = 253924, upload-time = "2026-05-10T18:01:38.985Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/33/c4/59c3de0bd1b538824173fd518fed51c1ce740ca5ed68e74545983f4053a9/coverage-7.14.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b9bf47223dd8db3d4c4b2e443b02bace480d428f0822c3f991600448a176c97", size = 255269, upload-time = "2026-05-10T18:01:40.957Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/a9/36dfa153a62040296f6e7febfdb20a5720622f6ef5a81a41e8237b9a5344/coverage-7.14.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3485a836550b303d006d57cc06e3d5afaabc642c77050b7c985a97b13e3776b8", size = 257583, upload-time = "2026-05-10T18:01:42.607Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/26/7b/cc2c048d4114d9ab1c2409e9ee365e5ae10736df6dffcfc9444effa6c708/coverage-7.14.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e7e88110bae996d199d1693ca8ec3fd52441d426401ae963437598667b4c5eb", size = 251434, upload-time = "2026-05-10T18:01:44.537Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ee/df/6770eaa576e604575e9a78055313250faef5faa84bd6f71a39fece519c43/coverage-7.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15228a6800ce7bdf1b74800595e56db7138cecb338fdbf044806e10dcf182dfe", size = 253280, upload-time = "2026-05-10T18:01:46.175Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ad/9e/1c0264514a3f98259a6d64765a397b2c8373e3ba59ee722a4802d3ec0c61/coverage-7.14.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9d26ac7f5398bafc5b57421ad994e8a4749e8a7a0e62d05ec7d53014d5963bfa", size = 251241, upload-time = "2026-05-10T18:01:48.732Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/16/4efdf3e3c4079cdbf0ece56a2fea872df9e8a3e15a13a0af4400e1075944/coverage-7.14.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2fb73254ff43c911c967a899e1359bc5049b4b115d6e8fbdde4937d0a2246cd5", size = 255516, upload-time = "2026-05-10T18:01:50.819Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/93/69/b1de96346603881b3d1bc8d6447c83200e1c9700ffbaff926ba01ff5724c/coverage-7.14.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:454a380af72c6adada298ed270d38c7a391288198dbfb8467f786f588751a90c", size = 251059, upload-time = "2026-05-10T18:01:52.773Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/66/2881853e0363a5e0a724d1103e53650795367471b6afb234f8b49e713bc6/coverage-7.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:65c86fb646d2bd2972e96bd1a8b45817ed907cee68655d6295fe7ec031d04cca", size = 252716, upload-time = "2026-05-10T18:01:54.506Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/55/5c/0d3305d002c41dcde873dbe456491e663dc55152ca526b630b5c47efd62f/coverage-7.14.0-cp314-cp314-win32.whl", hash = "sha256:6a6516b02a6101398e19a3f44820f69bab2590697f7def4331f668b14adaf828", size = 222788, upload-time = "2026-05-10T18:01:56.487Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/58/6e1b8f52fdc3184b47dc5037f5070d83a3d11042db1594b02d2a44d786c8/coverage-7.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:45e0f79d8351fa76e256716df91eab12890d32678b9590df7ae1042e4bd4cf5d", size = 223600, upload-time = "2026-05-10T18:01:58.497Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/00/70/a18c408e674bc26281cadaedc7351f929bd2094e191e4b15271c30b084cc/coverage-7.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:4b899594a8b2d81e5cc064a0d7f9cac2081fed91049456cae7676787e41549c9", size = 222168, upload-time = "2026-05-10T18:02:00.411Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/89/2681f071d238b62aff8dfc2ab44fc24cfdb38d1c01f391a80522ff5d3a16/coverage-7.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f580f8c80acd94ac72e863efe2cab791d8c38d153e0b463b92dfa000d5c84cd1", size = 220766, upload-time = "2026-05-10T18:02:02.313Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/c7/c987babafd9207ffa1995e1ef1f9b26762cf4963aa768a66b6f0501e4616/coverage-7.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a2bd259c442cd43c49b30fbafc51776eb19ea396faf159d26a83e6a0a5f13b0c", size = 221035, upload-time = "2026-05-10T18:02:04.017Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/e9/d6a5ac3b333088143d6fc877d398a9a674dc03124a2f776e131f03864823/coverage-7.14.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a706b908dfa85538863504c624b237a3cc34232bf403c057414ebfdb3b4d9f84", size = 262405, upload-time = "2026-05-10T18:02:05.915Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/38/b1/e70838d29a7c08e22d44398a46db90815bbcbf28de06992bd9210d1a8d8e/coverage-7.14.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7333cd944ee4393b9b3d3c1b598c936d4fc8d70573a4c7dacfec5590dd50e436", size = 264530, upload-time = "2026-05-10T18:02:07.582Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6b/73/5c31ef97763288d03d9995152b96d5475b527c63d91c84b01caea894b83a/coverage-7.14.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f162bc9a15b82d947b02651b0c7e1609d6f7a8735ca330cfadec8481dd97d5a", size = 266932, upload-time = "2026-05-10T18:02:09.401Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e1/76/dd56d80f29c5f05b4d76f7e7c6d47cafacae017189c75c5759d24f9ff0cc/coverage-7.14.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:362cb78e01a5dc82009d88004cf60f2e6b6d6fcbfdec05b05af73b0abf40118f", size = 268062, upload-time = "2026-05-10T18:02:11.399Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6e/c7/27ba85cd5b95614f159ff93ebff1901584a8d192e2e5e24c4943a7453f59/coverage-7.14.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:acebd068fca5512c3a6fde9c045f901613478781a73f0e82b307b214daef23fb", size = 261504, upload-time = "2026-05-10T18:02:13.257Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/13/2e/e8149f60ab5d5684c6eee881bdf34b127115cddbb958b196768dd9d63473/coverage-7.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:29fe3da551dface75deb2ccbf87b6b66e2e7ef38f6d89050b428be94afff3490", size = 264398, upload-time = "2026-05-10T18:02:15.063Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/7f/1261b025285323225f4b4abffa5a643649dfd67e25ddca7ebcbdea3b7cb3/coverage-7.14.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b4cc4fce8672fffcb09b0eafc167b396b3ba53c4a7230f54b7aaffbf6c835fa9", size = 262000, upload-time = "2026-05-10T18:02:16.756Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/dc/829c54f60b9d08389439c00f813c752781c496fc5788c78d8006db4b4f2b/coverage-7.14.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5d4a51aad8ba8bdcd2b8bd8f03d4aca19693fa2327a3470e4718a25b03481020", size = 265732, upload-time = "2026-05-10T18:02:18.817Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/b0/70bd1419941652fa062689cba9c3eeafb8f5e6fbb890bce41c3bdda5dbd6/coverage-7.14.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9f323af3e1e4f68b60b7b247e37b8515563a61375518fa59de1af48ba28a3db6", size = 260847, upload-time = "2026-05-10T18:02:20.528Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/73/be40b2390656c654d35ea0015ea7ba3d945769cf80790ad5e0bb2d56d2ba/coverage-7.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1a0abc7342ea9711c469dd8b821c6c311e6bc6aac1442e5fbd6b27fae0a8f3db", size = 263166, upload-time = "2026-05-10T18:02:22.337Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/29/55/4a643f712fcf7cf2881f8ec1e0ccb7b164aff3108f69b51801246c8799f2/coverage-7.14.0-cp314-cp314t-win32.whl", hash = "sha256:a9f864ef57b7172e2db87a096642dd51e179e085ab6b2c371c29e885f65c8fb2", size = 223573, upload-time = "2026-05-10T18:02:24.11Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/27/96/3acae5da0953be042c0b4dea6d6789d2f080701c77b88e44d5bd41b9219b/coverage-7.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:29943e552fdc08e082eb51400fb2f58e118a83b5542bd06531214e084399b644", size = 224680, upload-time = "2026-05-10T18:02:25.896Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/93/3d/6ab5d2dd8325d838737c6f8d83d62eb6230e0d70b87b51b57bbfd08fa767/coverage-7.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:742a73ea621953b012f2c4c2219b512180dd84489acf5b1596b0aafc55b9100b", size = 222703, upload-time = "2026-05-10T18:02:27.822Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/e8/cb8e80d6f9f55b99588625062822bf946cf03ed06315df4bd8397f5632a1/coverage-7.14.0-py3-none-any.whl", hash = "sha256:8de5b61163aee3d05c8a2beab6f47913df7981dad1baf82c414d99158c286ab1", size = 211764, upload-time = "2026-05-10T18:02:29.538Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crashtest"
|
name = "crashtest"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
@@ -222,6 +343,7 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" },
|
{ url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" },
|
{ url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" },
|
{ url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" },
|
||||||
@@ -233,6 +355,9 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" },
|
{ url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" },
|
{ url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" },
|
{ url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" },
|
{ url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" },
|
{ url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" },
|
{ url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" },
|
||||||
@@ -244,6 +369,9 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" },
|
{ url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" },
|
{ url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" },
|
{ url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" },
|
{ url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" },
|
{ url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" },
|
{ url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" },
|
||||||
@@ -255,6 +383,8 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" },
|
{ url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" },
|
{ url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" },
|
{ url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -479,16 +609,32 @@ version = "0.1.0"
|
|||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "fastapi", extra = ["standard"] },
|
{ name = "fastapi", extra = ["standard"] },
|
||||||
|
{ name = "paho-mqtt" },
|
||||||
{ name = "poetry" },
|
{ name = "poetry" },
|
||||||
|
{ name = "pwdlib", extra = ["argon2"] },
|
||||||
|
{ name = "pyjwt", extra = ["crypto"] },
|
||||||
|
{ name = "pyscard" },
|
||||||
|
{ name = "pytest" },
|
||||||
|
{ name = "pytest-cov" },
|
||||||
{ name = "python-desfire" },
|
{ name = "python-desfire" },
|
||||||
|
{ name = "requests" },
|
||||||
|
{ name = "setuptools" },
|
||||||
{ name = "sqlmodel" },
|
{ name = "sqlmodel" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "fastapi", extras = ["standard"], specifier = ">=0.135.3" },
|
{ name = "fastapi", extras = ["standard"], specifier = ">=0.135.3" },
|
||||||
|
{ name = "paho-mqtt", specifier = ">=2.1.0" },
|
||||||
{ name = "poetry", specifier = ">=2.3.4" },
|
{ name = "poetry", specifier = ">=2.3.4" },
|
||||||
|
{ name = "pwdlib", extras = ["argon2"], specifier = ">=0.3.0" },
|
||||||
|
{ name = "pyjwt", extras = ["crypto"], specifier = ">=2.12.1" },
|
||||||
|
{ name = "pyscard", specifier = ">=2.3.1" },
|
||||||
|
{ name = "pytest", specifier = ">=9.0.3" },
|
||||||
|
{ name = "pytest-cov", specifier = ">=7.1.0" },
|
||||||
{ name = "python-desfire", git = "https://github.com/waza-ari/python-desfire" },
|
{ name = "python-desfire", git = "https://github.com/waza-ari/python-desfire" },
|
||||||
|
{ name = "requests", specifier = ">=2.33.1" },
|
||||||
|
{ name = "setuptools", specifier = ">=82.0.1" },
|
||||||
{ name = "sqlmodel", specifier = ">=0.0.38" },
|
{ name = "sqlmodel", specifier = ">=0.0.38" },
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -591,6 +737,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "installer"
|
name = "installer"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
@@ -797,6 +952,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" },
|
{ url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "paho-mqtt"
|
||||||
|
version = "2.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/39/15/0a6214e76d4d32e7f663b109cf71fb22561c2be0f701d67f93950cd40542/paho_mqtt-2.1.0.tar.gz", hash = "sha256:12d6e7511d4137555a3f6ea167ae846af2c7357b10bc6fa4f7c3968fc1723834", size = 148848, upload-time = "2024-04-29T19:52:55.591Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c4/cb/00451c3cf31790287768bb12c6bec834f5d292eaf3022afc88e14b8afc94/paho_mqtt-2.1.0-py3-none-any.whl", hash = "sha256:6db9ba9b34ed5bc6b6e3812718c7e06e2fd7444540df2455d2c51bd58808feee", size = 67219, upload-time = "2024-04-29T19:52:48.345Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pbs-installer"
|
name = "pbs-installer"
|
||||||
version = "2026.4.7"
|
version = "2026.4.7"
|
||||||
@@ -832,6 +996,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" },
|
{ url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pluggy"
|
||||||
|
version = "1.6.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "poetry"
|
name = "poetry"
|
||||||
version = "2.3.4"
|
version = "2.3.4"
|
||||||
@@ -873,6 +1046,20 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/0e/38/646023abf93e47c6808cbee82423324505ec075d7b90a22654765e1eade7/poetry_core-2.3.2-py3-none-any.whl", hash = "sha256:23df641b64f87fbb4ce1873c1915a4d4bb1b7d808c596e4307edc073e68d7234", size = 340872, upload-time = "2026-03-29T07:44:52.143Z" },
|
{ url = "https://files.pythonhosted.org/packages/0e/38/646023abf93e47c6808cbee82423324505ec075d7b90a22654765e1eade7/poetry_core-2.3.2-py3-none-any.whl", hash = "sha256:23df641b64f87fbb4ce1873c1915a4d4bb1b7d808c596e4307edc073e68d7234", size = 340872, upload-time = "2026-03-29T07:44:52.143Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pwdlib"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/5f/41/a7c0d8a003c36ce3828ae3ed0391fe6a15aad65f082dbd6bec817ea95c0b/pwdlib-0.3.0.tar.gz", hash = "sha256:6ca30f9642a1467d4f5d0a4d18619de1c77f17dfccb42dd200b144127d3c83fc", size = 215810, upload-time = "2025-10-25T12:44:24.395Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/0c/9086a357d02a050fbb3270bf5043ac284dbfb845670e16c9389a41defc9e/pwdlib-0.3.0-py3-none-any.whl", hash = "sha256:f86c15c138858c09f3bba0a10984d4f9178158c55deaa72eac0210849b1a140d", size = 8633, upload-time = "2025-10-25T12:44:23.406Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.optional-dependencies]
|
||||||
|
argon2 = [
|
||||||
|
{ name = "argon2-cffi" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pycparser"
|
name = "pycparser"
|
||||||
version = "3.0"
|
version = "3.0"
|
||||||
@@ -1024,6 +1211,20 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyjwt"
|
||||||
|
version = "2.12.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.optional-dependencies]
|
||||||
|
crypto = [
|
||||||
|
{ name = "cryptography" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyproject-hooks"
|
name = "pyproject-hooks"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@@ -1033,6 +1234,49 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" },
|
{ url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyscard"
|
||||||
|
version = "2.3.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/93/c9/65c68738a94b44b67b3c5e68a815890bbd225f2ae11ef1ace9b61fa9d5f3/pyscard-2.3.1.tar.gz", hash = "sha256:a24356f57a0a950740b6e54f51f819edd5296ee8892a6625b0da04724e9e6c13", size = 160650, upload-time = "2025-10-29T15:49:08.353Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/08/e2/d8f967fe6fee82c02ee35d736e8707d65ed1e37fa7635f3bdca96bdcd2ff/pyscard-2.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c11a596407e18cdcf16a4ccd8cdeaa55846d4f7ec2eefc529483e201f6906658", size = 170956, upload-time = "2025-10-29T15:53:32.072Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ca/a1/508fc5733e2ce01a71c6abc94dae543311cf9d0962e309b0a78b4cb4031a/pyscard-2.3.1-cp313-cp313-macosx_15_0_x86_64.whl", hash = "sha256:72b1ab922fad5e050144ec72762e36741271b09d2389cda9b976b61ee0564e71", size = 138809, upload-time = "2025-10-29T15:49:06.528Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/a4/5481cd54f882ea61bfc334667f1e5126ac51124a464297dbe24592288b14/pyscard-2.3.1-cp313-cp313-win32.whl", hash = "sha256:a0b59d1961ff9fb15d980ad64edae13e4512b7e641ea8959e86133f34091aa5c", size = 139294, upload-time = "2025-10-29T15:53:35.805Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8c/2b/c98e7bcf45958905d1bfdab59fe5acd67fe9ff0ad78145c4783886c38c84/pyscard-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:df2b256bc719b701807114bdd179f7b303f309a954d5689188b088adfc33ead2", size = 145501, upload-time = "2025-10-29T15:53:33.549Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/93/a6/74cca0ec3c08f35c1a7468a8508d0e3989e63298d3a4b1f928542719aebc/pyscard-2.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:f58b46cd78455a29a0abceabff21b37da81a385a737b29e6dd5e25acb7e3f3da", size = 140758, upload-time = "2025-10-29T15:53:34.542Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest"
|
||||||
|
version = "9.0.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
{ name = "iniconfig" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "pluggy" },
|
||||||
|
{ name = "pygments" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-cov"
|
||||||
|
version = "7.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "coverage" },
|
||||||
|
{ name = "pluggy" },
|
||||||
|
{ name = "pytest" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-desfire"
|
name = "python-desfire"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
@@ -1302,6 +1546,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/fa/eb/d875669993b762556ae8b2efd86219943b4c0864d22204d622a9aee3052b/sentry_sdk-2.58.0-py2.py3-none-any.whl", hash = "sha256:688d1c704ddecf382ea3326f21a67453d4caa95592d722b7c780a36a9d23109e", size = 460919, upload-time = "2026-04-13T17:23:24.675Z" },
|
{ url = "https://files.pythonhosted.org/packages/fa/eb/d875669993b762556ae8b2efd86219943b4c0864d22204d622a9aee3052b/sentry_sdk-2.58.0-py2.py3-none-any.whl", hash = "sha256:688d1c704ddecf382ea3326f21a67453d4caa95592d722b7c780a36a9d23109e", size = 460919, upload-time = "2026-04-13T17:23:24.675Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "setuptools"
|
||||||
|
version = "82.0.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shellingham"
|
name = "shellingham"
|
||||||
version = "1.5.4"
|
version = "1.5.4"
|
||||||
|
|||||||
Reference in New Issue
Block a user