feat: Enhance transaction model and dashboard with Azure OAuth integration
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
# OAuth 2.0 Integration with Azure Entra ID
|
||||
|
||||
## Overview
|
||||
|
||||
The dashboard (`/`) is now protected by Azure Entra ID OAuth authentication. Unauthenticated users are redirected to login.
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Unauthenticated Request** → User visits `/` without a session
|
||||
2. **Redirect to Login** → Dashboard redirects to `/login`
|
||||
3. **OAuth Flow** → `/login` initiates Azure Entra ID authorization
|
||||
4. **Callback** → After user approves, Entra redirects to `/auth/callback`
|
||||
5. **Session Created** → User info stored in `request.session["user"]`
|
||||
6. **Access Granted** → Dashboard renders with authenticated user data
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `app/core/settings.py` | Load OAuth config from `.env` |
|
||||
| `app/core/auth.py` | OAuth client setup (Authlib + Entra metadata) |
|
||||
| `app/views/auth.py` | Routes: `/login`, `/auth/callback`, `/logout` |
|
||||
| `app/views/dashboard.py` | Protected route; redirects unauthenticated users |
|
||||
| `app/core/app_factory.py` | Register SessionMiddleware + routers |
|
||||
| `.env` | Runtime secrets (tenant ID, client ID/secret, session key) |
|
||||
|
||||
## Session & Middleware
|
||||
|
||||
- **SessionMiddleware**: Signs/validates session cookies with `SESSION_SECRET_KEY`
|
||||
- **Session data**: Stored client-side in encrypted cookie (secure, stateless)
|
||||
- **Session keys**: `request.session.get("user")` contains user object with `sub`, `name`, `email`
|
||||
|
||||
## Environment Variables
|
||||
|
||||
```bash
|
||||
SESSION_SECRET_KEY # 32-byte random secret for session signing
|
||||
AZURE_TENANT_ID # Azure Entra tenant ID
|
||||
AZURE_CLIENT_ID # Entra app registration client ID
|
||||
AZURE_CLIENT_SECRET # Entra app registration client secret
|
||||
```
|
||||
|
||||
## Dependencies Added
|
||||
|
||||
- **authlib**: OAuth 2.0 client for OpenID Connect flows
|
||||
- **httpx**: HTTP client for Authlib OAuth requests
|
||||
- **itsdangerous**: Session cookie signing
|
||||
|
||||
## Testing Locally
|
||||
|
||||
1. Register app in [Azure Portal](https://portal.azure.com) (see [README.md](README.md))
|
||||
2. Create `.env` with credentials
|
||||
3. Start: `uv run uvicorn app.main:app --reload`
|
||||
4. Visit `http://127.0.0.1:8000/` → redirects to `/login`
|
||||
5. Click login → completes Entra auth flow → redirects to dashboard
|
||||
6. Click logout at `/logout` → clears session, returns to home
|
||||
+20
-5
@@ -13,7 +13,9 @@ from app.service.recon_service import (
|
||||
router = APIRouter(prefix="/api", tags=["Transactions"])
|
||||
|
||||
|
||||
@router.get("/transactions", response_model=list[Transaction], summary="List transactions")
|
||||
@router.get(
|
||||
"/transactions", response_model=list[Transaction], summary="List transactions"
|
||||
)
|
||||
async def list_transactions_endpoint(
|
||||
status: StatusType | None = Query(None, description="Filter by status"),
|
||||
flag: FlagType | None = Query(None, description="Filter by flag"),
|
||||
@@ -21,7 +23,11 @@ async def list_transactions_endpoint(
|
||||
return list_transactions(status=status, flag=flag)
|
||||
|
||||
|
||||
@router.get("/transactions/{transaction_id}", response_model=Transaction, summary="Get a transaction")
|
||||
@router.get(
|
||||
"/transactions/{transaction_id}",
|
||||
response_model=Transaction,
|
||||
summary="Get a transaction",
|
||||
)
|
||||
async def get_transaction_endpoint(transaction_id: str) -> Transaction:
|
||||
transaction = get_transaction_by_id(transaction_id)
|
||||
if transaction is None:
|
||||
@@ -29,15 +35,24 @@ async def get_transaction_endpoint(transaction_id: str) -> Transaction:
|
||||
return transaction
|
||||
|
||||
|
||||
@router.post("/transactions", response_model=Transaction, status_code=201, summary="Submit a transaction")
|
||||
@router.post(
|
||||
"/transactions",
|
||||
response_model=Transaction,
|
||||
status_code=201,
|
||||
summary="Submit a transaction",
|
||||
)
|
||||
async def create_transaction_endpoint(transaction: Transaction) -> Transaction:
|
||||
created = add_transaction(transaction)
|
||||
if not created:
|
||||
raise HTTPException(status_code=409, detail=f"{transaction.transaction_id} already exists")
|
||||
raise HTTPException(
|
||||
status_code=409, detail=f"{transaction.transaction_id} already exists"
|
||||
)
|
||||
return transaction
|
||||
|
||||
|
||||
@router.delete("/transactions/{transaction_id}", status_code=204, summary="Delete a transaction")
|
||||
@router.delete(
|
||||
"/transactions/{transaction_id}", status_code=204, summary="Delete a transaction"
|
||||
)
|
||||
async def delete_transaction_endpoint(transaction_id: str) -> None:
|
||||
removed = delete_transaction_by_id(transaction_id)
|
||||
if not removed:
|
||||
|
||||
@@ -11,14 +11,18 @@ load_dotenv(PROJECT_ROOT / ".env")
|
||||
|
||||
class Settings:
|
||||
def __init__(self) -> None:
|
||||
self.session_secret_key = getenv("SESSION_SECRET_KEY", "change-me-in-production")
|
||||
self.session_secret_key = getenv(
|
||||
"SESSION_SECRET_KEY", "change-me-in-production"
|
||||
)
|
||||
self.azure_tenant_id = getenv("AZURE_TENANT_ID")
|
||||
self.azure_client_id = getenv("AZURE_CLIENT_ID")
|
||||
self.azure_client_secret = getenv("AZURE_CLIENT_SECRET")
|
||||
|
||||
@property
|
||||
def azure_configured(self) -> bool:
|
||||
return bool(self.azure_tenant_id and self.azure_client_id and self.azure_client_secret)
|
||||
return bool(
|
||||
self.azure_tenant_id and self.azure_client_id and self.azure_client_secret
|
||||
)
|
||||
|
||||
|
||||
@lru_cache
|
||||
|
||||
@@ -8,4 +8,3 @@ app = create_app()
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=True)
|
||||
|
||||
|
||||
@@ -11,6 +11,12 @@ class Transaction(BaseModel):
|
||||
amount: float
|
||||
status: Literal["Matched", "Unmatched", "Pending"]
|
||||
flag: Literal["None", "Duplicate", "Threshold Breach", "Manual Review"]
|
||||
reference_id: str
|
||||
counterparty: str
|
||||
currency: str
|
||||
booking_date: date
|
||||
settlement_date: date
|
||||
description: str
|
||||
|
||||
|
||||
class ReconSummary(BaseModel):
|
||||
|
||||
@@ -15,6 +15,12 @@ TRANSACTIONS: list[Transaction] = [
|
||||
amount=12400.00,
|
||||
status="Matched",
|
||||
flag="None",
|
||||
reference_id="REF-2026-0001",
|
||||
counterparty="Morgan Stanley",
|
||||
currency="USD",
|
||||
booking_date=date(2026, 4, 9),
|
||||
settlement_date=date(2026, 4, 12),
|
||||
description="FX Swap USD/EUR",
|
||||
),
|
||||
Transaction(
|
||||
transaction_id="TXN-002",
|
||||
@@ -23,6 +29,12 @@ TRANSACTIONS: list[Transaction] = [
|
||||
amount=3750.50,
|
||||
status="Unmatched",
|
||||
flag="Duplicate",
|
||||
reference_id="REF-2026-0002",
|
||||
counterparty="JPMorgan Chase",
|
||||
currency="USD",
|
||||
booking_date=date(2026, 4, 10),
|
||||
settlement_date=date(2026, 4, 13),
|
||||
description="Interest rate swap",
|
||||
),
|
||||
Transaction(
|
||||
transaction_id="TXN-003",
|
||||
@@ -31,6 +43,12 @@ TRANSACTIONS: list[Transaction] = [
|
||||
amount=88200.00,
|
||||
status="Unmatched",
|
||||
flag="Threshold Breach",
|
||||
reference_id="REF-2026-0003",
|
||||
counterparty="Goldman Sachs",
|
||||
currency="GBP",
|
||||
booking_date=date(2026, 4, 10),
|
||||
settlement_date=date(2026, 4, 14),
|
||||
description="Cross-currency swap",
|
||||
),
|
||||
Transaction(
|
||||
transaction_id="TXN-004",
|
||||
@@ -39,6 +57,12 @@ TRANSACTIONS: list[Transaction] = [
|
||||
amount=540.00,
|
||||
status="Matched",
|
||||
flag="None",
|
||||
reference_id="REF-2026-0004",
|
||||
counterparty="Barclays",
|
||||
currency="EUR",
|
||||
booking_date=date(2026, 4, 11),
|
||||
settlement_date=date(2026, 4, 15),
|
||||
description="Bond settlement",
|
||||
),
|
||||
Transaction(
|
||||
transaction_id="TXN-005",
|
||||
@@ -47,11 +71,229 @@ TRANSACTIONS: list[Transaction] = [
|
||||
amount=21000.00,
|
||||
status="Pending",
|
||||
flag="Manual Review",
|
||||
reference_id="REF-2026-0005",
|
||||
counterparty="Deutsche Bank",
|
||||
currency="USD",
|
||||
booking_date=date(2026, 4, 12),
|
||||
settlement_date=date(2026, 4, 16),
|
||||
description="Equity options trade",
|
||||
),
|
||||
Transaction(
|
||||
transaction_id="TXN-006",
|
||||
date=date(2026, 4, 14),
|
||||
account="ACC-5542",
|
||||
amount=15600.00,
|
||||
status="Matched",
|
||||
flag="None",
|
||||
reference_id="REF-2026-0006",
|
||||
counterparty="Credit Suisse",
|
||||
currency="CHF",
|
||||
booking_date=date(2026, 4, 13),
|
||||
settlement_date=date(2026, 4, 17),
|
||||
description="Repo transaction",
|
||||
),
|
||||
Transaction(
|
||||
transaction_id="TXN-007",
|
||||
date=date(2026, 4, 14),
|
||||
account="ACC-8834",
|
||||
amount=2250.75,
|
||||
status="Unmatched",
|
||||
flag="Threshold Breach",
|
||||
reference_id="REF-2026-0007",
|
||||
counterparty="Citigroup",
|
||||
currency="JPY",
|
||||
booking_date=date(2026, 4, 13),
|
||||
settlement_date=date(2026, 4, 18),
|
||||
description="Forward contract",
|
||||
),
|
||||
Transaction(
|
||||
transaction_id="TXN-008",
|
||||
date=date(2026, 4, 15),
|
||||
account="ACC-2201",
|
||||
amount=45000.00,
|
||||
status="Matched",
|
||||
flag="None",
|
||||
reference_id="REF-2026-0008",
|
||||
counterparty="UBS",
|
||||
currency="USD",
|
||||
booking_date=date(2026, 4, 14),
|
||||
settlement_date=date(2026, 4, 19),
|
||||
description="Commodity swap",
|
||||
),
|
||||
Transaction(
|
||||
transaction_id="TXN-009",
|
||||
date=date(2026, 4, 15),
|
||||
account="ACC-7123",
|
||||
amount=8900.50,
|
||||
status="Pending",
|
||||
flag="Manual Review",
|
||||
reference_id="REF-2026-0009",
|
||||
counterparty="HSBC",
|
||||
currency="AUD",
|
||||
booking_date=date(2026, 4, 14),
|
||||
settlement_date=date(2026, 4, 20),
|
||||
description="Index futures",
|
||||
),
|
||||
Transaction(
|
||||
transaction_id="TXN-010",
|
||||
date=date(2026, 4, 16),
|
||||
account="ACC-3345",
|
||||
amount=1200.00,
|
||||
status="Matched",
|
||||
flag="None",
|
||||
reference_id="REF-2026-0010",
|
||||
counterparty="Bank of America",
|
||||
currency="USD",
|
||||
booking_date=date(2026, 4, 15),
|
||||
settlement_date=date(2026, 4, 21),
|
||||
description="Interest rate cap",
|
||||
),
|
||||
Transaction(
|
||||
transaction_id="TXN-011",
|
||||
date=date(2026, 4, 16),
|
||||
account="ACC-5501",
|
||||
amount=67500.00,
|
||||
status="Unmatched",
|
||||
flag="Duplicate",
|
||||
reference_id="REF-2026-0011",
|
||||
counterparty="Wells Fargo",
|
||||
currency="EUR",
|
||||
booking_date=date(2026, 4, 15),
|
||||
settlement_date=date(2026, 4, 22),
|
||||
description="Currency forward",
|
||||
),
|
||||
Transaction(
|
||||
transaction_id="TXN-012",
|
||||
date=date(2026, 4, 17),
|
||||
account="ACC-6789",
|
||||
amount=3400.25,
|
||||
status="Matched",
|
||||
flag="None",
|
||||
reference_id="REF-2026-0012",
|
||||
counterparty="BNY Mellon",
|
||||
currency="GBP",
|
||||
booking_date=date(2026, 4, 16),
|
||||
settlement_date=date(2026, 4, 23),
|
||||
description="Bond purchase",
|
||||
),
|
||||
Transaction(
|
||||
transaction_id="TXN-013",
|
||||
date=date(2026, 4, 17),
|
||||
account="ACC-1234",
|
||||
amount=52100.00,
|
||||
status="Pending",
|
||||
flag="Threshold Breach",
|
||||
reference_id="REF-2026-0013",
|
||||
counterparty="State Street",
|
||||
currency="JPY",
|
||||
booking_date=date(2026, 4, 16),
|
||||
settlement_date=date(2026, 4, 24),
|
||||
description="Swaption settlement",
|
||||
),
|
||||
Transaction(
|
||||
transaction_id="TXN-014",
|
||||
date=date(2026, 4, 18),
|
||||
account="ACC-9876",
|
||||
amount=920.50,
|
||||
status="Matched",
|
||||
flag="None",
|
||||
reference_id="REF-2026-0014",
|
||||
counterparty="Nomura",
|
||||
currency="CHF",
|
||||
booking_date=date(2026, 4, 17),
|
||||
settlement_date=date(2026, 4, 25),
|
||||
description="Basket trade",
|
||||
),
|
||||
Transaction(
|
||||
transaction_id="TXN-015",
|
||||
date=date(2026, 4, 18),
|
||||
account="ACC-4455",
|
||||
amount=29300.00,
|
||||
status="Unmatched",
|
||||
flag="Manual Review",
|
||||
reference_id="REF-2026-0015",
|
||||
counterparty="Mizuho",
|
||||
currency="USD",
|
||||
booking_date=date(2026, 4, 17),
|
||||
settlement_date=date(2026, 4, 26),
|
||||
description="Variance swap",
|
||||
),
|
||||
Transaction(
|
||||
transaction_id="TXN-016",
|
||||
date=date(2026, 4, 19),
|
||||
account="ACC-2288",
|
||||
amount=11500.75,
|
||||
status="Matched",
|
||||
flag="None",
|
||||
reference_id="REF-2026-0016",
|
||||
counterparty="RBC",
|
||||
currency="CAD",
|
||||
booking_date=date(2026, 4, 18),
|
||||
settlement_date=date(2026, 4, 27),
|
||||
description="CDS contract",
|
||||
),
|
||||
Transaction(
|
||||
transaction_id="TXN-017",
|
||||
date=date(2026, 4, 19),
|
||||
account="ACC-3399",
|
||||
amount=76800.00,
|
||||
status="Unmatched",
|
||||
flag="Threshold Breach",
|
||||
reference_id="REF-2026-0017",
|
||||
counterparty="Scotiabank",
|
||||
currency="AUD",
|
||||
booking_date=date(2026, 4, 18),
|
||||
settlement_date=date(2026, 4, 28),
|
||||
description="Total return swap",
|
||||
),
|
||||
Transaction(
|
||||
transaction_id="TXN-018",
|
||||
date=date(2026, 4, 20),
|
||||
account="ACC-5566",
|
||||
amount=4100.25,
|
||||
status="Matched",
|
||||
flag="None",
|
||||
reference_id="REF-2026-0018",
|
||||
counterparty="TD Bank",
|
||||
currency="USD",
|
||||
booking_date=date(2026, 4, 19),
|
||||
settlement_date=date(2026, 4, 29),
|
||||
description="Equity collar",
|
||||
),
|
||||
Transaction(
|
||||
transaction_id="TXN-019",
|
||||
date=date(2026, 4, 20),
|
||||
account="ACC-7788",
|
||||
amount=18600.00,
|
||||
status="Pending",
|
||||
flag="Manual Review",
|
||||
reference_id="REF-2026-0019",
|
||||
counterparty="MUFG",
|
||||
currency="EUR",
|
||||
booking_date=date(2026, 4, 19),
|
||||
settlement_date=date(2026, 4, 30),
|
||||
description="Volatility swap",
|
||||
),
|
||||
Transaction(
|
||||
transaction_id="TXN-020",
|
||||
date=date(2026, 4, 21),
|
||||
account="ACC-9900",
|
||||
amount=8250.50,
|
||||
status="Matched",
|
||||
flag="None",
|
||||
reference_id="REF-2026-0020",
|
||||
counterparty="Sumitomo Mitsui",
|
||||
currency="GBP",
|
||||
booking_date=date(2026, 4, 20),
|
||||
settlement_date=date(2026, 5, 1),
|
||||
description="Knock-out option",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def list_transactions(status: StatusType | None = None, flag: FlagType | None = None) -> list[Transaction]:
|
||||
def list_transactions(
|
||||
status: StatusType | None = None, flag: FlagType | None = None
|
||||
) -> list[Transaction]:
|
||||
results = TRANSACTIONS
|
||||
if status:
|
||||
results = [item for item in results if item.status == status]
|
||||
|
||||
+14
-6
@@ -12,27 +12,35 @@ project_root = Path(__file__).resolve().parents[2]
|
||||
templates = Jinja2Templates(directory=str(project_root / "data" / "templates"))
|
||||
|
||||
|
||||
@router.get("/", response_class=HTMLResponse, include_in_schema=False)
|
||||
@router.get("/", response_model=None, include_in_schema=False)
|
||||
async def dashboard(
|
||||
request: Request,
|
||||
recon_job_name: str | None = Query(default=None),
|
||||
as_at_date: str | None = Query(default=None),
|
||||
) -> HTMLResponse:
|
||||
) -> HTMLResponse | RedirectResponse:
|
||||
user = request.session.get("user")
|
||||
if not user:
|
||||
return RedirectResponse(url="/login", status_code=302)
|
||||
# if not user:
|
||||
# return RedirectResponse(url="/login", status_code=302)
|
||||
|
||||
transactions = list_transactions()
|
||||
|
||||
if as_at_date:
|
||||
transactions = [item for item in transactions if item.date.isoformat() == as_at_date]
|
||||
transactions = [
|
||||
item for item in transactions if item.date.isoformat() == as_at_date
|
||||
]
|
||||
|
||||
results = [
|
||||
{
|
||||
"Transaction ID": item.transaction_id,
|
||||
"Txn ID": item.transaction_id,
|
||||
"Date": item.date.isoformat(),
|
||||
"Ref ID": item.reference_id,
|
||||
"Account": item.account,
|
||||
"Counterparty": item.counterparty,
|
||||
"Amount": f"{item.amount:,.2f}",
|
||||
"CCY": item.currency,
|
||||
"Booking": item.booking_date.isoformat(),
|
||||
"Settlement": item.settlement_date.isoformat(),
|
||||
"Description": item.description,
|
||||
"Status": item.status,
|
||||
"Flag": item.flag,
|
||||
}
|
||||
|
||||
+21
-11
@@ -16,12 +16,13 @@ nav{
|
||||
z-index: 12;
|
||||
}
|
||||
nav .menu{
|
||||
max-width: 1250px;
|
||||
max-width: none;
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
padding: 0 clamp(8px, 1vw, 16px);
|
||||
}
|
||||
.menu .logo a{
|
||||
text-decoration: none;
|
||||
@@ -73,6 +74,8 @@ body::before {
|
||||
main {
|
||||
flex: 1;
|
||||
padding-top: 70px; /* clear fixed nav */
|
||||
padding-left: clamp(4px, 0.8vw, 12px);
|
||||
padding-right: clamp(4px, 0.8vw, 12px);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
@@ -90,29 +93,34 @@ footer {
|
||||
|
||||
/* ── Dashboard content container ────────────────────────── */
|
||||
.dashboard-container {
|
||||
max-width: 1250px;
|
||||
margin: 32px auto;
|
||||
padding: 40px 24px 60px;
|
||||
width: 100%;
|
||||
margin: 8px 0;
|
||||
padding: 14px clamp(10px, 1vw, 18px);
|
||||
background: rgba(15, 17, 23, 0.45);
|
||||
backdrop-filter: blur(6px);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 86px);
|
||||
}
|
||||
.dashboard-container h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #f1f5f9;
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 12px;
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 2px solid #2d2d3a;
|
||||
letter-spacing: 0.02em;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── Data table ─────────────────────────────────────────── */
|
||||
.table-scroll {
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
max-height: calc(100vh - 340px);
|
||||
flex: 1;
|
||||
border-radius: 10px;
|
||||
min-height: 0;
|
||||
}
|
||||
.table-scroll .data-table thead th {
|
||||
position: sticky;
|
||||
@@ -181,11 +189,12 @@ footer {
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
padding: 18px 20px;
|
||||
margin-bottom: 8px;
|
||||
padding: 12px 16px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid #2d2d3a;
|
||||
border-radius: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.filter-group {
|
||||
display: flex;
|
||||
@@ -263,5 +272,6 @@ footer {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
margin-top: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -14,3 +14,8 @@ dependencies = [
|
||||
"python-dotenv>=1.2.2",
|
||||
"uvicorn[standard]>=0.45.0",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"ruff>=0.15.12",
|
||||
]
|
||||
|
||||
@@ -188,6 +188,11 @@ dependencies = [
|
||||
{ name = "uvicorn", extra = ["standard"] },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "authlib", specifier = ">=1.7.2" },
|
||||
@@ -200,6 +205,9 @@ requires-dist = [
|
||||
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.45.0" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [{ name = "ruff", specifier = ">=0.15.12" }]
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.136.0"
|
||||
@@ -494,6 +502,31 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.15.12"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/99/43/3291f1cc9106f4c63bdce7a8d0df5047fe8422a75b091c16b5e9355e0b11/ruff-0.15.12.tar.gz", hash = "sha256:ecea26adb26b4232c0c2ca19ccbc0083a68344180bba2a600605538ce51a40a6", size = 4643852, upload-time = "2026-04-24T18:17:14.305Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/6e/e78ffb61d4686f3d96ba3df2c801161843746dcbcbb17a1e927d4829312b/ruff-0.15.12-py3-none-linux_armv6l.whl", hash = "sha256:f86f176e188e94d6bdbc09f09bfd9dc729059ad93d0e7390b5a73efe19f8861c", size = 10640713, upload-time = "2026-04-24T18:17:22.841Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/08/a317bc231fb9e7b93e4ef3089501e51922ff88d6936ce5cf870c4fe55419/ruff-0.15.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e3bcd123364c3770b8e1b7baaf343cc99a35f197c5c6e8af79015c666c423a6c", size = 11069267, upload-time = "2026-04-24T18:17:30.105Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fe87510d000220aa1ed530d4448a7c696a0cae1213e5ec30e5874287b66557b5", size = 10397182, upload-time = "2026-04-24T18:17:07.177Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/e0/3310fc6d1b5e1fdea22bf3b1b807c7e187b581021b0d7d4514cccdb5fb71/ruff-0.15.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84a1630093121375a3e2a95b4a6dc7b59e2b4ee76216e32d81aae550a832d002", size = 10758012, upload-time = "2026-04-24T18:16:55.759Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/c1/a606911aee04c324ddaa883ae418f3569792fd3c4a10c50e0dd0a2311e1e/ruff-0.15.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fb129f40f114f089ebe0ca56c0d251cf2061b17651d464bb6478dc01e69f11f5", size = 10447479, upload-time = "2026-04-24T18:16:51.677Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/68/4201e8444f0894f21ab4aeeaee68aa4f10b51613514a20d80bd628d57e88/ruff-0.15.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0c862b172d695db7598426b8af465e7e9ac00a3ea2a3630ee67eb82e366aaa6", size = 11234040, upload-time = "2026-04-24T18:17:16.529Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/ff/8a6d6cf4ccc23fd67060874e832c18919d1557a0611ebef03fdb01fff11e/ruff-0.15.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2849ea9f3484c3aca43a82f484210370319e7170df4dfe4843395ddf6c57bc33", size = 12087377, upload-time = "2026-04-24T18:17:04.944Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/f6/c669cf73f5152f623d34e69866a46d5e6185816b19fcd5b6dd8a2d299922/ruff-0.15.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e77c7e51c07fe396826d5969a5b846d9cd4c402535835fb6e21ce8b28fef847", size = 11367784, upload-time = "2026-04-24T18:17:25.409Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b2f4f2f3b1026b5fb449b467d9264bf22067b600f7b6f41fc5958909f449d0", size = 11344088, upload-time = "2026-04-24T18:17:12.258Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/8d/49afab3645e31e12c590acb6d3b5b69d7aab5b81926dbaf7461f9441f37a/ruff-0.15.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9ba3b8f1afd7e2e43d8943e55f249e13f9682fde09711644a6e7290eb4f3e339", size = 11271770, upload-time = "2026-04-24T18:17:02.457Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/06/33f41fe94403e2b755481cdfb9b7ef3e4e0ed031c4581124658d935d52b4/ruff-0.15.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e852ba9fdc890655e1d78f2df1499efbe0e54126bd405362154a75e2bde159c5", size = 10719355, upload-time = "2026-04-24T18:17:27.648Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/59/18aa4e014debbf559670e4048e39260a85c7fcee84acfd761ac01e7b8d35/ruff-0.15.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dd8aed930da53780d22fc70bdf84452c843cf64f8cb4eb38984319c24c5cd5fd", size = 10462758, upload-time = "2026-04-24T18:17:32.347Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/e7/cc9f16fd0f3b5fddcbd7ec3d6ae30c8f3fde1047f32a4093a98d633c6570/ruff-0.15.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:01da3988d225628b709493d7dc67c3b9b12c0210016b08690ef9bd27970b262b", size = 10953498, upload-time = "2026-04-24T18:17:20.674Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/7a/a9ba7f98c7a575978698f4230c5e8cc54bbc761af34f560818f933dafa0c/ruff-0.15.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9cae0f92bd5700d1213188b31cd3bdd2b315361296d10b96b8e2337d3d11f53e", size = 11447765, upload-time = "2026-04-24T18:17:09.755Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/f9/0ae446942c846b8266059ad8a30702a35afae55f5cdc54c5adf8d7afdc27/ruff-0.15.12-py3-none-win32.whl", hash = "sha256:d0185894e038d7043ba8fd6aee7499ece6462dc0ea9f1e260c7451807c714c20", size = 10657277, upload-time = "2026-04-24T18:17:18.591Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.whl", hash = "sha256:c87a162d61ab3adca47c03f7f717c68672edec7d1b5499e652331780fe74950d", size = 11837758, upload-time = "2026-04-24T18:17:00.113Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/98/6beb4b351e472e5f4c4613f7c35a5290b8be2497e183825310c4c3a3984b/ruff-0.15.12-py3-none-win_arm64.whl", hash = "sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f", size = 11120821, upload-time = "2026-04-24T18:16:57.979Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "starlette"
|
||||
version = "1.0.0"
|
||||
|
||||
Reference in New Issue
Block a user