Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Sample code to reproduce the problem
Please provide the following information:
- OS: [e.g. Windows]
- Browser (if you're reporting a bug in jupyter): [e.g. Edge, Firefox, Chrome, Safari]
- Python version: [e.g. 3.10.11]
- Python version: [e.g. 3.11.9]
- PyRIT version: [e.g. 0.1.0 or installed from main branch in editable mode]
- version of Python packages: please run the following snippet and paste the output:
```python
Expand Down
18 changes: 9 additions & 9 deletions .github/instructions/style-guide.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def validate_input(self, data: dict) -> None: # Should be private
- **EVERY** function MUST declare its return type
- Use `None` for functions that don't return a value

### Modern Type Syntax (Python 3.10+)
### Modern Type Syntax
- Use built-in generics and union syntax:
- `list[str]` not `List[str]`
- `dict[str, Any]` not `Dict[str, Any]`
Expand Down Expand Up @@ -210,17 +210,17 @@ Within the same package, import from the specific file to avoid circular imports

### Typing Backports (`typing_extensions`)

For typing features that don't exist on every supported Python (`Self`,
`override`, `TypeAlias`, `Unpack`, `NotRequired`, etc.), import from
``typing_extensions`` rather than ``typing``. `typing_extensions` is already a
transitive dependency (pulled in by ``pydantic``) and works across all supported
Python versions, so this avoids per-version branching and ``# type: ignore`` noise.
Import typing features from ``typing`` when they exist on every supported Python
version. For newer features, import from ``typing_extensions``. It is already a
transitive dependency (pulled in by ``pydantic``), so this avoids per-version
branching and ``# type: ignore`` noise.

```python
# CORRECT — works on 3.10+
from typing_extensions import Self, override
# CORRECT — works on every supported Python version
from typing import Self
from typing_extensions import override

# INCORRECT — `Self` is 3.11+, `override` is 3.12+, breaks on older runtimes
# INCORRECT — `override` is 3.12+ and breaks on Python 3.11
from typing import Self, override
```

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build_and_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ jobs:
fail-fast: true
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python: ["3.10", "3.11", "3.12", "3.13", "3.14"]
python: ["3.11", "3.12", "3.13", "3.14"]
package_name: ["pyrit"]
package_extras: ["dev", "dev_all"]
runs-on: ${{ matrix.os }}
Expand Down
2 changes: 1 addition & 1 deletion doc/code/targets/10_3_websocket_copilot_target.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.18"
"version": "3.11.0"
}
},
"nbformat": 4,
Expand Down
2 changes: 1 addition & 1 deletion doc/getting_started/install_local.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Install PyRIT directly into your Python environment for full control and easy in

## Prerequisites

- Python 3.10, 3.11, 3.12, 3.13, or 3.14 (check with `python --version`)
- Python 3.11, 3.12, 3.13, or 3.14 (check with `python --version`)

## Install with pip or uv

Expand Down
2 changes: 1 addition & 1 deletion doc/getting_started/install_local_dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Set up a PyRIT development environment on your local machine.
wget -qO- https://astral.sh/uv/install.sh | sh
```

2. **Python 3.10-3.14**: PyRIT supports these versions, and CI tests all of them. The repository does
2. **Python 3.11-3.14**: PyRIT supports these versions, and CI tests all of them. The repository does
not pin an interpreter, so `uv` selects a compatible one for you (downloading it if needed).

3. **Git**. Git is required to clone the repo locally. It is available to download [here](https://git-scm.com/downloads).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.13"
"version": "3.11.0"
}
},
"nbformat": 4,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.13"
"version": "3.11.0"
}
},
"nbformat": 4,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.13"
"version": "3.11.0"
}
},
"nbformat": 4,
Expand Down
2 changes: 1 addition & 1 deletion infra/DEPLOY_NEW_INSTANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ All authenticated users on a GUI instance are **fully trusted**. Any user with E
| Prerequisite | Notes |
| --- | --- |
| [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) 2.84+ | Version 2.77 has a known `content-already-consumed` bug |
| Python 3.10+ | For running the deployment script |
| Python 3.11+ | For running the deployment script |
| `az login` with Graph permissions | The script creates Entra app registrations, which requires Graph API access. Run `az login --scope https://graph.microsoft.com//.default` |
| Azure permissions | **Owner** (or Contributor + User Access Administrator) on the subscription, and **Application Administrator** in Entra ID for app registrations and Graph API operations |
| Container image pushed to ACR | Build and push before deploying (see [Building the Image](#building-the-image)) |
Expand Down
11 changes: 5 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,12 @@ keywords = [
]
classifiers = [
"Development Status :: 3 - Alpha",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
]
requires-python = ">=3.10, <3.15"
requires-python = ">=3.11, <3.15"
dependencies = [
"aiofiles>=24,<26",
"alembic>=1.16.0",
Expand All @@ -43,7 +42,6 @@ dependencies = [
"confusable-homoglyphs>=3.3.1",
"ecoji>=0.1.1",
"datasets>=4.8.0",
"exceptiongroup>=1.2.0; python_version < '3.11'",
"fastapi>=0.133.0",
"httpx[http2]>=0.27.2",
"jinja2>=3.1.6",
Expand Down Expand Up @@ -188,7 +186,7 @@ all = "error"
# re-enabled as the code is cleaned up.
# missing-type-argument is clean under numpy>=2.4 (Python 3.14) but flags 38
# bare `np.ndarray` generics under numpy 2.2.6 -- the version the lock pins for
# Python 3.10-3.13, i.e. CI's 3.11 -- across 5 files (analytics/conversation_analytics,
# Python 3.11-3.13, i.e. CI's 3.11 -- across 5 files (analytics/conversation_analytics,
# converter/transparency_attack_converter, score/scorer_evaluation/*).
# Annotating those generics is real code work, so keep the rule deferred until
# that numpy-generics debt is addressed.
Expand All @@ -205,7 +203,7 @@ unsound-assignment = "ignore"
unused-type-ignore-comment = "ignore"
# ty 0.0.43 (bumped from 0.0.32 in #1919) activated this rule under
# [tool.ty.rules] all = "error". Adopting @override across pyrit/ (~695 methods)
# would require the typing_extensions backport on Python 3.10/3.11
# would require the typing_extensions backport on Python 3.11
# (typing.override is 3.12+), which adds a direct dep and ongoing import-path
# friction. Suppressing the rule keeps the codebase consistent for now.
missing-override-decorator = "ignore"
Expand Down Expand Up @@ -306,7 +304,7 @@ formats = "ipynb,py:percent"

[tool.ruff]
line-length = 120
target-version = "py310"
target-version = "py311"

[tool.ruff.lint]
preview = true
Expand Down Expand Up @@ -407,6 +405,7 @@ ignore = [
"DOC502", # Raised exception is not explicitly raised
"PERF203", # try-except-in-loop (intentional per-item error handling)
"SIM117", # multiple-with-statements (combining often exceeds line length)
"UP042", # replace-str-enum: StrEnum changes str(member) from "Class.MEMBER" to member.value
]
extend-select = [
"D204", # 1 blank line required after class docstring
Expand Down
6 changes: 3 additions & 3 deletions pyrit/auth/azure_storage_auth.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.

from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from urllib.parse import urlparse

from azure.identity.aio import DefaultAzureCredential
Expand Down Expand Up @@ -31,7 +31,7 @@ async def get_user_delegation_key_async(blob_service_client: BlobServiceClient)
Returns:
UserDelegationKey: A user delegation key valid for one day.
"""
delegation_key_start_time = datetime.now(tz=timezone.utc)
delegation_key_start_time = datetime.now(tz=UTC)
delegation_key_expiry_time = delegation_key_start_time + timedelta(days=1)

return await blob_service_client.get_user_delegation_key(
Expand Down Expand Up @@ -79,7 +79,7 @@ async def get_sas_token_async(container_url: str) -> str:
storage_account_name = parsed_url.netloc.split(".")[0]

# Set start_time 5 minutes before the current time to account for any clock skew
start_time = datetime.now(tz=timezone.utc) - timedelta(minutes=5)
start_time = datetime.now(tz=UTC) - timedelta(minutes=5)
expiry_time = start_time + timedelta(days=1)

sas_token = generate_container_sas(
Expand Down
10 changes: 5 additions & 5 deletions pyrit/auth/copilot_authenticator.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import logging
import os
import sys
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from typing import Any

from msal_extensions import FilePersistence, build_encrypted_persistence
Expand Down Expand Up @@ -217,8 +217,8 @@ async def _get_cached_token_if_available_and_valid_async(self) -> dict[str, Any]

expires_at = token_data.get("expires_at")
if expires_at:
expiry_time = datetime.fromtimestamp(expires_at, tz=timezone.utc)
current_time = datetime.now(timezone.utc)
expiry_time = datetime.fromtimestamp(expires_at, tz=UTC)
current_time = datetime.now(UTC)

# This should prevent most mid-request failures due to token expiration
expiry_with_buffer = expiry_time - timedelta(seconds=self.EXPIRY_BUFFER_SECONDS)
Expand Down Expand Up @@ -265,11 +265,11 @@ def _save_token_to_cache(self, *, token: str, expires_in: int | None = None) ->
"access_token": token,
"token_type": "Bearer",
"claims": self._current_claims,
"cached_at": datetime.now(timezone.utc).timestamp(),
"cached_at": datetime.now(UTC).timestamp(),
}

if expires_in:
expires_at = datetime.now(timezone.utc).timestamp() + expires_in
expires_at = datetime.now(UTC).timestamp() + expires_in
token_data["expires_at"] = expires_at
token_data["expires_in"] = expires_in

Expand Down
6 changes: 3 additions & 3 deletions pyrit/backend/mappers/attack_mappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import logging
import time
import uuid
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from pathlib import Path
from typing import TYPE_CHECKING, cast
from urllib.parse import quote, urlparse
Expand Down Expand Up @@ -98,7 +98,7 @@ async def _get_sas_for_container_async(*, container_url: str) -> str:
container_name = parsed.path.strip("/")
storage_account_name = parsed.netloc.split(".")[0]

start_time = datetime.now(tz=timezone.utc) - timedelta(minutes=5)
start_time = datetime.now(tz=UTC) - timedelta(minutes=5)
expiry_time = start_time + timedelta(hours=1)

credential = DefaultAzureCredential()
Expand Down Expand Up @@ -263,7 +263,7 @@ def _resolve_summary_timestamps(ar: AttackResult) -> tuple[datetime, datetime]:
elif ar.timestamp is not None:
created_at = ar.timestamp
else:
created_at = datetime.now(timezone.utc)
created_at = datetime.now(UTC)
updated_at = ar.timestamp if ar.timestamp is not None else created_at
return created_at, updated_at

Expand Down
12 changes: 4 additions & 8 deletions pyrit/backend/models/attacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"""

import uuid
from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import Annotated, Any, Literal, cast

from pydantic import BaseModel, Field, computed_field, field_serializer, model_validator
Expand Down Expand Up @@ -203,7 +203,7 @@ def role(self) -> ChatMessageRole:
@property
def created_at(self) -> datetime:
"""The timestamp of the first piece."""
return self.message_pieces[0].timestamp if self.message_pieces else datetime.now(timezone.utc)
return self.message_pieces[0].timestamp if self.message_pieces else datetime.now(UTC)


class AttackSummary(AttackResult):
Expand All @@ -223,12 +223,8 @@ class AttackSummary(AttackResult):
# Mapper-populated presentation fields (need external stats / metadata).
message_count: int = Field(default=0, description="Total number of messages in the attack")
last_message_preview: str | None = Field(default=None, description="Preview of the last message")
created_at: datetime = Field(
default_factory=lambda: datetime.now(timezone.utc), description="Attack creation timestamp"
)
updated_at: datetime = Field(
default_factory=lambda: datetime.now(timezone.utc), description="Last update timestamp"
)
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC), description="Attack creation timestamp")
updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC), description="Last update timestamp")

@field_serializer("related_conversations")
def _serialize_related_conversations(
Expand Down
4 changes: 2 additions & 2 deletions pyrit/backend/routes/health.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
Health check endpoints.
"""

from datetime import datetime, timezone
from datetime import UTC, datetime

from fastapi import APIRouter

Expand All @@ -27,6 +27,6 @@ async def health_check_async() -> dict[str, str]:
"""
return {
"status": "healthy",
"timestamp": datetime.now(timezone.utc).isoformat(),
"timestamp": datetime.now(UTC).isoformat(),
"service": "pyrit-backend",
}
18 changes: 9 additions & 9 deletions pyrit/backend/services/attack_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import mimetypes
import uuid
from collections.abc import Mapping, Sequence
from datetime import datetime, timezone
from datetime import UTC, datetime
from functools import lru_cache
from pathlib import Path
from typing import Any, Literal, cast
Expand Down Expand Up @@ -354,7 +354,7 @@ async def create_attack_async(self, *, request: CreateAttackRequest) -> CreateAt
target_obj = target_service.get_target_object(target_registry_name=request.target_registry_name)
target_identifier = target_obj.get_identifier() if target_obj else None

now = datetime.now(timezone.utc)
now = datetime.now(UTC)

# Merge source label with any user-supplied labels
labels = dict(request.labels) if request.labels else {}
Expand Down Expand Up @@ -448,7 +448,7 @@ async def update_attack_async(self, *, attack_result_id: str, request: UpdateAtt
attack_result_id=attack_result_id,
update_fields={
"outcome": new_outcome.value,
"timestamp": datetime.now(timezone.utc),
"timestamp": datetime.now(UTC),
},
)

Expand Down Expand Up @@ -482,7 +482,7 @@ async def get_conversations_async(self, *, attack_result_id: str) -> AttackConve
created_at = stats.created_at if stats else None
# SQLite returns naive datetimes — normalize to UTC (same pattern as the UTCDateTime column type)
if created_at is not None and created_at.tzinfo is None:
created_at = created_at.replace(tzinfo=timezone.utc)
created_at = created_at.replace(tzinfo=UTC)
conversations.append(
ConversationSummary(
conversation_id=conv_id,
Expand All @@ -499,7 +499,7 @@ async def get_conversations_async(self, *, attack_result_id: str) -> AttackConve
# have no stored messages yet so created_at is None — treat them as the most
# recent (they were just created) so they sort after older conversations
# instead of jumping to an arbitrary position.
now = datetime.now(timezone.utc)
now = datetime.now(UTC)
conversations.sort(key=lambda c: c.created_at or now)

return AttackConversationsResponse(
Expand Down Expand Up @@ -527,7 +527,7 @@ async def create_related_conversation_async(
return None

ar = results[0]
now = datetime.now(timezone.utc)
now = datetime.now(UTC)

# Validate that both or neither branching fields are provided
if (request.source_conversation_id is None) != (request.cutoff_index is None):
Expand Down Expand Up @@ -589,7 +589,7 @@ async def update_main_conversation_async(
return UpdateMainConversationResponse(
attack_result_id=attack_result_id,
conversation_id=target_conv_id,
updated_at=datetime.now(timezone.utc),
updated_at=datetime.now(UTC),
)

# Verify the conversation belongs to this attack (main or related)
Expand All @@ -612,7 +612,7 @@ async def update_main_conversation_async(
# visible in the GUI and fetchable via get_conversation_messages.
updated_pruned.append(ar.conversation_id)

now = datetime.now(timezone.utc)
now = datetime.now(UTC)

self._memory.update_attack_result_by_id(
attack_result_id=attack_result_id,
Expand Down Expand Up @@ -805,7 +805,7 @@ async def _update_attack_after_message_async(
request_converter_configurations: Resolved request converter configurations used for this message.
response_converter_configurations: Resolved response converter configurations used for this message.
"""
update_fields: dict[str, Any] = {"timestamp": datetime.now(timezone.utc)}
update_fields: dict[str, Any] = {"timestamp": datetime.now(UTC)}

request_converter_ids = self._get_converter_identifiers(configurations=request_converter_configurations)
response_converter_ids = self._get_converter_identifiers(configurations=response_converter_configurations)
Expand Down
Loading
Loading