mirror of
https://github.com/msoedov/agentic_security.git
synced 2026-06-24 22:29:56 +02:00
Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 317fd33480 | |||
| 56e3c91af2 | |||
| 594f8960e8 | |||
| 51a9b5de5f | |||
| 0a555b8427 | |||
| aa27817f94 | |||
| 8bd76b9f05 | |||
| 6f3c522d59 | |||
| 896ca95ae2 | |||
| f85c77d622 | |||
| 684ba0b70d | |||
| 21b43b18e7 | |||
| d20c1a3d0d | |||
| ebac62e21a | |||
| 21180b53e5 | |||
| a8808b3165 | |||
| 87c26ca3cc | |||
| e06c6932de | |||
| 51fcc38885 | |||
| 06a7bbfd87 | |||
| 50f3e52445 | |||
| 2bd62c21be | |||
| d5d5dd48aa | |||
| bb2437197a | |||
| 51bb79aa6d | |||
| 94f034fa9f | |||
| f69de8720b | |||
| 7c9d83b1a7 | |||
| a9d4d671ba | |||
| 554a219535 | |||
| 32e99006bf | |||
| 8c09d65687 | |||
| a2842755fa | |||
| b923f7fea5 | |||
| 7f30a8ff7a | |||
| 909cbd69b4 | |||
| 4f0ebf180e | |||
| 6be9673aa7 | |||
| bd9ed97d85 | |||
| 3c88a4d6ba | |||
| 2001eeb125 | |||
| a26b5dd448 | |||
| 716a0f67f3 | |||
| c1bbf6b422 |
@@ -17,3 +17,4 @@ inv/
|
||||
scripts/
|
||||
docx/
|
||||
agentic_security.toml
|
||||
/venv
|
||||
@@ -84,6 +84,7 @@ agentic_security --port=PORT --host=HOST
|
||||
## UI 🧙
|
||||
|
||||
<img width="100%" alt="booking-screen" src="https://res.cloudinary.com/dq0w2rtm9/image/upload/v1736433557/z0bsyzhsqlgcr3w4ovwp.gif">
|
||||
<img width="100%" alt="booking-screen" src="https://res.cloudinary.com/dq0w2rtm9/image/upload/v1741192668/final_aa9jhb.gif">
|
||||
|
||||
## LLM kwargs
|
||||
|
||||
@@ -408,10 +409,15 @@ For more detailed information on how to use Agentic Security, including advanced
|
||||
|
||||
## Roadmap and Future Goals
|
||||
|
||||
- \[ \] Expand dataset variety
|
||||
- \[ \] Introduce two new attack vectors
|
||||
- \[ \] Develop initial attacker LLM
|
||||
- \[ \] Complete integration of OWASP Top 10 classification
|
||||
|
||||
|
||||
We’re just getting started! Here’s what’s on the horizon:
|
||||
|
||||
- **RL-Powered Attacks**: An attacker LLM trained with reinforcement learning to dynamically evolve jailbreaks and outsmart defenses.
|
||||
- **Massive Dataset Expansion**: Scaling to 100,000+ prompts across text, image, and audio modalities—curated for real-world threats.
|
||||
- **Daily Attack Updates**: Fresh attack vectors delivered daily, keeping your scans ahead of the curve.
|
||||
- **Community Modules**: A plug-and-play ecosystem where you can share and deploy custom probes, datasets, and integrations.
|
||||
|
||||
|
||||
| Tool | Source | Integrated |
|
||||
|-------------------------|-------------------------------------------------------------------------------|------------|
|
||||
@@ -439,4 +445,9 @@ Before contributing, please read the contributing guidelines.
|
||||
|
||||
Agentic Security is released under the Apache License v2.
|
||||
|
||||
|
||||
## 🚫 No Cryptocurrency Affiliation
|
||||
|
||||
Agentic Security is focused solely on AI security and has no affiliation with cryptocurrency projects, blockchain technologies, or related initiatives. Our mission is to advance the safety and reliability of AI systems—no tokens, no coins, just code.
|
||||
|
||||
## Contact us
|
||||
|
||||
@@ -6,12 +6,30 @@ from agentic_security.core.app import expand_secrets
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_globals():
|
||||
"""
|
||||
Reset globals (_secrets, current_run, tools_inbox, stop_event) before each test.
|
||||
This ensures tests run in a clean state.
|
||||
"""
|
||||
from agentic_security.core.app import _secrets, current_run, get_tools_inbox, get_stop_event
|
||||
_secrets.clear()
|
||||
current_run["spec"] = ""
|
||||
current_run["id"] = ""
|
||||
# Clear tools_inbox queue
|
||||
queue = get_tools_inbox()
|
||||
while not queue.empty():
|
||||
queue.get_nowait()
|
||||
# Reset stop_event if it is set
|
||||
event = get_stop_event()
|
||||
if event.is_set():
|
||||
event.clear()
|
||||
def setup_env_vars():
|
||||
# Set up environment variables for testing
|
||||
os.environ["TEST_ENV_VAR"] = "test_value"
|
||||
|
||||
|
||||
def test_expand_secrets_with_env_var():
|
||||
os.environ["TEST_ENV_VAR"] = "test_value"
|
||||
secrets = {"secret_key": "$TEST_ENV_VAR"}
|
||||
expand_secrets(secrets)
|
||||
assert secrets["secret_key"] == "test_value"
|
||||
@@ -27,3 +45,180 @@ def test_expand_secrets_without_dollar_sign():
|
||||
secrets = {"secret_key": "plain_value"}
|
||||
expand_secrets(secrets)
|
||||
assert secrets["secret_key"] == "plain_value"
|
||||
|
||||
import asyncio
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import ORJSONResponse
|
||||
from agentic_security.core.app import create_app, get_tools_inbox, get_stop_event, get_current_run, set_current_run, get_secrets, set_secrets, expand_secrets
|
||||
|
||||
class DummyLLMSpec:
|
||||
"""A dummy LLMSpec for testing purposes."""
|
||||
pass
|
||||
|
||||
def test_create_app():
|
||||
"""Test that create_app returns a FastAPI app with ORJSONResponse."""
|
||||
app = create_app()
|
||||
assert isinstance(app, FastAPI)
|
||||
assert app.router.default_response_class == ORJSONResponse
|
||||
|
||||
def test_get_tools_inbox():
|
||||
"""Test that get_tools_inbox returns a Queue instance."""
|
||||
queue = get_tools_inbox()
|
||||
from asyncio import Queue
|
||||
assert isinstance(queue, Queue)
|
||||
|
||||
def test_get_stop_event():
|
||||
"""Test that get_stop_event returns an Event instance."""
|
||||
event = get_stop_event()
|
||||
from asyncio import Event
|
||||
assert isinstance(event, Event)
|
||||
|
||||
def test_get_current_run_initial():
|
||||
"""Test that get_current_run returns the initial current run dictionary."""
|
||||
current = get_current_run()
|
||||
# The initial dictionary should have an empty spec and id.
|
||||
assert current["spec"] == ""
|
||||
assert current["id"] == ""
|
||||
|
||||
def test_set_current_run():
|
||||
"""Test that set_current_run updates the current run with the dummy LLMSpec."""
|
||||
dummy_spec = DummyLLMSpec()
|
||||
updated = set_current_run(dummy_spec)
|
||||
assert updated["spec"] is dummy_spec
|
||||
# Ensure that the id is computed as hash(id(dummy_spec))
|
||||
expected_id = hash(id(dummy_spec))
|
||||
assert updated["id"] == expected_id
|
||||
|
||||
def test_get_and_set_secrets():
|
||||
"""Test that set_secrets updates the secrets dictionary and get_secrets returns the updated values."""
|
||||
# Clear any previously set secrets
|
||||
secrets_before = get_secrets().copy()
|
||||
os.environ["MY_SECRET"] = "secret_value"
|
||||
new_secrets = {"key1": "$MY_SECRET", "key2": "plain"}
|
||||
updated = set_secrets(new_secrets)
|
||||
assert updated["key1"] == "secret_value"
|
||||
assert updated["key2"] == "plain"
|
||||
|
||||
def test_expand_secrets_multiple_keys():
|
||||
"""Test expand_secrets with multiple keys, including one with an environment variable,
|
||||
one with a non-existent variable, and one that is plain."""
|
||||
os.environ["TEST_ENV_VAR"] = "test_value"
|
||||
secrets = {"env_key": "$TEST_ENV_VAR", "nonexistent_key": "$NON_EXISTENT", "plain_key": "value"}
|
||||
expand_secrets(secrets)
|
||||
assert secrets["env_key"] == "test_value"
|
||||
# For a non-existent environment variable, os.getenv returns None
|
||||
assert secrets["nonexistent_key"] is None
|
||||
# Plain values should not be changed.
|
||||
assert secrets["plain_key"] == "value"
|
||||
def test_expand_secrets_with_space_after_dollar():
|
||||
"""Test expand_secrets when the value has a dollar sign followed by a space.
|
||||
Since the value does not start strictly with "$", the secret remains unchanged.
|
||||
Also verifies that the stripping in expand_secrets (via strip("$"))
|
||||
will remove both dollar and any whitespace if the value actually started with '$'.
|
||||
"""
|
||||
os.environ["SPACED_VAR"] = "spaced_value"
|
||||
secrets = {"key": "$ SPACED_VAR"}
|
||||
expand_secrets(secrets)
|
||||
# " $ SPACED_VAR" after strip("$") becomes " SPACED_VAR" which is not a valid env key so returns None.
|
||||
assert secrets["key"] is None
|
||||
|
||||
def test_set_secrets_update_existing():
|
||||
"""Test that set_secrets updates an existing secret and retains previously set keys."""
|
||||
os.environ["VAR1"] = "value1"
|
||||
os.environ["VAR2"] = "value2"
|
||||
result_first = set_secrets({"a": "$VAR1", "b": "b_val"})
|
||||
assert result_first["a"] == "value1"
|
||||
# Change VAR1 in environment and update secret "a", and add secret "c"
|
||||
os.environ["VAR1"] = "new_value1"
|
||||
result_second = set_secrets({"a": "$VAR1", "c": "$VAR2"})
|
||||
assert result_second["a"] == "new_value1"
|
||||
assert result_second["b"] == "b_val"
|
||||
assert result_second["c"] == "value2"
|
||||
|
||||
def test_tools_inbox_state():
|
||||
"""Test that get_tools_inbox returns the same queue instance
|
||||
and that the queue state persists across multiple calls.
|
||||
"""
|
||||
from asyncio import Queue
|
||||
inbox1 = get_tools_inbox()
|
||||
inbox1.put_nowait("message")
|
||||
inbox2 = get_tools_inbox()
|
||||
# inbox2 should contain the "message" from inbox1
|
||||
msg = inbox2.get_nowait()
|
||||
assert msg == "message"
|
||||
|
||||
def test_stop_event_state():
|
||||
"""Test that stop_event can be set and cleared, and its state persists."""
|
||||
event = get_stop_event()
|
||||
# Initially the event should not be set
|
||||
assert not event.is_set()
|
||||
event.set()
|
||||
assert event.is_set()
|
||||
event.clear()
|
||||
assert not event.is_set()
|
||||
|
||||
def test_set_current_run_returns_global_dict():
|
||||
"""Test that set_current_run returns the same global current_run dictionary
|
||||
as returned by get_current_run.
|
||||
"""
|
||||
dummy_spec = DummyLLMSpec()
|
||||
updated = set_current_run(dummy_spec)
|
||||
current = get_current_run()
|
||||
assert updated is current
|
||||
def test_get_secrets_initial():
|
||||
"""Test that get_secrets returns an empty dictionary initially."""
|
||||
assert get_secrets() == {}
|
||||
|
||||
def test_set_secrets_empty():
|
||||
"""Test that setting an empty secrets dictionary does not modify existing secrets."""
|
||||
# first set initial secrets
|
||||
initial = {"key": "value"}
|
||||
set_secrets(initial)
|
||||
# update with an empty dict – the existing keys remain
|
||||
result = set_secrets({})
|
||||
assert result == initial
|
||||
|
||||
def test_update_current_run_twice():
|
||||
"""Test updating current run twice with different LLMSpec values."""
|
||||
dummy1 = DummyLLMSpec()
|
||||
dummy2 = DummyLLMSpec()
|
||||
set_current_run(dummy1)
|
||||
first = get_current_run().copy()
|
||||
set_current_run(dummy2)
|
||||
second = get_current_run().copy()
|
||||
# first update should hold dummy1, second should hold dummy2
|
||||
assert first["spec"] is dummy1
|
||||
assert second["spec"] is dummy2
|
||||
# Ensure that id has changed (using hash(id(dummy_spec)))
|
||||
assert first["id"] != second["id"]
|
||||
|
||||
def test_expand_secrets_trailing_whitespace():
|
||||
"""Test expand_secrets when the secret value has trailing whitespace after the dollar sign.
|
||||
The trailing whitespace remains after stripping only the dollar sign, so the looked-up environment variable key will not match.
|
||||
"""
|
||||
os.environ["TRIM_TEST"] = "trimmed"
|
||||
secrets = {"key": "$TRIM_TEST "}
|
||||
expand_secrets(secrets)
|
||||
# Since "TRIM_TEST " (with trailing space) is not set in the environment, the secret should be None.
|
||||
assert secrets["key"] is None
|
||||
def test_expand_secrets_empty_dict():
|
||||
"""Test expand_secrets with an empty dictionary does nothing."""
|
||||
secrets = {}
|
||||
expand_secrets(secrets)
|
||||
assert secrets == {}
|
||||
|
||||
def test_expand_secrets_with_non_string_value():
|
||||
"""Test that expand_secrets raises an AttributeError when a secret value is not a string."""
|
||||
secrets = {"key": 123}
|
||||
with pytest.raises(AttributeError):
|
||||
expand_secrets(secrets)
|
||||
|
||||
def test_expand_secrets_multiple_dollar_signs():
|
||||
"""Test expand_secrets with a value that contains multiple leading dollar signs.
|
||||
The extra dollar signs are removed by the strip method.
|
||||
"""
|
||||
os.environ["MULTI_DOLLAR_VAR"] = "multi_value"
|
||||
secrets = {"key": "$$MULTI_DOLLAR_VAR"}
|
||||
expand_secrets(secrets)
|
||||
# After stripping, "$$MULTI_DOLLAR_VAR".strip("$") returns "MULTI_DOLLAR_VAR"
|
||||
assert secrets["key"] == "multi_value"
|
||||
@@ -81,7 +81,11 @@ def generate_banner(
|
||||
|
||||
def init_banner():
|
||||
ver = version("agentic_security")
|
||||
print(generate_banner(version=ver))
|
||||
try:
|
||||
print(generate_banner(version=ver))
|
||||
except Exception:
|
||||
# UnicodeEncodeError with codec on some systems
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -107,6 +107,17 @@ async def process_prompt_batch(
|
||||
return total_tokens, failures
|
||||
|
||||
|
||||
async def with_error_handling(agen):
|
||||
try:
|
||||
async for t in agen:
|
||||
yield t
|
||||
except Exception as e:
|
||||
logger.exception("Scan failed")
|
||||
yield ScanResult.status_msg(f"Scan failed: {str(e)}")
|
||||
finally:
|
||||
yield ScanResult.status_msg("Scan completed.")
|
||||
|
||||
|
||||
async def perform_single_shot_scan(
|
||||
request_factory,
|
||||
max_budget: int,
|
||||
@@ -120,125 +131,117 @@ async def perform_single_shot_scan(
|
||||
max_budget = max_budget * BUDGET_MULTIPLIER
|
||||
selected_datasets = [m for m in datasets if m["selected"]]
|
||||
request_factory = multi_modality_spec(request_factory)
|
||||
try:
|
||||
yield ScanResult.status_msg("Loading datasets...")
|
||||
prompt_modules = prepare_prompts(
|
||||
dataset_names=[m["dataset_name"] for m in selected_datasets],
|
||||
budget=max_budget,
|
||||
tools_inbox=tools_inbox,
|
||||
options=[m.get("opts", {}) for m in selected_datasets],
|
||||
)
|
||||
yield ScanResult.status_msg("Datasets loaded. Starting scan...")
|
||||
yield ScanResult.status_msg("Loading datasets...")
|
||||
prompt_modules = prepare_prompts(
|
||||
dataset_names=[m["dataset_name"] for m in selected_datasets],
|
||||
budget=max_budget,
|
||||
tools_inbox=tools_inbox,
|
||||
options=[m.get("opts", {}) for m in selected_datasets],
|
||||
)
|
||||
yield ScanResult.status_msg("Datasets loaded. Starting scan...")
|
||||
|
||||
errors = []
|
||||
refusals = []
|
||||
outputs = []
|
||||
total_prompts = sum(len(m.prompts) for m in prompt_modules if not m.lazy)
|
||||
processed_prompts = 0
|
||||
errors = []
|
||||
refusals = []
|
||||
outputs = []
|
||||
total_prompts = sum(len(m.prompts) for m in prompt_modules if not m.lazy)
|
||||
processed_prompts = 0
|
||||
|
||||
optimizer = (
|
||||
Optimizer([Real(0, 1)], base_estimator="GP", n_initial_points=25)
|
||||
if optimize
|
||||
else None
|
||||
)
|
||||
failure_rates = []
|
||||
optimizer = (
|
||||
Optimizer([Real(0, 1)], base_estimator="GP", n_initial_points=25)
|
||||
if optimize
|
||||
else None
|
||||
)
|
||||
failure_rates = []
|
||||
|
||||
total_tokens = 0
|
||||
total_tokens = 0
|
||||
tokens = 0
|
||||
should_stop = False
|
||||
for module in prompt_modules:
|
||||
if should_stop:
|
||||
break
|
||||
tokens = 0
|
||||
should_stop = False
|
||||
for module in prompt_modules:
|
||||
if should_stop:
|
||||
break
|
||||
tokens = 0
|
||||
module_failures = 0
|
||||
module_size = 0 if module.lazy else len(module.prompts)
|
||||
logger.info(f"Scanning {module.dataset_name} {module_size}")
|
||||
module_prompts = 0 # Reset for each module
|
||||
module_failures = 0
|
||||
module_size = 0 if module.lazy else len(module.prompts)
|
||||
logger.info(f"Scanning {module.dataset_name} {module_size}")
|
||||
module_prompts = 0 # Reset for each module
|
||||
|
||||
async for prompt in generate_prompts(module.prompts):
|
||||
if stop_event and stop_event.is_set():
|
||||
stop_event.clear()
|
||||
logger.info("Scan stopped by user.")
|
||||
yield ScanResult.status_msg("Scan stopped by user.")
|
||||
return
|
||||
async for prompt in generate_prompts(module.prompts):
|
||||
if stop_event and stop_event.is_set():
|
||||
stop_event.clear()
|
||||
logger.info("Scan stopped by user.")
|
||||
yield ScanResult.status_msg("Scan stopped by user.")
|
||||
return
|
||||
|
||||
processed_prompts += 1
|
||||
module_prompts += 1 # Fixed increment syntax
|
||||
# Calculate progress based on total processed prompts
|
||||
progress = (
|
||||
100 * processed_prompts / total_prompts if total_prompts else 0
|
||||
)
|
||||
processed_prompts += 1
|
||||
module_prompts += 1 # Fixed increment syntax
|
||||
# Calculate progress based on total processed prompts
|
||||
progress = 100 * processed_prompts / total_prompts if total_prompts else 0
|
||||
progress = progress % 100
|
||||
|
||||
total_tokens -= tokens
|
||||
start = time.time()
|
||||
tokens, failed = await process_prompt(
|
||||
request_factory,
|
||||
prompt,
|
||||
tokens,
|
||||
module.dataset_name,
|
||||
refusals,
|
||||
errors,
|
||||
outputs,
|
||||
)
|
||||
end = time.time()
|
||||
total_tokens += tokens
|
||||
total_tokens -= tokens
|
||||
start = time.time()
|
||||
tokens, failed = await process_prompt(
|
||||
request_factory,
|
||||
prompt,
|
||||
tokens,
|
||||
module.dataset_name,
|
||||
refusals,
|
||||
errors,
|
||||
outputs,
|
||||
)
|
||||
end = time.time()
|
||||
total_tokens += tokens
|
||||
|
||||
if failed:
|
||||
module_failures += 1
|
||||
failure_rate = module_failures / max(module_prompts, 1)
|
||||
failure_rates.append(failure_rate)
|
||||
cost = calculate_cost(tokens)
|
||||
if failed:
|
||||
module_failures += 1
|
||||
failure_rate = module_failures / max(module_prompts, 1)
|
||||
failure_rates.append(failure_rate)
|
||||
cost = calculate_cost(tokens)
|
||||
|
||||
last_output = outputs[-1] if outputs else None
|
||||
if last_output and last_output[1] == prompt:
|
||||
response_text = last_output[2]
|
||||
else:
|
||||
response_text = ""
|
||||
last_output = outputs[-1] if outputs else None
|
||||
if last_output and last_output[1] == prompt:
|
||||
response_text = last_output[2]
|
||||
else:
|
||||
response_text = ""
|
||||
|
||||
yield ScanResult(
|
||||
module=module.dataset_name,
|
||||
tokens=round(tokens / 1000, 1),
|
||||
cost=cost,
|
||||
progress=round(progress, 2),
|
||||
failureRate=round(failure_rate * 100, 2),
|
||||
prompt=prompt[:MAX_PROMPT_LENGTH],
|
||||
latency=end - start,
|
||||
model=response_text,
|
||||
).model_dump_json()
|
||||
yield ScanResult(
|
||||
module=module.dataset_name,
|
||||
tokens=round(tokens / 1000, 1),
|
||||
cost=cost,
|
||||
progress=round(progress, 2),
|
||||
failureRate=round(failure_rate * 100, 2),
|
||||
prompt=prompt[:MAX_PROMPT_LENGTH],
|
||||
latency=end - start,
|
||||
model=response_text,
|
||||
).model_dump_json()
|
||||
|
||||
if optimize and len(failure_rates) >= 5:
|
||||
next_point = optimizer.ask()
|
||||
optimizer.tell(next_point, -failure_rate)
|
||||
best_failure_rate = -optimizer.get_result().fun
|
||||
if best_failure_rate > 0.5:
|
||||
yield ScanResult.status_msg(
|
||||
f"High failure rate detected ({best_failure_rate:.2%}). Stopping this module..."
|
||||
)
|
||||
should_stop = True
|
||||
break
|
||||
if total_tokens > max_budget:
|
||||
logger.info(
|
||||
f"Scan ran out of budget and stopped. {total_tokens=} {max_budget=}"
|
||||
)
|
||||
if optimize and len(failure_rates) >= 5:
|
||||
next_point = optimizer.ask()
|
||||
optimizer.tell(next_point, -failure_rate)
|
||||
best_failure_rate = -optimizer.get_result().fun
|
||||
if best_failure_rate > 0.5:
|
||||
yield ScanResult.status_msg(
|
||||
f"Scan ran out of budget and stopped. {total_tokens=} {max_budget=}"
|
||||
f"High failure rate detected ({best_failure_rate:.2%}). Stopping this module..."
|
||||
)
|
||||
should_stop = True
|
||||
break
|
||||
if total_tokens > max_budget:
|
||||
logger.info(
|
||||
f"Scan ran out of budget and stopped. {total_tokens=} {max_budget=}"
|
||||
)
|
||||
yield ScanResult.status_msg(
|
||||
f"Scan ran out of budget and stopped. {total_tokens=} {max_budget=}"
|
||||
)
|
||||
should_stop = True
|
||||
break
|
||||
|
||||
yield ScanResult.status_msg("Scan completed.")
|
||||
yield ScanResult.status_msg("Scan completed.")
|
||||
|
||||
failure_data = errors + refusals
|
||||
df = pd.DataFrame(
|
||||
failure_data, columns=["module", "prompt", "status_code", "content"]
|
||||
)
|
||||
df.to_csv("failures.csv", index=False)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("Scan failed")
|
||||
yield ScanResult.status_msg(f"Scan failed: {str(e)}")
|
||||
finally:
|
||||
yield ScanResult.status_msg("Scan completed.")
|
||||
failure_data = errors + refusals
|
||||
df = pd.DataFrame(
|
||||
failure_data, columns=["module", "prompt", "status_code", "content"]
|
||||
)
|
||||
df.to_csv("failures.csv", index=False)
|
||||
|
||||
|
||||
async def perform_many_shot_scan(
|
||||
@@ -255,114 +258,107 @@ async def perform_many_shot_scan(
|
||||
) -> AsyncGenerator[str, None]:
|
||||
"""Perform a multi-step security scan with probe injection."""
|
||||
request_factory = multi_modality_spec(request_factory)
|
||||
try:
|
||||
# Load main and probe datasets
|
||||
yield ScanResult.status_msg("Loading datasets...")
|
||||
prompt_modules = prepare_prompts(
|
||||
dataset_names=[m["dataset_name"] for m in datasets if m["selected"]],
|
||||
budget=max_budget,
|
||||
tools_inbox=tools_inbox,
|
||||
)
|
||||
yield ScanResult.status_msg("Loading datasets for MSJ...")
|
||||
msj_modules = msj_data.prepare_prompts(probe_datasets)
|
||||
yield ScanResult.status_msg("Datasets loaded. Starting scan...")
|
||||
# Load main and probe datasets
|
||||
yield ScanResult.status_msg("Loading datasets...")
|
||||
prompt_modules = prepare_prompts(
|
||||
dataset_names=[m["dataset_name"] for m in datasets if m["selected"]],
|
||||
budget=max_budget,
|
||||
tools_inbox=tools_inbox,
|
||||
)
|
||||
yield ScanResult.status_msg("Loading datasets for MSJ...")
|
||||
msj_modules = msj_data.prepare_prompts(probe_datasets)
|
||||
yield ScanResult.status_msg("Datasets loaded. Starting scan...")
|
||||
|
||||
errors = []
|
||||
refusals = []
|
||||
outputs = []
|
||||
total_prompts = sum(len(m.prompts) for m in prompt_modules if not m.lazy)
|
||||
processed_prompts = 0
|
||||
errors = []
|
||||
refusals = []
|
||||
outputs = []
|
||||
total_prompts = sum(len(m.prompts) for m in prompt_modules if not m.lazy)
|
||||
processed_prompts = 0
|
||||
|
||||
optimizer = (
|
||||
Optimizer([Real(0, 1)], base_estimator="GP", n_initial_points=25)
|
||||
if optimize
|
||||
else None
|
||||
)
|
||||
failure_rates = []
|
||||
optimizer = (
|
||||
Optimizer([Real(0, 1)], base_estimator="GP", n_initial_points=25)
|
||||
if optimize
|
||||
else None
|
||||
)
|
||||
failure_rates = []
|
||||
|
||||
for module in prompt_modules:
|
||||
module_failures = 0
|
||||
module_size = 0 if module.lazy else len(module.prompts)
|
||||
logger.info(f"Scanning {module.dataset_name} {module_size}")
|
||||
for module in prompt_modules:
|
||||
module_failures = 0
|
||||
module_size = 0 if module.lazy else len(module.prompts)
|
||||
logger.info(f"Scanning {module.dataset_name} {module_size}")
|
||||
|
||||
async for prompt in generate_prompts(module.prompts):
|
||||
if stop_event and stop_event.is_set():
|
||||
stop_event.clear()
|
||||
logger.info("Scan stopped by user.")
|
||||
yield ScanResult.status_msg("Scan stopped by user.")
|
||||
return
|
||||
tokens = 0
|
||||
processed_prompts += 1
|
||||
progress = (
|
||||
100 * processed_prompts / total_prompts if total_prompts else 0
|
||||
async for prompt in generate_prompts(module.prompts):
|
||||
if stop_event and stop_event.is_set():
|
||||
stop_event.clear()
|
||||
logger.info("Scan stopped by user.")
|
||||
yield ScanResult.status_msg("Scan stopped by user.")
|
||||
return
|
||||
tokens = 0
|
||||
processed_prompts += 1
|
||||
progress = 100 * processed_prompts / total_prompts if total_prompts else 0
|
||||
progress = progress % 100
|
||||
|
||||
full_prompt = ""
|
||||
msj_module = random.choice(msj_modules)
|
||||
|
||||
prompt_tokens = len(full_prompt.split())
|
||||
tokens += prompt_tokens
|
||||
|
||||
injected = False
|
||||
for _ in range(20):
|
||||
if injected:
|
||||
break
|
||||
|
||||
m_prompt = random.choice(msj_module.prompts)
|
||||
full_prompt += "\n" + m_prompt
|
||||
if tokens > max_ctx_length:
|
||||
full_prompt = "\n" + prompt
|
||||
injected = True
|
||||
|
||||
tokens, failed = await process_prompt(
|
||||
request_factory,
|
||||
full_prompt,
|
||||
tokens,
|
||||
module.dataset_name,
|
||||
refusals,
|
||||
errors,
|
||||
outputs,
|
||||
)
|
||||
if failed:
|
||||
module_failures += 1
|
||||
break
|
||||
if injected:
|
||||
break
|
||||
|
||||
full_prompt = ""
|
||||
msj_module = random.choice(msj_modules)
|
||||
failure_rate = module_failures / max(processed_prompts, 1)
|
||||
failure_rates.append(failure_rate)
|
||||
cost = calculate_cost(tokens)
|
||||
|
||||
prompt_tokens = len(full_prompt.split())
|
||||
tokens += prompt_tokens
|
||||
yield ScanResult(
|
||||
module=module.dataset_name,
|
||||
tokens=round(tokens / 1000, 1),
|
||||
cost=cost,
|
||||
progress=round(progress, 2),
|
||||
failureRate=round(failure_rate * 100, 2),
|
||||
prompt=prompt[:MAX_PROMPT_LENGTH],
|
||||
).model_dump_json()
|
||||
|
||||
injected = False
|
||||
for _ in range(20):
|
||||
if injected:
|
||||
break
|
||||
|
||||
m_prompt = random.choice(msj_module.prompts)
|
||||
full_prompt += "\n" + m_prompt
|
||||
if tokens > max_ctx_length:
|
||||
full_prompt = "\n" + prompt
|
||||
injected = True
|
||||
|
||||
tokens, failed = await process_prompt(
|
||||
request_factory,
|
||||
full_prompt,
|
||||
tokens,
|
||||
module.dataset_name,
|
||||
refusals,
|
||||
errors,
|
||||
outputs,
|
||||
if optimize and len(failure_rates) >= 5:
|
||||
next_point = optimizer.ask()
|
||||
optimizer.tell(next_point, -failure_rate)
|
||||
best_failure_rate = -optimizer.get_result().fun
|
||||
if best_failure_rate > 0.5:
|
||||
yield ScanResult.status_msg(
|
||||
f"High failure rate detected ({best_failure_rate:.2%}). Stopping this module..."
|
||||
)
|
||||
if failed:
|
||||
module_failures += 1
|
||||
break
|
||||
if injected:
|
||||
break
|
||||
break
|
||||
|
||||
failure_rate = module_failures / max(processed_prompts, 1)
|
||||
failure_rates.append(failure_rate)
|
||||
cost = calculate_cost(tokens)
|
||||
yield ScanResult.status_msg("Scan completed.")
|
||||
|
||||
yield ScanResult(
|
||||
module=module.dataset_name,
|
||||
tokens=round(tokens / 1000, 1),
|
||||
cost=cost,
|
||||
progress=round(progress, 2),
|
||||
failureRate=round(failure_rate * 100, 2),
|
||||
prompt=prompt[:MAX_PROMPT_LENGTH],
|
||||
).model_dump_json()
|
||||
|
||||
if optimize and len(failure_rates) >= 5:
|
||||
next_point = optimizer.ask()
|
||||
optimizer.tell(next_point, -failure_rate)
|
||||
best_failure_rate = -optimizer.get_result().fun
|
||||
if best_failure_rate > 0.5:
|
||||
yield ScanResult.status_msg(
|
||||
f"High failure rate detected ({best_failure_rate:.2%}). Stopping this module..."
|
||||
)
|
||||
break
|
||||
|
||||
yield ScanResult.status_msg("Scan completed.")
|
||||
|
||||
df = pd.DataFrame(
|
||||
errors + refusals, columns=["module", "prompt", "status_code", "content"]
|
||||
)
|
||||
df.to_csv("failures.csv", index=False)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("Scan failed")
|
||||
yield ScanResult.status_msg(f"Scan failed: {str(e)}")
|
||||
raise e
|
||||
df = pd.DataFrame(
|
||||
errors + refusals, columns=["module", "prompt", "status_code", "content"]
|
||||
)
|
||||
df.to_csv("failures.csv", index=False)
|
||||
|
||||
|
||||
def scan_router(
|
||||
@@ -372,23 +368,27 @@ def scan_router(
|
||||
stop_event: asyncio.Event = None,
|
||||
):
|
||||
if scan_parameters.enableMultiStepAttack:
|
||||
return perform_many_shot_scan(
|
||||
request_factory=request_factory,
|
||||
max_budget=scan_parameters.maxBudget,
|
||||
datasets=scan_parameters.datasets,
|
||||
probe_datasets=scan_parameters.probe_datasets,
|
||||
tools_inbox=tools_inbox,
|
||||
optimize=scan_parameters.optimize,
|
||||
stop_event=stop_event,
|
||||
secrets=scan_parameters.secrets,
|
||||
return with_error_handling(
|
||||
perform_many_shot_scan(
|
||||
request_factory=request_factory,
|
||||
max_budget=scan_parameters.maxBudget,
|
||||
datasets=scan_parameters.datasets,
|
||||
probe_datasets=scan_parameters.probe_datasets,
|
||||
tools_inbox=tools_inbox,
|
||||
optimize=scan_parameters.optimize,
|
||||
stop_event=stop_event,
|
||||
secrets=scan_parameters.secrets,
|
||||
)
|
||||
)
|
||||
else:
|
||||
return perform_single_shot_scan(
|
||||
request_factory=request_factory,
|
||||
max_budget=scan_parameters.maxBudget,
|
||||
datasets=scan_parameters.datasets,
|
||||
tools_inbox=tools_inbox,
|
||||
optimize=scan_parameters.optimize,
|
||||
stop_event=stop_event,
|
||||
secrets=scan_parameters.secrets,
|
||||
return with_error_handling(
|
||||
perform_single_shot_scan(
|
||||
request_factory=request_factory,
|
||||
max_budget=scan_parameters.maxBudget,
|
||||
datasets=scan_parameters.datasets,
|
||||
tools_inbox=tools_inbox,
|
||||
optimize=scan_parameters.optimize,
|
||||
stop_event=stop_event,
|
||||
secrets=scan_parameters.secrets,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from agentic_security.refusal_classifier import RefusalClassifier
|
||||
from agentic_security.refusal_classifier.model import RefusalClassifier
|
||||
|
||||
classifier = RefusalClassifier()
|
||||
classifier.load_model()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from agentic_security.probe_actor.refusal import DefaultRefusalClassifier
|
||||
from agentic_security.probe_actor.refusal import refusal_heuristic, RefusalClassifierManager, RefusalClassifierPlugin
|
||||
|
||||
|
||||
class TestCheckRefusal:
|
||||
@@ -11,3 +12,40 @@ class TestCheckRefusal:
|
||||
def test_empty_response(self):
|
||||
response = ""
|
||||
assert not DefaultRefusalClassifier().is_refusal(response)
|
||||
def test_non_refusal_phrase(self):
|
||||
"""Test that a response without refusal phrases returns False."""
|
||||
response = "Here is some helpful information."
|
||||
assert not DefaultRefusalClassifier().is_refusal(response)
|
||||
|
||||
def test_refusal_heuristic(self):
|
||||
"""Test that the refusal_heuristic function correctly identifies refusal phrases."""
|
||||
response = "Sorry, I cannot assist with your request."
|
||||
# refusal_heuristic will cast the argument to a string and use the plugin manager to check for refusal markers
|
||||
assert refusal_heuristic(response)
|
||||
|
||||
def test_plugin_manager_with_dummy_plugin(self):
|
||||
"""Test that RefusalClassifierManager returns True if any registered plugin returns True."""
|
||||
class AlwaysFalsePlugin(RefusalClassifierPlugin):
|
||||
def is_refusal(self, response: str) -> bool:
|
||||
return False
|
||||
|
||||
class AlwaysTruePlugin(RefusalClassifierPlugin):
|
||||
def is_refusal(self, response: str) -> bool:
|
||||
return True
|
||||
|
||||
manager = RefusalClassifierManager()
|
||||
manager.register_plugin("false", AlwaysFalsePlugin())
|
||||
manager.register_plugin("true", AlwaysTruePlugin())
|
||||
response = "Any response text"
|
||||
assert manager.is_refusal(response)
|
||||
|
||||
def test_default_classifier_with_empty_phrases(self):
|
||||
"""Test that DefaultRefusalClassifier returns False when provided with an empty refusal phrase list."""
|
||||
classifier = DefaultRefusalClassifier(refusal_phrases=[])
|
||||
response = "I do not have any info."
|
||||
assert not classifier.is_refusal(response)
|
||||
|
||||
def test_case_sensitivity(self):
|
||||
"""Test that string matching is case-sensitive."""
|
||||
response = "i'm sorry, but can you help me?" # lower-case "i'm sorry" does not match "I'm sorry" and no extra refusal phrases are present
|
||||
assert not DefaultRefusalClassifier().is_refusal(response)
|
||||
|
||||
@@ -10,6 +10,7 @@ from fastapi import (
|
||||
UploadFile,
|
||||
)
|
||||
from fastapi.responses import StreamingResponse
|
||||
from loguru import logger
|
||||
|
||||
from ..core.app import get_stop_event, get_tools_inbox, set_current_run
|
||||
from ..dependencies import InMemorySecrets, get_in_memory_secrets
|
||||
@@ -25,7 +26,12 @@ async def verify(
|
||||
info: LLMInfo, secrets: InMemorySecrets = Depends(get_in_memory_secrets)
|
||||
):
|
||||
spec = LLMSpec.from_string(info.spec)
|
||||
r = await spec.verify()
|
||||
try:
|
||||
r = await spec.verify()
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
if r.status_code >= 400:
|
||||
raise HTTPException(status_code=r.status_code, detail=r.text)
|
||||
return dict(
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
from:python-pytest-poetry
|
||||
from: python-pytest-poetry
|
||||
# This file was generated automatically by CodeBeaver based on your repository. Learn how to customize it here: https://docs.codebeaver.ai/configuration/
|
||||
+1
-1
@@ -21,4 +21,4 @@ Note: Please be aware that Agentic Security is designed as a safety scanner tool
|
||||
|
||||
## UI 🧙
|
||||
|
||||
<img width="100%" alt="booking-screen" src="https://res.cloudinary.com/dq0w2rtm9/image/upload/v1736433557/z0bsyzhsqlgcr3w4ovwp.gif">
|
||||
<img width="100%" alt="booking-screen" src="https://res.cloudinary.com/dq0w2rtm9/image/upload/v1741192668/final_aa9jhb.gif">
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
:root {
|
||||
--md-primary-fg-color: #e92063;
|
||||
--md-primary-fg-color--light: #e92063;
|
||||
--md-primary-fg-color--dark: #e92063;
|
||||
--md-primary-fg-color: #2E4053;
|
||||
/* Primary color changed to pinkish */
|
||||
--md-primary-fg-color--light: #E0A3B6;
|
||||
--md-primary-fg-color--dark: #1C3F74;
|
||||
/* Dark variant changed to blue */
|
||||
}
|
||||
|
||||
|
||||
/* Revert hue value to that of pre mkdocs-material v9.4.0 */
|
||||
/* Updated slate color scheme with new background */
|
||||
[data-md-color-scheme="slate"] {
|
||||
--md-hue: 230;
|
||||
--md-default-bg-color: hsla(230, 15%, 21%, 1);
|
||||
--md-default-bg-color: #1A1A1A;
|
||||
/* Background changed to dark gray */
|
||||
}
|
||||
|
||||
.hide {
|
||||
@@ -24,12 +26,15 @@ img.index-header {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
/* Updated custom colors */
|
||||
.pydantic-pink {
|
||||
color: #FF007F;
|
||||
color: #E0A3B6;
|
||||
/* Updated to match new theme */
|
||||
}
|
||||
|
||||
.team-blue {
|
||||
color: #0072CE;
|
||||
color: #1C3F74;
|
||||
/* Updated to match new theme */
|
||||
}
|
||||
|
||||
.secure-green {
|
||||
@@ -67,7 +72,6 @@ img.index-header {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
/* Hide the entire footer */
|
||||
.md-footer {
|
||||
display: none;
|
||||
|
||||
+1
-1
@@ -89,7 +89,7 @@ theme:
|
||||
name: Switch to light mode
|
||||
icon:
|
||||
repo: fontawesome/brands/github
|
||||
favicon: "https://res.cloudinary.com/dq0w2rtm9/image/upload/v1737555066/r17hrkre246doczwmvbv.png"
|
||||
favicon: https://res.cloudinary.com/dq0w2rtm9/image/upload/v1741195421/favicon_kuz6xr.png
|
||||
|
||||
extra:
|
||||
generator: false
|
||||
|
||||
Generated
+200
-257
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "aiohappyeyeballs"
|
||||
@@ -229,6 +229,24 @@ files = [
|
||||
[package.extras]
|
||||
dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"]
|
||||
|
||||
[[package]]
|
||||
name = "backrefs"
|
||||
version = "5.8"
|
||||
description = "A wrapper around re and regex that adds additional back references."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "backrefs-5.8-py310-none-any.whl", hash = "sha256:c67f6638a34a5b8730812f5101376f9d41dc38c43f1fdc35cb54700f6ed4465d"},
|
||||
{file = "backrefs-5.8-py311-none-any.whl", hash = "sha256:2e1c15e4af0e12e45c8701bd5da0902d326b2e200cafcd25e49d9f06d44bb61b"},
|
||||
{file = "backrefs-5.8-py312-none-any.whl", hash = "sha256:bbef7169a33811080d67cdf1538c8289f76f0942ff971222a16034da88a73486"},
|
||||
{file = "backrefs-5.8-py313-none-any.whl", hash = "sha256:e3a63b073867dbefd0536425f43db618578528e3896fb77be7141328642a1585"},
|
||||
{file = "backrefs-5.8-py39-none-any.whl", hash = "sha256:a66851e4533fb5b371aa0628e1fee1af05135616b86140c9d787a2ffdf4b8fdc"},
|
||||
{file = "backrefs-5.8.tar.gz", hash = "sha256:2cab642a205ce966af3dd4b38ee36009b31fa9502a35fd61d59ccc116e40a6bd"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
extras = ["regex"]
|
||||
|
||||
[[package]]
|
||||
name = "beautifulsoup4"
|
||||
version = "4.12.3"
|
||||
@@ -768,6 +786,20 @@ files = [
|
||||
{file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "execnet"
|
||||
version = "2.1.1"
|
||||
description = "execnet: rapid multi-Python deployment"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc"},
|
||||
{file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
testing = ["hatch", "pre-commit", "pytest", "tox"]
|
||||
|
||||
[[package]]
|
||||
name = "executing"
|
||||
version = "2.2.0"
|
||||
@@ -1311,13 +1343,13 @@ testing = ["Django", "attrs", "colorama", "docopt", "pytest (<9.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.5"
|
||||
version = "3.1.6"
|
||||
description = "A very fast and expressive template engine."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"},
|
||||
{file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"},
|
||||
{file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"},
|
||||
{file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1695,45 +1727,45 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "matplotlib"
|
||||
version = "3.10.0"
|
||||
version = "3.10.1"
|
||||
description = "Python plotting package"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
files = [
|
||||
{file = "matplotlib-3.10.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2c5829a5a1dd5a71f0e31e6e8bb449bc0ee9dbfb05ad28fc0c6b55101b3a4be6"},
|
||||
{file = "matplotlib-3.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2a43cbefe22d653ab34bb55d42384ed30f611bcbdea1f8d7f431011a2e1c62e"},
|
||||
{file = "matplotlib-3.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:607b16c8a73943df110f99ee2e940b8a1cbf9714b65307c040d422558397dac5"},
|
||||
{file = "matplotlib-3.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01d2b19f13aeec2e759414d3bfe19ddfb16b13a1250add08d46d5ff6f9be83c6"},
|
||||
{file = "matplotlib-3.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e6c6461e1fc63df30bf6f80f0b93f5b6784299f721bc28530477acd51bfc3d1"},
|
||||
{file = "matplotlib-3.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:994c07b9d9fe8d25951e3202a68c17900679274dadfc1248738dcfa1bd40d7f3"},
|
||||
{file = "matplotlib-3.10.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:fd44fc75522f58612ec4a33958a7e5552562b7705b42ef1b4f8c0818e304a363"},
|
||||
{file = "matplotlib-3.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c58a9622d5dbeb668f407f35f4e6bfac34bb9ecdcc81680c04d0258169747997"},
|
||||
{file = "matplotlib-3.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:845d96568ec873be63f25fa80e9e7fae4be854a66a7e2f0c8ccc99e94a8bd4ef"},
|
||||
{file = "matplotlib-3.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5439f4c5a3e2e8eab18e2f8c3ef929772fd5641876db71f08127eed95ab64683"},
|
||||
{file = "matplotlib-3.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4673ff67a36152c48ddeaf1135e74ce0d4bce1bbf836ae40ed39c29edf7e2765"},
|
||||
{file = "matplotlib-3.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:7e8632baebb058555ac0cde75db885c61f1212e47723d63921879806b40bec6a"},
|
||||
{file = "matplotlib-3.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4659665bc7c9b58f8c00317c3c2a299f7f258eeae5a5d56b4c64226fca2f7c59"},
|
||||
{file = "matplotlib-3.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d44cb942af1693cced2604c33a9abcef6205601c445f6d0dc531d813af8a2f5a"},
|
||||
{file = "matplotlib-3.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a994f29e968ca002b50982b27168addfd65f0105610b6be7fa515ca4b5307c95"},
|
||||
{file = "matplotlib-3.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b0558bae37f154fffda54d779a592bc97ca8b4701f1c710055b609a3bac44c8"},
|
||||
{file = "matplotlib-3.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:503feb23bd8c8acc75541548a1d709c059b7184cde26314896e10a9f14df5f12"},
|
||||
{file = "matplotlib-3.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:c40ba2eb08b3f5de88152c2333c58cee7edcead0a2a0d60fcafa116b17117adc"},
|
||||
{file = "matplotlib-3.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96f2886f5c1e466f21cc41b70c5a0cd47bfa0015eb2d5793c88ebce658600e25"},
|
||||
{file = "matplotlib-3.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:12eaf48463b472c3c0f8dbacdbf906e573013df81a0ab82f0616ea4b11281908"},
|
||||
{file = "matplotlib-3.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fbbabc82fde51391c4da5006f965e36d86d95f6ee83fb594b279564a4c5d0d2"},
|
||||
{file = "matplotlib-3.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad2e15300530c1a94c63cfa546e3b7864bd18ea2901317bae8bbf06a5ade6dcf"},
|
||||
{file = "matplotlib-3.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3547d153d70233a8496859097ef0312212e2689cdf8d7ed764441c77604095ae"},
|
||||
{file = "matplotlib-3.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:c55b20591ced744aa04e8c3e4b7543ea4d650b6c3c4b208c08a05b4010e8b442"},
|
||||
{file = "matplotlib-3.10.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9ade1003376731a971e398cc4ef38bb83ee8caf0aee46ac6daa4b0506db1fd06"},
|
||||
{file = "matplotlib-3.10.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95b710fea129c76d30be72c3b38f330269363fbc6e570a5dd43580487380b5ff"},
|
||||
{file = "matplotlib-3.10.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cdbaf909887373c3e094b0318d7ff230b2ad9dcb64da7ade654182872ab2593"},
|
||||
{file = "matplotlib-3.10.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d907fddb39f923d011875452ff1eca29a9e7f21722b873e90db32e5d8ddff12e"},
|
||||
{file = "matplotlib-3.10.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3b427392354d10975c1d0f4ee18aa5844640b512d5311ef32efd4dd7db106ede"},
|
||||
{file = "matplotlib-3.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5fd41b0ec7ee45cd960a8e71aea7c946a28a0b8a4dcee47d2856b2af051f334c"},
|
||||
{file = "matplotlib-3.10.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:81713dd0d103b379de4516b861d964b1d789a144103277769238c732229d7f03"},
|
||||
{file = "matplotlib-3.10.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:359f87baedb1f836ce307f0e850d12bb5f1936f70d035561f90d41d305fdacea"},
|
||||
{file = "matplotlib-3.10.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae80dc3a4add4665cf2faa90138384a7ffe2a4e37c58d83e115b54287c4f06ef"},
|
||||
{file = "matplotlib-3.10.0.tar.gz", hash = "sha256:b886d02a581b96704c9d1ffe55709e49b4d2d52709ccebc4be42db856e511278"},
|
||||
{file = "matplotlib-3.10.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ff2ae14910be903f4a24afdbb6d7d3a6c44da210fc7d42790b87aeac92238a16"},
|
||||
{file = "matplotlib-3.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0721a3fd3d5756ed593220a8b86808a36c5031fce489adb5b31ee6dbb47dd5b2"},
|
||||
{file = "matplotlib-3.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0673b4b8f131890eb3a1ad058d6e065fb3c6e71f160089b65f8515373394698"},
|
||||
{file = "matplotlib-3.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e875b95ac59a7908978fe307ecdbdd9a26af7fa0f33f474a27fcf8c99f64a19"},
|
||||
{file = "matplotlib-3.10.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2589659ea30726284c6c91037216f64a506a9822f8e50592d48ac16a2f29e044"},
|
||||
{file = "matplotlib-3.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:a97ff127f295817bc34517255c9db6e71de8eddaab7f837b7d341dee9f2f587f"},
|
||||
{file = "matplotlib-3.10.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:057206ff2d6ab82ff3e94ebd94463d084760ca682ed5f150817b859372ec4401"},
|
||||
{file = "matplotlib-3.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a144867dd6bf8ba8cb5fc81a158b645037e11b3e5cf8a50bd5f9917cb863adfe"},
|
||||
{file = "matplotlib-3.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56c5d9fcd9879aa8040f196a235e2dcbdf7dd03ab5b07c0696f80bc6cf04bedd"},
|
||||
{file = "matplotlib-3.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f69dc9713e4ad2fb21a1c30e37bd445d496524257dfda40ff4a8efb3604ab5c"},
|
||||
{file = "matplotlib-3.10.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4c59af3e8aca75d7744b68e8e78a669e91ccbcf1ac35d0102a7b1b46883f1dd7"},
|
||||
{file = "matplotlib-3.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:11b65088c6f3dae784bc72e8d039a2580186285f87448babb9ddb2ad0082993a"},
|
||||
{file = "matplotlib-3.10.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:66e907a06e68cb6cfd652c193311d61a12b54f56809cafbed9736ce5ad92f107"},
|
||||
{file = "matplotlib-3.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e9b4bb156abb8fa5e5b2b460196f7db7264fc6d62678c03457979e7d5254b7be"},
|
||||
{file = "matplotlib-3.10.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1985ad3d97f51307a2cbfc801a930f120def19ba22864182dacef55277102ba6"},
|
||||
{file = "matplotlib-3.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c96f2c2f825d1257e437a1482c5a2cf4fee15db4261bd6fc0750f81ba2b4ba3d"},
|
||||
{file = "matplotlib-3.10.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35e87384ee9e488d8dd5a2dd7baf471178d38b90618d8ea147aced4ab59c9bea"},
|
||||
{file = "matplotlib-3.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:cfd414bce89cc78a7e1d25202e979b3f1af799e416010a20ab2b5ebb3a02425c"},
|
||||
{file = "matplotlib-3.10.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c42eee41e1b60fd83ee3292ed83a97a5f2a8239b10c26715d8a6172226988d7b"},
|
||||
{file = "matplotlib-3.10.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4f0647b17b667ae745c13721602b540f7aadb2a32c5b96e924cd4fea5dcb90f1"},
|
||||
{file = "matplotlib-3.10.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa3854b5f9473564ef40a41bc922be978fab217776e9ae1545c9b3a5cf2092a3"},
|
||||
{file = "matplotlib-3.10.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e496c01441be4c7d5f96d4e40f7fca06e20dcb40e44c8daa2e740e1757ad9e6"},
|
||||
{file = "matplotlib-3.10.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5d45d3f5245be5b469843450617dcad9af75ca50568acf59997bed9311131a0b"},
|
||||
{file = "matplotlib-3.10.1-cp313-cp313-win_amd64.whl", hash = "sha256:8e8e25b1209161d20dfe93037c8a7f7ca796ec9aa326e6e4588d8c4a5dd1e473"},
|
||||
{file = "matplotlib-3.10.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:19b06241ad89c3ae9469e07d77efa87041eac65d78df4fcf9cac318028009b01"},
|
||||
{file = "matplotlib-3.10.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01e63101ebb3014e6e9f80d9cf9ee361a8599ddca2c3e166c563628b39305dbb"},
|
||||
{file = "matplotlib-3.10.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f06bad951eea6422ac4e8bdebcf3a70c59ea0a03338c5d2b109f57b64eb3972"},
|
||||
{file = "matplotlib-3.10.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3dfb036f34873b46978f55e240cff7a239f6c4409eac62d8145bad3fc6ba5a3"},
|
||||
{file = "matplotlib-3.10.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dc6ab14a7ab3b4d813b88ba957fc05c79493a037f54e246162033591e770de6f"},
|
||||
{file = "matplotlib-3.10.1-cp313-cp313t-win_amd64.whl", hash = "sha256:bc411ebd5889a78dabbc457b3fa153203e22248bfa6eedc6797be5df0164dbf9"},
|
||||
{file = "matplotlib-3.10.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:648406f1899f9a818cef8c0231b44dcfc4ff36f167101c3fd1c9151f24220fdc"},
|
||||
{file = "matplotlib-3.10.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:02582304e352f40520727984a5a18f37e8187861f954fea9be7ef06569cf85b4"},
|
||||
{file = "matplotlib-3.10.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3809916157ba871bcdd33d3493acd7fe3037db5daa917ca6e77975a94cef779"},
|
||||
{file = "matplotlib-3.10.1.tar.gz", hash = "sha256:e8d2d0e3881b129268585bf4765ad3ee73a4591d77b9a18c214ac7e3a79fb2ba"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1848,13 +1880,13 @@ min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-imp
|
||||
|
||||
[[package]]
|
||||
name = "mkdocs-autorefs"
|
||||
version = "1.3.0"
|
||||
version = "1.4.0"
|
||||
description = "Automatically link across pages in MkDocs."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "mkdocs_autorefs-1.3.0-py3-none-any.whl", hash = "sha256:d180f9778a04e78b7134e31418f238bba56f56d6a8af97873946ff661befffb3"},
|
||||
{file = "mkdocs_autorefs-1.3.0.tar.gz", hash = "sha256:6867764c099ace9025d6ac24fd07b85a98335fbd30107ef01053697c8f46db61"},
|
||||
{file = "mkdocs_autorefs-1.4.0-py3-none-any.whl", hash = "sha256:bad19f69655878d20194acd0162e29a89c3f7e6365ffe54e72aa3fd1072f240d"},
|
||||
{file = "mkdocs_autorefs-1.4.0.tar.gz", hash = "sha256:a9c0aa9c90edbce302c09d050a3c4cb7c76f8b7b2c98f84a7a05f53d00392156"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1899,17 +1931,18 @@ pygments = ">2.12.0"
|
||||
|
||||
[[package]]
|
||||
name = "mkdocs-material"
|
||||
version = "9.6.4"
|
||||
version = "9.6.7"
|
||||
description = "Documentation that simply works"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "mkdocs_material-9.6.4-py3-none-any.whl", hash = "sha256:414e8376551def6d644b8e6f77226022868532a792eb2c9accf52199009f568f"},
|
||||
{file = "mkdocs_material-9.6.4.tar.gz", hash = "sha256:4d1d35e1c1d3e15294cb7fa5d02e0abaee70d408f75027dc7be6e30fb32e6867"},
|
||||
{file = "mkdocs_material-9.6.7-py3-none-any.whl", hash = "sha256:8a159e45e80fcaadd9fbeef62cbf928569b93df954d4dc5ba76d46820caf7b47"},
|
||||
{file = "mkdocs_material-9.6.7.tar.gz", hash = "sha256:3e2c1fceb9410056c2d91f334a00cdea3215c28750e00c691c1e46b2a33309b4"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
babel = ">=2.10,<3.0"
|
||||
backrefs = ">=5.7.post1,<6.0"
|
||||
colorama = ">=0.4,<1.0"
|
||||
jinja2 = ">=3.0,<4.0"
|
||||
markdown = ">=3.2,<4.0"
|
||||
@@ -1918,7 +1951,6 @@ mkdocs-material-extensions = ">=1.3,<2.0"
|
||||
paginate = ">=0.5,<1.0"
|
||||
pygments = ">=2.16,<3.0"
|
||||
pymdown-extensions = ">=10.2,<11.0"
|
||||
regex = ">=2022.4"
|
||||
requests = ">=2.26,<3.0"
|
||||
|
||||
[package.extras]
|
||||
@@ -1939,13 +1971,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "mkdocstrings"
|
||||
version = "0.28.1"
|
||||
version = "0.28.2"
|
||||
description = "Automatic documentation from sources, for MkDocs."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "mkdocstrings-0.28.1-py3-none-any.whl", hash = "sha256:a5878ae5cd1e26f491ff084c1f9ab995687d52d39a5c558e9b7023d0e4e0b740"},
|
||||
{file = "mkdocstrings-0.28.1.tar.gz", hash = "sha256:fb64576906771b7701e8e962fd90073650ff689e95eb86e86751a66d65ab4489"},
|
||||
{file = "mkdocstrings-0.28.2-py3-none-any.whl", hash = "sha256:57f79c557e2718d217d6f6a81bf75a0de097f10e922e7e5e00f085c3f0ff6895"},
|
||||
{file = "mkdocstrings-0.28.2.tar.gz", hash = "sha256:9b847266d7a588ea76a8385eaebe1538278b4361c0d1ce48ed005be59f053569"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1953,7 +1985,7 @@ Jinja2 = ">=2.11.1"
|
||||
Markdown = ">=3.6"
|
||||
MarkupSafe = ">=1.1"
|
||||
mkdocs = ">=1.4"
|
||||
mkdocs-autorefs = ">=1.3"
|
||||
mkdocs-autorefs = ">=1.4"
|
||||
mkdocs-get-deps = ">=0.2"
|
||||
pymdown-extensions = ">=6.3"
|
||||
|
||||
@@ -2087,49 +2119,43 @@ dill = ">=0.3.8"
|
||||
|
||||
[[package]]
|
||||
name = "mypy"
|
||||
version = "1.14.1"
|
||||
version = "1.15.0"
|
||||
description = "Optional static typing for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb"},
|
||||
{file = "mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0"},
|
||||
{file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d"},
|
||||
{file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b"},
|
||||
{file = "mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427"},
|
||||
{file = "mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f"},
|
||||
{file = "mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c"},
|
||||
{file = "mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1"},
|
||||
{file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8"},
|
||||
{file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f"},
|
||||
{file = "mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1"},
|
||||
{file = "mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae"},
|
||||
{file = "mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14"},
|
||||
{file = "mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9"},
|
||||
{file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11"},
|
||||
{file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e"},
|
||||
{file = "mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89"},
|
||||
{file = "mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b"},
|
||||
{file = "mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255"},
|
||||
{file = "mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34"},
|
||||
{file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a"},
|
||||
{file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9"},
|
||||
{file = "mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd"},
|
||||
{file = "mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107"},
|
||||
{file = "mypy-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7084fb8f1128c76cd9cf68fe5971b37072598e7c31b2f9f95586b65c741a9d31"},
|
||||
{file = "mypy-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f845a00b4f420f693f870eaee5f3e2692fa84cc8514496114649cfa8fd5e2c6"},
|
||||
{file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44bf464499f0e3a2d14d58b54674dee25c031703b2ffc35064bd0df2e0fac319"},
|
||||
{file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c99f27732c0b7dc847adb21c9d47ce57eb48fa33a17bc6d7d5c5e9f9e7ae5bac"},
|
||||
{file = "mypy-1.14.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:bce23c7377b43602baa0bd22ea3265c49b9ff0b76eb315d6c34721af4cdf1d9b"},
|
||||
{file = "mypy-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:8edc07eeade7ebc771ff9cf6b211b9a7d93687ff892150cb5692e4f4272b0837"},
|
||||
{file = "mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35"},
|
||||
{file = "mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc"},
|
||||
{file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9"},
|
||||
{file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb"},
|
||||
{file = "mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60"},
|
||||
{file = "mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c"},
|
||||
{file = "mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1"},
|
||||
{file = "mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6"},
|
||||
{file = "mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13"},
|
||||
{file = "mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559"},
|
||||
{file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b"},
|
||||
{file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3"},
|
||||
{file = "mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b"},
|
||||
{file = "mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828"},
|
||||
{file = "mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f"},
|
||||
{file = "mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5"},
|
||||
{file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e"},
|
||||
{file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c"},
|
||||
{file = "mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f"},
|
||||
{file = "mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f"},
|
||||
{file = "mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd"},
|
||||
{file = "mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f"},
|
||||
{file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464"},
|
||||
{file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee"},
|
||||
{file = "mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e"},
|
||||
{file = "mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22"},
|
||||
{file = "mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445"},
|
||||
{file = "mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d"},
|
||||
{file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5"},
|
||||
{file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036"},
|
||||
{file = "mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357"},
|
||||
{file = "mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf"},
|
||||
{file = "mypy-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e601a7fa172c2131bff456bb3ee08a88360760d0d2f8cbd7a75a65497e2df078"},
|
||||
{file = "mypy-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:712e962a6357634fef20412699a3655c610110e01cdaa6180acec7fc9f8513ba"},
|
||||
{file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95579473af29ab73a10bada2f9722856792a36ec5af5399b653aa28360290a5"},
|
||||
{file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f8722560a14cde92fdb1e31597760dc35f9f5524cce17836c0d22841830fd5b"},
|
||||
{file = "mypy-1.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fbb8da62dc352133d7d7ca90ed2fb0e9d42bb1a32724c287d3c76c58cbaa9c2"},
|
||||
{file = "mypy-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:d10d994b41fb3497719bbf866f227b3489048ea4bbbb5015357db306249f7980"},
|
||||
{file = "mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e"},
|
||||
{file = "mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2257,66 +2283,66 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "2.2.2"
|
||||
version = "2.2.3"
|
||||
description = "Fundamental package for array computing in Python"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
files = [
|
||||
{file = "numpy-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7079129b64cb78bdc8d611d1fd7e8002c0a2565da6a47c4df8062349fee90e3e"},
|
||||
{file = "numpy-2.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ec6c689c61df613b783aeb21f945c4cbe6c51c28cb70aae8430577ab39f163e"},
|
||||
{file = "numpy-2.2.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:40c7ff5da22cd391944a28c6a9c638a5eef77fcf71d6e3a79e1d9d9e82752715"},
|
||||
{file = "numpy-2.2.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:995f9e8181723852ca458e22de5d9b7d3ba4da3f11cc1cb113f093b271d7965a"},
|
||||
{file = "numpy-2.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b78ea78450fd96a498f50ee096f69c75379af5138f7881a51355ab0e11286c97"},
|
||||
{file = "numpy-2.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3fbe72d347fbc59f94124125e73fc4976a06927ebc503ec5afbfb35f193cd957"},
|
||||
{file = "numpy-2.2.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8e6da5cffbbe571f93588f562ed130ea63ee206d12851b60819512dd3e1ba50d"},
|
||||
{file = "numpy-2.2.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:09d6a2032faf25e8d0cadde7fd6145118ac55d2740132c1d845f98721b5ebcfd"},
|
||||
{file = "numpy-2.2.2-cp310-cp310-win32.whl", hash = "sha256:159ff6ee4c4a36a23fe01b7c3d07bd8c14cc433d9720f977fcd52c13c0098160"},
|
||||
{file = "numpy-2.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:64bd6e1762cd7f0986a740fee4dff927b9ec2c5e4d9a28d056eb17d332158014"},
|
||||
{file = "numpy-2.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:642199e98af1bd2b6aeb8ecf726972d238c9877b0f6e8221ee5ab945ec8a2189"},
|
||||
{file = "numpy-2.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6d9fc9d812c81e6168b6d405bf00b8d6739a7f72ef22a9214c4241e0dc70b323"},
|
||||
{file = "numpy-2.2.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:c7d1fd447e33ee20c1f33f2c8e6634211124a9aabde3c617687d8b739aa69eac"},
|
||||
{file = "numpy-2.2.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:451e854cfae0febe723077bd0cf0a4302a5d84ff25f0bfece8f29206c7bed02e"},
|
||||
{file = "numpy-2.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd249bc894af67cbd8bad2c22e7cbcd46cf87ddfca1f1289d1e7e54868cc785c"},
|
||||
{file = "numpy-2.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02935e2c3c0c6cbe9c7955a8efa8908dd4221d7755644c59d1bba28b94fd334f"},
|
||||
{file = "numpy-2.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a972cec723e0563aa0823ee2ab1df0cb196ed0778f173b381c871a03719d4826"},
|
||||
{file = "numpy-2.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d6d6a0910c3b4368d89dde073e630882cdb266755565155bc33520283b2d9df8"},
|
||||
{file = "numpy-2.2.2-cp311-cp311-win32.whl", hash = "sha256:860fd59990c37c3ef913c3ae390b3929d005243acca1a86facb0773e2d8d9e50"},
|
||||
{file = "numpy-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:da1eeb460ecce8d5b8608826595c777728cdf28ce7b5a5a8c8ac8d949beadcf2"},
|
||||
{file = "numpy-2.2.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ac9bea18d6d58a995fac1b2cb4488e17eceeac413af014b1dd26170b766d8467"},
|
||||
{file = "numpy-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23ae9f0c2d889b7b2d88a3791f6c09e2ef827c2446f1c4a3e3e76328ee4afd9a"},
|
||||
{file = "numpy-2.2.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3074634ea4d6df66be04f6728ee1d173cfded75d002c75fac79503a880bf3825"},
|
||||
{file = "numpy-2.2.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:8ec0636d3f7d68520afc6ac2dc4b8341ddb725039de042faf0e311599f54eb37"},
|
||||
{file = "numpy-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ffbb1acd69fdf8e89dd60ef6182ca90a743620957afb7066385a7bbe88dc748"},
|
||||
{file = "numpy-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0349b025e15ea9d05c3d63f9657707a4e1d471128a3b1d876c095f328f8ff7f0"},
|
||||
{file = "numpy-2.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:463247edcee4a5537841d5350bc87fe8e92d7dd0e8c71c995d2c6eecb8208278"},
|
||||
{file = "numpy-2.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9dd47ff0cb2a656ad69c38da850df3454da88ee9a6fde0ba79acceee0e79daba"},
|
||||
{file = "numpy-2.2.2-cp312-cp312-win32.whl", hash = "sha256:4525b88c11906d5ab1b0ec1f290996c0020dd318af8b49acaa46f198b1ffc283"},
|
||||
{file = "numpy-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:5acea83b801e98541619af398cc0109ff48016955cc0818f478ee9ef1c5c3dcb"},
|
||||
{file = "numpy-2.2.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b208cfd4f5fe34e1535c08983a1a6803fdbc7a1e86cf13dd0c61de0b51a0aadc"},
|
||||
{file = "numpy-2.2.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d0bbe7dd86dca64854f4b6ce2ea5c60b51e36dfd597300057cf473d3615f2369"},
|
||||
{file = "numpy-2.2.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:22ea3bb552ade325530e72a0c557cdf2dea8914d3a5e1fecf58fa5dbcc6f43cd"},
|
||||
{file = "numpy-2.2.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:128c41c085cab8a85dc29e66ed88c05613dccf6bc28b3866cd16050a2f5448be"},
|
||||
{file = "numpy-2.2.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:250c16b277e3b809ac20d1f590716597481061b514223c7badb7a0f9993c7f84"},
|
||||
{file = "numpy-2.2.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0c8854b09bc4de7b041148d8550d3bd712b5c21ff6a8ed308085f190235d7ff"},
|
||||
{file = "numpy-2.2.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b6fb9c32a91ec32a689ec6410def76443e3c750e7cfc3fb2206b985ffb2b85f0"},
|
||||
{file = "numpy-2.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:57b4012e04cc12b78590a334907e01b3a85efb2107df2b8733ff1ed05fce71de"},
|
||||
{file = "numpy-2.2.2-cp313-cp313-win32.whl", hash = "sha256:4dbd80e453bd34bd003b16bd802fac70ad76bd463f81f0c518d1245b1c55e3d9"},
|
||||
{file = "numpy-2.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:5a8c863ceacae696aff37d1fd636121f1a512117652e5dfb86031c8d84836369"},
|
||||
{file = "numpy-2.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:b3482cb7b3325faa5f6bc179649406058253d91ceda359c104dac0ad320e1391"},
|
||||
{file = "numpy-2.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9491100aba630910489c1d0158034e1c9a6546f0b1340f716d522dc103788e39"},
|
||||
{file = "numpy-2.2.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:41184c416143defa34cc8eb9d070b0a5ba4f13a0fa96a709e20584638254b317"},
|
||||
{file = "numpy-2.2.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7dca87ca328f5ea7dafc907c5ec100d187911f94825f8700caac0b3f4c384b49"},
|
||||
{file = "numpy-2.2.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bc61b307655d1a7f9f4b043628b9f2b721e80839914ede634e3d485913e1fb2"},
|
||||
{file = "numpy-2.2.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fad446ad0bc886855ddf5909cbf8cb5d0faa637aaa6277fb4b19ade134ab3c7"},
|
||||
{file = "numpy-2.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:149d1113ac15005652e8d0d3f6fd599360e1a708a4f98e43c9c77834a28238cb"},
|
||||
{file = "numpy-2.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:106397dbbb1896f99e044efc90360d098b3335060375c26aa89c0d8a97c5f648"},
|
||||
{file = "numpy-2.2.2-cp313-cp313t-win32.whl", hash = "sha256:0eec19f8af947a61e968d5429f0bd92fec46d92b0008d0a6685b40d6adf8a4f4"},
|
||||
{file = "numpy-2.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:97b974d3ba0fb4612b77ed35d7627490e8e3dff56ab41454d9e8b23448940576"},
|
||||
{file = "numpy-2.2.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b0531f0b0e07643eb089df4c509d30d72c9ef40defa53e41363eca8a8cc61495"},
|
||||
{file = "numpy-2.2.2-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:e9e82dcb3f2ebbc8cb5ce1102d5f1c5ed236bf8a11730fb45ba82e2841ec21df"},
|
||||
{file = "numpy-2.2.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0d4142eb40ca6f94539e4db929410f2a46052a0fe7a2c1c59f6179c39938d2a"},
|
||||
{file = "numpy-2.2.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:356ca982c188acbfa6af0d694284d8cf20e95b1c3d0aefa8929376fea9146f60"},
|
||||
{file = "numpy-2.2.2.tar.gz", hash = "sha256:ed6906f61834d687738d25988ae117683705636936cc605be0bb208b23df4d8f"},
|
||||
{file = "numpy-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cbc6472e01952d3d1b2772b720428f8b90e2deea8344e854df22b0618e9cce71"},
|
||||
{file = "numpy-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdfe0c22692a30cd830c0755746473ae66c4a8f2e7bd508b35fb3b6a0813d787"},
|
||||
{file = "numpy-2.2.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:e37242f5324ffd9f7ba5acf96d774f9276aa62a966c0bad8dae692deebec7716"},
|
||||
{file = "numpy-2.2.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:95172a21038c9b423e68be78fd0be6e1b97674cde269b76fe269a5dfa6fadf0b"},
|
||||
{file = "numpy-2.2.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5b47c440210c5d1d67e1cf434124e0b5c395eee1f5806fdd89b553ed1acd0a3"},
|
||||
{file = "numpy-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0391ea3622f5c51a2e29708877d56e3d276827ac5447d7f45e9bc4ade8923c52"},
|
||||
{file = "numpy-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f6b3dfc7661f8842babd8ea07e9897fe3d9b69a1d7e5fbb743e4160f9387833b"},
|
||||
{file = "numpy-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1ad78ce7f18ce4e7df1b2ea4019b5817a2f6a8a16e34ff2775f646adce0a5027"},
|
||||
{file = "numpy-2.2.3-cp310-cp310-win32.whl", hash = "sha256:5ebeb7ef54a7be11044c33a17b2624abe4307a75893c001a4800857956b41094"},
|
||||
{file = "numpy-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:596140185c7fa113563c67c2e894eabe0daea18cf8e33851738c19f70ce86aeb"},
|
||||
{file = "numpy-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:16372619ee728ed67a2a606a614f56d3eabc5b86f8b615c79d01957062826ca8"},
|
||||
{file = "numpy-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5521a06a3148686d9269c53b09f7d399a5725c47bbb5b35747e1cb76326b714b"},
|
||||
{file = "numpy-2.2.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:7c8dde0ca2f77828815fd1aedfdf52e59071a5bae30dac3b4da2a335c672149a"},
|
||||
{file = "numpy-2.2.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:77974aba6c1bc26e3c205c2214f0d5b4305bdc719268b93e768ddb17e3fdd636"},
|
||||
{file = "numpy-2.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d42f9c36d06440e34226e8bd65ff065ca0963aeecada587b937011efa02cdc9d"},
|
||||
{file = "numpy-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2712c5179f40af9ddc8f6727f2bd910ea0eb50206daea75f58ddd9fa3f715bb"},
|
||||
{file = "numpy-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c8b0451d2ec95010d1db8ca733afc41f659f425b7f608af569711097fd6014e2"},
|
||||
{file = "numpy-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9b4a8148c57ecac25a16b0e11798cbe88edf5237b0df99973687dd866f05e1b"},
|
||||
{file = "numpy-2.2.3-cp311-cp311-win32.whl", hash = "sha256:1f45315b2dc58d8a3e7754fe4e38b6fce132dab284a92851e41b2b344f6441c5"},
|
||||
{file = "numpy-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f48ba6f6c13e5e49f3d3efb1b51c8193215c42ac82610a04624906a9270be6f"},
|
||||
{file = "numpy-2.2.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12c045f43b1d2915eca6b880a7f4a256f59d62df4f044788c8ba67709412128d"},
|
||||
{file = "numpy-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:87eed225fd415bbae787f93a457af7f5990b92a334e346f72070bf569b9c9c95"},
|
||||
{file = "numpy-2.2.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:712a64103d97c404e87d4d7c47fb0c7ff9acccc625ca2002848e0d53288b90ea"},
|
||||
{file = "numpy-2.2.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a5ae282abe60a2db0fd407072aff4599c279bcd6e9a2475500fc35b00a57c532"},
|
||||
{file = "numpy-2.2.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5266de33d4c3420973cf9ae3b98b54a2a6d53a559310e3236c4b2b06b9c07d4e"},
|
||||
{file = "numpy-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b787adbf04b0db1967798dba8da1af07e387908ed1553a0d6e74c084d1ceafe"},
|
||||
{file = "numpy-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:34c1b7e83f94f3b564b35f480f5652a47007dd91f7c839f404d03279cc8dd021"},
|
||||
{file = "numpy-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4d8335b5f1b6e2bce120d55fb17064b0262ff29b459e8493d1785c18ae2553b8"},
|
||||
{file = "numpy-2.2.3-cp312-cp312-win32.whl", hash = "sha256:4d9828d25fb246bedd31e04c9e75714a4087211ac348cb39c8c5f99dbb6683fe"},
|
||||
{file = "numpy-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:83807d445817326b4bcdaaaf8e8e9f1753da04341eceec705c001ff342002e5d"},
|
||||
{file = "numpy-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bfdb06b395385ea9b91bf55c1adf1b297c9fdb531552845ff1d3ea6e40d5aba"},
|
||||
{file = "numpy-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:23c9f4edbf4c065fddb10a4f6e8b6a244342d95966a48820c614891e5059bb50"},
|
||||
{file = "numpy-2.2.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:a0c03b6be48aaf92525cccf393265e02773be8fd9551a2f9adbe7db1fa2b60f1"},
|
||||
{file = "numpy-2.2.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:2376e317111daa0a6739e50f7ee2a6353f768489102308b0d98fcf4a04f7f3b5"},
|
||||
{file = "numpy-2.2.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8fb62fe3d206d72fe1cfe31c4a1106ad2b136fcc1606093aeab314f02930fdf2"},
|
||||
{file = "numpy-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52659ad2534427dffcc36aac76bebdd02b67e3b7a619ac67543bc9bfe6b7cdb1"},
|
||||
{file = "numpy-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b416af7d0ed3271cad0f0a0d0bee0911ed7eba23e66f8424d9f3dfcdcae1304"},
|
||||
{file = "numpy-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1402da8e0f435991983d0a9708b779f95a8c98c6b18a171b9f1be09005e64d9d"},
|
||||
{file = "numpy-2.2.3-cp313-cp313-win32.whl", hash = "sha256:136553f123ee2951bfcfbc264acd34a2fc2f29d7cdf610ce7daf672b6fbaa693"},
|
||||
{file = "numpy-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:5b732c8beef1d7bc2d9e476dbba20aaff6167bf205ad9aa8d30913859e82884b"},
|
||||
{file = "numpy-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:435e7a933b9fda8126130b046975a968cc2d833b505475e588339e09f7672890"},
|
||||
{file = "numpy-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7678556eeb0152cbd1522b684dcd215250885993dd00adb93679ec3c0e6e091c"},
|
||||
{file = "numpy-2.2.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2e8da03bd561504d9b20e7a12340870dfc206c64ea59b4cfee9fceb95070ee94"},
|
||||
{file = "numpy-2.2.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:c9aa4496fd0e17e3843399f533d62857cef5900facf93e735ef65aa4bbc90ef0"},
|
||||
{file = "numpy-2.2.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4ca91d61a4bf61b0f2228f24bbfa6a9facd5f8af03759fe2a655c50ae2c6610"},
|
||||
{file = "numpy-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:deaa09cd492e24fd9b15296844c0ad1b3c976da7907e1c1ed3a0ad21dded6f76"},
|
||||
{file = "numpy-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:246535e2f7496b7ac85deffe932896a3577be7af8fb7eebe7146444680297e9a"},
|
||||
{file = "numpy-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:daf43a3d1ea699402c5a850e5313680ac355b4adc9770cd5cfc2940e7861f1bf"},
|
||||
{file = "numpy-2.2.3-cp313-cp313t-win32.whl", hash = "sha256:cf802eef1f0134afb81fef94020351be4fe1d6681aadf9c5e862af6602af64ef"},
|
||||
{file = "numpy-2.2.3-cp313-cp313t-win_amd64.whl", hash = "sha256:aee2512827ceb6d7f517c8b85aa5d3923afe8fc7a57d028cffcd522f1c6fd082"},
|
||||
{file = "numpy-2.2.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3c2ec8a0f51d60f1e9c0c5ab116b7fc104b165ada3f6c58abf881cb2eb16044d"},
|
||||
{file = "numpy-2.2.3-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:ed2cf9ed4e8ebc3b754d398cba12f24359f018b416c380f577bbae112ca52fc9"},
|
||||
{file = "numpy-2.2.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39261798d208c3095ae4f7bc8eaeb3481ea8c6e03dc48028057d3cbdbdb8937e"},
|
||||
{file = "numpy-2.2.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:783145835458e60fa97afac25d511d00a1eca94d4a8f3ace9fe2043003c678e4"},
|
||||
{file = "numpy-2.2.3.tar.gz", hash = "sha256:dbdc15f0c81611925f382dfa97b3bd0bc2c1ce19d4fe50482cb0ddc12ba30020"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3161,13 +3187,13 @@ diagrams = ["jinja2", "railroad-diagrams"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.3.4"
|
||||
version = "8.3.5"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"},
|
||||
{file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"},
|
||||
{file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"},
|
||||
{file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -3232,6 +3258,26 @@ pytest = ">=6.2.5"
|
||||
[package.extras]
|
||||
dev = ["pre-commit", "pytest-asyncio", "tox"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-xdist"
|
||||
version = "3.6.1"
|
||||
description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7"},
|
||||
{file = "pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
execnet = ">=2.1"
|
||||
pytest = ">=7.0.0"
|
||||
|
||||
[package.extras]
|
||||
psutil = ["psutil (>=3.0)"]
|
||||
setproctitle = ["setproctitle"]
|
||||
testing = ["filelock"]
|
||||
|
||||
[[package]]
|
||||
name = "python-dateutil"
|
||||
version = "2.9.0.post0"
|
||||
@@ -3507,109 +3553,6 @@ files = [
|
||||
attrs = ">=22.2.0"
|
||||
rpds-py = ">=0.7.0"
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "2024.11.6"
|
||||
description = "Alternative regular expression module, to replace re."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91"},
|
||||
{file = "regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0"},
|
||||
{file = "regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e"},
|
||||
{file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde"},
|
||||
{file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e"},
|
||||
{file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2"},
|
||||
{file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf"},
|
||||
{file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c"},
|
||||
{file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86"},
|
||||
{file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67"},
|
||||
{file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d"},
|
||||
{file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2"},
|
||||
{file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008"},
|
||||
{file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62"},
|
||||
{file = "regex-2024.11.6-cp310-cp310-win32.whl", hash = "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e"},
|
||||
{file = "regex-2024.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519"},
|
||||
{file = "regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638"},
|
||||
{file = "regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7"},
|
||||
{file = "regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20"},
|
||||
{file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114"},
|
||||
{file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3"},
|
||||
{file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f"},
|
||||
{file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0"},
|
||||
{file = "regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55"},
|
||||
{file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89"},
|
||||
{file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d"},
|
||||
{file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34"},
|
||||
{file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d"},
|
||||
{file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45"},
|
||||
{file = "regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9"},
|
||||
{file = "regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60"},
|
||||
{file = "regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a"},
|
||||
{file = "regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9"},
|
||||
{file = "regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2"},
|
||||
{file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4"},
|
||||
{file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577"},
|
||||
{file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3"},
|
||||
{file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e"},
|
||||
{file = "regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe"},
|
||||
{file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e"},
|
||||
{file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29"},
|
||||
{file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39"},
|
||||
{file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51"},
|
||||
{file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad"},
|
||||
{file = "regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54"},
|
||||
{file = "regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b"},
|
||||
{file = "regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84"},
|
||||
{file = "regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4"},
|
||||
{file = "regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0"},
|
||||
{file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0"},
|
||||
{file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7"},
|
||||
{file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7"},
|
||||
{file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c"},
|
||||
{file = "regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3"},
|
||||
{file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07"},
|
||||
{file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e"},
|
||||
{file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6"},
|
||||
{file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4"},
|
||||
{file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d"},
|
||||
{file = "regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff"},
|
||||
{file = "regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a"},
|
||||
{file = "regex-2024.11.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3a51ccc315653ba012774efca4f23d1d2a8a8f278a6072e29c7147eee7da446b"},
|
||||
{file = "regex-2024.11.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ad182d02e40de7459b73155deb8996bbd8e96852267879396fb274e8700190e3"},
|
||||
{file = "regex-2024.11.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ba9b72e5643641b7d41fa1f6d5abda2c9a263ae835b917348fc3c928182ad467"},
|
||||
{file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40291b1b89ca6ad8d3f2b82782cc33807f1406cf68c8d440861da6304d8ffbbd"},
|
||||
{file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cdf58d0e516ee426a48f7b2c03a332a4114420716d55769ff7108c37a09951bf"},
|
||||
{file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a36fdf2af13c2b14738f6e973aba563623cb77d753bbbd8d414d18bfaa3105dd"},
|
||||
{file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1cee317bfc014c2419a76bcc87f071405e3966da434e03e13beb45f8aced1a6"},
|
||||
{file = "regex-2024.11.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50153825ee016b91549962f970d6a4442fa106832e14c918acd1c8e479916c4f"},
|
||||
{file = "regex-2024.11.6-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea1bfda2f7162605f6e8178223576856b3d791109f15ea99a9f95c16a7636fb5"},
|
||||
{file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:df951c5f4a1b1910f1a99ff42c473ff60f8225baa1cdd3539fe2819d9543e9df"},
|
||||
{file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:072623554418a9911446278f16ecb398fb3b540147a7828c06e2011fa531e773"},
|
||||
{file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f654882311409afb1d780b940234208a252322c24a93b442ca714d119e68086c"},
|
||||
{file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:89d75e7293d2b3e674db7d4d9b1bee7f8f3d1609428e293771d1a962617150cc"},
|
||||
{file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:f65557897fc977a44ab205ea871b690adaef6b9da6afda4790a2484b04293a5f"},
|
||||
{file = "regex-2024.11.6-cp38-cp38-win32.whl", hash = "sha256:6f44ec28b1f858c98d3036ad5d7d0bfc568bdd7a74f9c24e25f41ef1ebfd81a4"},
|
||||
{file = "regex-2024.11.6-cp38-cp38-win_amd64.whl", hash = "sha256:bb8f74f2f10dbf13a0be8de623ba4f9491faf58c24064f32b65679b021ed0001"},
|
||||
{file = "regex-2024.11.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5704e174f8ccab2026bd2f1ab6c510345ae8eac818b613d7d73e785f1310f839"},
|
||||
{file = "regex-2024.11.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:220902c3c5cc6af55d4fe19ead504de80eb91f786dc102fbd74894b1551f095e"},
|
||||
{file = "regex-2024.11.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e7e351589da0850c125f1600a4c4ba3c722efefe16b297de54300f08d734fbf"},
|
||||
{file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5056b185ca113c88e18223183aa1a50e66507769c9640a6ff75859619d73957b"},
|
||||
{file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e34b51b650b23ed3354b5a07aab37034d9f923db2a40519139af34f485f77d0"},
|
||||
{file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5670bce7b200273eee1840ef307bfa07cda90b38ae56e9a6ebcc9f50da9c469b"},
|
||||
{file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08986dce1339bc932923e7d1232ce9881499a0e02925f7402fb7c982515419ef"},
|
||||
{file = "regex-2024.11.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93c0b12d3d3bc25af4ebbf38f9ee780a487e8bf6954c115b9f015822d3bb8e48"},
|
||||
{file = "regex-2024.11.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:764e71f22ab3b305e7f4c21f1a97e1526a25ebdd22513e251cf376760213da13"},
|
||||
{file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f056bf21105c2515c32372bbc057f43eb02aae2fda61052e2f7622c801f0b4e2"},
|
||||
{file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:69ab78f848845569401469da20df3e081e6b5a11cb086de3eed1d48f5ed57c95"},
|
||||
{file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:86fddba590aad9208e2fa8b43b4c098bb0ec74f15718bb6a704e3c63e2cef3e9"},
|
||||
{file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:684d7a212682996d21ca12ef3c17353c021fe9de6049e19ac8481ec35574a70f"},
|
||||
{file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a03e02f48cd1abbd9f3b7e3586d97c8f7a9721c436f51a5245b3b9483044480b"},
|
||||
{file = "regex-2024.11.6-cp39-cp39-win32.whl", hash = "sha256:41758407fc32d5c3c5de163888068cfee69cb4c2be844e7ac517a52770f9af57"},
|
||||
{file = "regex-2024.11.6-cp39-cp39-win_amd64.whl", hash = "sha256:b2837718570f95dd41675328e111345f9b7095d821bac435aac173ac80b19983"},
|
||||
{file = "regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.3"
|
||||
@@ -4030,13 +3973,13 @@ widechars = ["wcwidth"]
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "2.4.0"
|
||||
version = "2.5.0"
|
||||
description = "ANSI color formatting for output in terminal"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "termcolor-2.4.0-py3-none-any.whl", hash = "sha256:9297c0df9c99445c2412e832e882a7884038a25617c60cea2ad69488d4040d63"},
|
||||
{file = "termcolor-2.4.0.tar.gz", hash = "sha256:aab9e56047c8ac41ed798fa36d892a37aca6b3e9159f3e0c24bc64a9b3ac7b7a"},
|
||||
{file = "termcolor-2.5.0-py3-none-any.whl", hash = "sha256:37b17b5fc1e604945c2642c872a3764b5d547a48009871aea3edd3afa180afb8"},
|
||||
{file = "termcolor-2.5.0.tar.gz", hash = "sha256:998d8d27da6d48442e8e1f016119076b690d962507531df4890fcd2db2ef8a6f"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@@ -4538,4 +4481,4 @@ propcache = ">=0.2.0"
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.11"
|
||||
content-hash = "28a2b74bfafa9f93d14d2f8d1fcaffa340db212acce6469d6714d342203ad77f"
|
||||
content-hash = "35e03dba41d30cf6129a4a4f3107eca560f779205b21d3ffb2871eeffc5d5a64"
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "agentic_security"
|
||||
version = "0.5.1"
|
||||
version = "0.6.0"
|
||||
description = "Agentic LLM vulnerability scanner"
|
||||
authors = ["Alexander Miasoiedov <msoedov@gmail.com>"]
|
||||
maintainers = ["Alexander Miasoiedov <msoedov@gmail.com>"]
|
||||
@@ -54,6 +54,7 @@ pyfiglet = "^1.0.2"
|
||||
termcolor = "^2.4.0"
|
||||
|
||||
# garak = { version = "*", optional = true }
|
||||
pytest-xdist = "3.6.1"
|
||||
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
@@ -86,7 +87,7 @@ build-backend = "poetry.core.masonry.api"
|
||||
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
addopts = "--durations=5 -m 'not slow'"
|
||||
addopts = "--durations=5 -m 'not slow' -n auto"
|
||||
asyncio_mode = "auto"
|
||||
asyncio_default_fixture_loop_scope = "function"
|
||||
markers = "slow: marks tests as slow"
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
from agentic_security.probe_actor.refusal import DefaultRefusalClassifier
|
||||
|
||||
|
||||
class TestCheckRefusal:
|
||||
# The function correctly identifies a refusal phrase in the response.
|
||||
def test_identify_refusal_phrase(self):
|
||||
response = "I'm sorry, but I cannot provide that information."
|
||||
assert DefaultRefusalClassifier().is_refusal(response)
|
||||
|
||||
# The response is an empty string.
|
||||
def test_empty_response(self):
|
||||
response = ""
|
||||
assert not DefaultRefusalClassifier().is_refusal(response)
|
||||
+1
-1
@@ -3,7 +3,7 @@ from unittest.mock import patch
|
||||
import pandas as pd
|
||||
import pytest
|
||||
|
||||
from .model import RefusalClassifier
|
||||
from agentic_security.refusal_classifier.model import RefusalClassifier
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -1,6 +1,6 @@
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from ..app import app
|
||||
from agentic_security.app import app
|
||||
|
||||
|
||||
def test_health_check():
|
||||
@@ -5,10 +5,10 @@ import httpx
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from ..app import app
|
||||
from ..primitives import Probe
|
||||
from ..probe_actor.refusal import REFUSAL_MARKS
|
||||
from ..probe_data import REGISTRY
|
||||
from agentic_security.app import app
|
||||
from agentic_security.primitives import Probe
|
||||
from agentic_security.probe_actor.refusal import REFUSAL_MARKS
|
||||
from agentic_security.probe_data import REGISTRY
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
@@ -4,7 +4,7 @@ from unittest.mock import patch
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from .report import router
|
||||
from agentic_security.routes.report import router
|
||||
|
||||
client = TestClient(router)
|
||||
|
||||
@@ -4,8 +4,8 @@ import pytest
|
||||
from fastapi import HTTPException
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from ..primitives import Settings
|
||||
from .static import get_static_file, router
|
||||
from agentic_security.primitives import Settings
|
||||
from agentic_security.routes.static import get_static_file, router
|
||||
|
||||
client = TestClient(router)
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
import pytest
|
||||
import asyncio
|
||||
from fastapi import FastAPI
|
||||
from asyncio import Queue, Event
|
||||
from agentic_security.core.app import create_app, get_tools_inbox, get_stop_event, get_current_run, set_current_run
|
||||
|
||||
class TestApp:
|
||||
"""Test suite for agentic_security.core.app module."""
|
||||
|
||||
def test_create_app(self):
|
||||
"""Test that create_app returns a FastAPI instance."""
|
||||
app = create_app()
|
||||
assert isinstance(app, FastAPI)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_tools_inbox(self):
|
||||
"""Test that get_tools_inbox returns the global Queue instance."""
|
||||
queue1 = get_tools_inbox()
|
||||
await queue1.put("test item")
|
||||
queue2 = get_tools_inbox()
|
||||
result = queue2.get_nowait()
|
||||
assert result == "test item"
|
||||
|
||||
def test_get_stop_event(self):
|
||||
"""Test that get_stop_event returns the global Event instance and is not set initially."""
|
||||
event = get_stop_event()
|
||||
assert isinstance(event, Event)
|
||||
assert not event.is_set()
|
||||
|
||||
def test_current_run_initial(self):
|
||||
"""Test that get_current_run returns the global current_run with default values initially."""
|
||||
run = get_current_run()
|
||||
# Default values should be empty strings
|
||||
assert run["spec"] == ""
|
||||
assert run["id"] == ""
|
||||
|
||||
def test_set_current_run(self):
|
||||
"""Test that set_current_run correctly updates current_run."""
|
||||
spec = "test run"
|
||||
result = set_current_run(spec)
|
||||
expected_id = hash(id(spec))
|
||||
# Verify that spec is set correctly
|
||||
assert result["spec"] == spec
|
||||
assert result["id"] == expected_id
|
||||
|
||||
def test_current_run_after_set(self):
|
||||
"""Test that get_current_run returns the updated current_run after set_current_run is called."""
|
||||
spec = "another test run"
|
||||
set_current_run(spec)
|
||||
current = get_current_run()
|
||||
assert current["spec"] == spec
|
||||
assert current["id"] == hash(id(spec))
|
||||
def test_tools_inbox_same_instance(self):
|
||||
"""Test that get_tools_inbox returns the same Queue instance by default."""
|
||||
queue1 = get_tools_inbox()
|
||||
queue2 = get_tools_inbox()
|
||||
assert queue1 is queue2
|
||||
|
||||
def test_stop_event_set(self):
|
||||
"""Test that setting the stop event is reflected in subsequent calls."""
|
||||
event = get_stop_event()
|
||||
event.set() # set the global event
|
||||
# Now, subsequent calls should return the same event which is set.
|
||||
event2 = get_stop_event()
|
||||
assert event2.is_set()
|
||||
|
||||
def test_set_current_run_with_none(self):
|
||||
"""Test that set_current_run handles None as a valid input and updates current_run accordingly."""
|
||||
result = set_current_run(None)
|
||||
expected_id = hash(id(None))
|
||||
assert result["spec"] is None
|
||||
assert result["id"] == expected_id
|
||||
|
||||
def test_multiple_current_run_assignments(self):
|
||||
"""Test multiple assignments to current_run to ensure it always updates correctly."""
|
||||
first_spec = "first run"
|
||||
result1 = set_current_run(first_spec)
|
||||
expected_id1 = hash(id(first_spec))
|
||||
assert result1["spec"] == first_spec
|
||||
assert result1["id"] == expected_id1
|
||||
|
||||
second_spec = "second run"
|
||||
result2 = set_current_run(second_spec)
|
||||
expected_id2 = hash(id(second_spec))
|
||||
assert result2["spec"] == second_spec
|
||||
assert result2["id"] == expected_id2
|
||||
|
||||
current = get_current_run()
|
||||
# The current_run should reflect the latest assignment.
|
||||
assert current["spec"] == second_spec
|
||||
assert current["id"] == expected_id2
|
||||
@pytest.mark.asyncio
|
||||
async def test_empty_tools_inbox_exception(self):
|
||||
"""Test that calling get_nowait on an empty tools_inbox raises QueueEmpty."""
|
||||
from asyncio import QueueEmpty
|
||||
queue = get_tools_inbox()
|
||||
# Clear any existing items in the queue
|
||||
while True:
|
||||
try:
|
||||
queue.get_nowait()
|
||||
except QueueEmpty:
|
||||
break
|
||||
with pytest.raises(QueueEmpty):
|
||||
queue.get_nowait()
|
||||
|
||||
def test_set_current_run_with_dict(self):
|
||||
"""Test that set_current_run correctly handles a dictionary input as spec."""
|
||||
spec = {"key": "value"}
|
||||
result = set_current_run(spec)
|
||||
expected_id = hash(id(spec))
|
||||
assert result["spec"] == spec
|
||||
assert result["id"] == expected_id
|
||||
@pytest.mark.asyncio
|
||||
async def test_stop_event_wait(self):
|
||||
"""Test that waiting on the stop event returns once the event is set."""
|
||||
event = get_stop_event()
|
||||
event.clear() # ensure event is not set
|
||||
async def waiter():
|
||||
await event.wait()
|
||||
return True
|
||||
waiter_task = asyncio.create_task(waiter())
|
||||
# Wait a moment to ensure the waiter is pending
|
||||
await asyncio.sleep(0.1)
|
||||
assert not waiter_task.done()
|
||||
event.set()
|
||||
result = await waiter_task
|
||||
assert result is True
|
||||
|
||||
def test_set_current_run_with_int(self):
|
||||
"""Test that set_current_run handles an integer input as spec."""
|
||||
spec = 12345
|
||||
result = set_current_run(spec)
|
||||
expected_id = hash(id(spec))
|
||||
assert result["spec"] == spec
|
||||
assert result["id"] == expected_id
|
||||
|
||||
def test_create_app_routes(self):
|
||||
"""Test that create_app returns a FastAPI instance with default routes available."""
|
||||
app = create_app()
|
||||
paths = [route.path for route in app.routes]
|
||||
# Check that the default OpenAPI route exists
|
||||
assert "/openapi.json" in paths
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tools_inbox_async_put_get_order(self):
|
||||
"""Test that tools_inbox preserves order when items are added and retrieved asynchronously."""
|
||||
queue = get_tools_inbox()
|
||||
# Clear any existing items in the queue
|
||||
from asyncio import QueueEmpty
|
||||
while True:
|
||||
try:
|
||||
queue.get_nowait()
|
||||
except QueueEmpty:
|
||||
break
|
||||
items = ["first", "second", "third"]
|
||||
for item in items:
|
||||
await queue.put(item)
|
||||
result_items = []
|
||||
for _ in items:
|
||||
result_items.append(await queue.get())
|
||||
assert result_items == items
|
||||
@@ -1,341 +0,0 @@
|
||||
import pytest
|
||||
import base64
|
||||
import httpx
|
||||
import asyncio
|
||||
from agentic_security.http_spec import (
|
||||
LLMSpec,
|
||||
parse_http_spec,
|
||||
escape_special_chars_for_json,
|
||||
encode_image_base64_by_url,
|
||||
encode_audio_base64_by_url,
|
||||
InvalidHTTPSpecError,
|
||||
Modality
|
||||
)
|
||||
|
||||
################################################################################
|
||||
# Tests for agentic_security/http_spec.py
|
||||
################################################################################
|
||||
|
||||
def test_escape_special_chars_for_json():
|
||||
"""Test escaping special characters in a prompt for JSON safety."""
|
||||
prompt = 'Line1\nLine2\t"Quote"\\Backslash'
|
||||
escaped = escape_special_chars_for_json(prompt)
|
||||
assert '\\n' in escaped
|
||||
assert '\\t' in escaped
|
||||
assert '\\"' in escaped
|
||||
assert '\\\\' in escaped
|
||||
|
||||
def test_parse_http_spec_text():
|
||||
"""Test parsing a text HTTP spec without image/audio/files requirements."""
|
||||
spec = "POST http://example.com/api\nContent-Type: application/json\n\nThis is a prompt: <<PROMPT>>"
|
||||
llm_spec = parse_http_spec(spec)
|
||||
assert llm_spec.method == "POST"
|
||||
assert llm_spec.url == "http://example.com/api"
|
||||
assert llm_spec.headers["Content-Type"] == "application/json"
|
||||
assert "<<PROMPT>>" in llm_spec.body
|
||||
assert not llm_spec.has_files
|
||||
assert not llm_spec.has_image
|
||||
assert not llm_spec.has_audio
|
||||
|
||||
def test_parse_http_spec_files():
|
||||
"""Test parsing a HTTP spec with multipart/form-data header indicating files."""
|
||||
spec = "PUT http://example.com/upload\nContent-Type: multipart/form-data\n\nFile upload test"
|
||||
llm_spec = parse_http_spec(spec)
|
||||
assert llm_spec.has_files
|
||||
|
||||
def test_parse_http_spec_image_audio():
|
||||
"""Test parsing a HTTP spec that requires image and audio via placeholders."""
|
||||
spec = "GET http://example.com/api\nContent-Type: application/json\n\nImage: <<BASE64_IMAGE>> and Audio: <<BASE64_AUDIO>>"
|
||||
llm_spec = parse_http_spec(spec)
|
||||
assert llm_spec.has_image
|
||||
assert llm_spec.has_audio
|
||||
|
||||
def test_encode_image_base64_by_url(monkeypatch):
|
||||
"""Test that image encoding returns the correct base64 string with prefix."""
|
||||
dummy_content = b'test_image'
|
||||
class DummyResponse:
|
||||
def __init__(self, content):
|
||||
self.content = content
|
||||
|
||||
def dummy_get(url):
|
||||
return DummyResponse(dummy_content)
|
||||
|
||||
monkeypatch.setattr(httpx, "get", dummy_get)
|
||||
result = encode_image_base64_by_url("http://dummyurl.com/image.jpg")
|
||||
expected = "data:image/jpeg;base64," + base64.b64encode(dummy_content).decode("utf-8")
|
||||
assert result == expected
|
||||
|
||||
def test_encode_audio_base64_by_url(monkeypatch):
|
||||
"""Test that audio encoding returns the correct base64 string with prefix."""
|
||||
dummy_content = b'test_audio'
|
||||
class DummyResponse:
|
||||
def __init__(self, content):
|
||||
self.content = content
|
||||
|
||||
def dummy_get(url):
|
||||
return DummyResponse(dummy_content)
|
||||
|
||||
monkeypatch.setattr(httpx, "get", dummy_get)
|
||||
result = encode_audio_base64_by_url("http://dummyurl.com/audio.mp3")
|
||||
expected = "data:audio/mpeg;base64," + base64.b64encode(dummy_content).decode("utf-8")
|
||||
assert result == expected
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_probe_text(monkeypatch):
|
||||
"""Test the probe function for text modality by replacing <<PROMPT>>."""
|
||||
spec = "POST http://example.com/api\nContent-Type: application/json\n\n{\"prompt\": \"<<PROMPT>>\"}"
|
||||
llm_spec = parse_http_spec(spec)
|
||||
|
||||
async def dummy_request(self, method, url, headers, content, timeout):
|
||||
return httpx.Response(200, text="ok")
|
||||
|
||||
monkeypatch.setattr(httpx.AsyncClient, "request", dummy_request)
|
||||
response = await llm_spec.probe("Hello")
|
||||
assert response.status_code == 200
|
||||
assert "ok" in response.text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_probe_with_files(monkeypatch):
|
||||
"""Test that probe correctly branches to _probe_with_files when files are provided."""
|
||||
spec = "POST http://example.com/api\nContent-Type: multipart/form-data\n\nFile data"
|
||||
llm_spec = parse_http_spec(spec)
|
||||
files = {"file": ("dummy.txt", b"data")}
|
||||
|
||||
async def dummy_request(self, method, url, headers, files, timeout):
|
||||
return httpx.Response(200, text="file upload ok")
|
||||
|
||||
monkeypatch.setattr(httpx.AsyncClient, "request", dummy_request)
|
||||
response = await llm_spec.probe("Unused", files=files)
|
||||
assert response.status_code == 200
|
||||
assert "file upload ok" in response.text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_verify_image(monkeypatch):
|
||||
"""Test verify method branch for image modality by monkeypatching image encoder."""
|
||||
spec = "POST http://example.com/api\nContent-Type: application/json\n\n{\"image\": \"<<BASE64_IMAGE>>\"}"
|
||||
llm_spec = parse_http_spec(spec)
|
||||
|
||||
# Replace the image encoder to return a dummy string
|
||||
monkeypatch.setattr("agentic_security.http_spec.encode_image_base64_by_url", lambda url="": "dummy_image")
|
||||
|
||||
async def dummy_request(self, method, url, headers, content, timeout):
|
||||
# Check that the dummy image is injected in the content
|
||||
assert "dummy_image" in content
|
||||
return httpx.Response(200, text="image ok")
|
||||
|
||||
monkeypatch.setattr(httpx.AsyncClient, "request", dummy_request)
|
||||
response = await llm_spec.verify()
|
||||
assert response.status_code == 200
|
||||
assert "image ok" in response.text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_verify_audio(monkeypatch):
|
||||
"""Test verify method branch for audio modality by monkeypatching audio encoder."""
|
||||
spec = "POST http://example.com/api\nContent-Type: application/json\n\n{\"audio\": \"<<BASE64_AUDIO>>\"}"
|
||||
llm_spec = parse_http_spec(spec)
|
||||
|
||||
monkeypatch.setattr("agentic_security.http_spec.encode_audio_base64_by_url", lambda url: "dummy_audio")
|
||||
|
||||
async def dummy_request(self, method, url, headers, content, timeout):
|
||||
# Ensure that the dummy audio string is present in the request content
|
||||
assert "dummy_audio" in content
|
||||
return httpx.Response(200, text="audio ok")
|
||||
|
||||
monkeypatch.setattr(httpx.AsyncClient, "request", dummy_request)
|
||||
response = await llm_spec.verify()
|
||||
assert response.status_code == 200
|
||||
assert "audio ok" in response.text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_verify_files(monkeypatch):
|
||||
"""Test verify method branch for files modality where _probe_with_files is invoked."""
|
||||
spec = "POST http://example.com/api\nContent-Type: multipart/form-data\n\nFile data"
|
||||
llm_spec = parse_http_spec(spec)
|
||||
|
||||
async def dummy_request(self, method, url, headers, files, timeout):
|
||||
return httpx.Response(200, text="files ok")
|
||||
|
||||
monkeypatch.setattr(httpx.AsyncClient, "request", dummy_request)
|
||||
response = await llm_spec.verify()
|
||||
assert response.status_code == 200
|
||||
assert "files ok" in response.text
|
||||
|
||||
def test_llm_spec_modality_property():
|
||||
"""Test that the modality property reflects the correct modality."""
|
||||
spec_text = "POST http://example.com/api\nContent-Type: application/json\n\nPrompt: <<PROMPT>>"
|
||||
llm_spec_text = parse_http_spec(spec_text)
|
||||
assert llm_spec_text.modality == Modality.TEXT
|
||||
|
||||
spec_image = "POST http://example.com/api\nContent-Type: application/json\n\nImage: <<BASE64_IMAGE>>"
|
||||
llm_spec_image = parse_http_spec(spec_image)
|
||||
assert llm_spec_image.modality == Modality.IMAGE
|
||||
|
||||
spec_audio = "POST http://example.com/api\nContent-Type: application/json\n\nAudio: <<BASE64_AUDIO>>"
|
||||
llm_spec_audio = parse_http_spec(spec_audio)
|
||||
assert llm_spec_audio.modality == Modality.AUDIO
|
||||
|
||||
def test_from_string_invalid():
|
||||
"""Test that LLMSpec.from_string raises an error for an invalid spec."""
|
||||
invalid_spec = "INVALID_SPEC"
|
||||
with pytest.raises(InvalidHTTPSpecError):
|
||||
LLMSpec.from_string(invalid_spec)
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_missing_files():
|
||||
"""Test that LLMSpec.validate raises a ValueError when files are required but missing."""
|
||||
spec = "POST http://example.com/api\nContent-Type: multipart/form-data\n\nFile upload test"
|
||||
llm_spec = parse_http_spec(spec)
|
||||
with pytest.raises(ValueError, match="Files are required"):
|
||||
llm_spec.validate("test prompt", "", "", {})
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_missing_image():
|
||||
"""Test that LLMSpec.validate raises a ValueError when an image is required but missing."""
|
||||
spec = "POST http://example.com/api\nContent-Type: application/json\n\nImage: <<BASE64_IMAGE>>"
|
||||
llm_spec = parse_http_spec(spec)
|
||||
with pytest.raises(ValueError, match="An image is required"):
|
||||
llm_spec.validate("test prompt", "", "dummy_audio", {})
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_missing_audio():
|
||||
"""Test that LLMSpec.validate raises a ValueError when audio is required but missing."""
|
||||
spec = "POST http://example.com/api\nContent-Type: application/json\n\nAudio: <<BASE64_AUDIO>>"
|
||||
llm_spec = parse_http_spec(spec)
|
||||
with pytest.raises(ValueError, match="Audio is required"):
|
||||
llm_spec.validate("test prompt", "dummy_image", "", {})
|
||||
|
||||
def test_fn_alias(monkeypatch):
|
||||
"""Test that LLMSpec.fn is a functional alias for LLMSpec.probe."""
|
||||
spec = "POST http://example.com/api\nContent-Type: application/json\n\n{\"prompt\": \"<<PROMPT>>\"}"
|
||||
llm_spec = parse_http_spec(spec)
|
||||
|
||||
# Instead of overriding the instance method, verify the alias at the class level.
|
||||
assert LLMSpec.fn is LLMSpec.probe
|
||||
|
||||
def test_escape_special_chars_no_special():
|
||||
"""Test that the escape function returns the original string if no special characters are present."""
|
||||
prompt = "Simple text without specials"
|
||||
escaped = escape_special_chars_for_json(prompt)
|
||||
assert escaped == "Simple text without specials"
|
||||
@pytest.mark.asyncio
|
||||
async def test_probe_text_with_special_chars(monkeypatch):
|
||||
"""Test probe for text modality with special characters in prompt ensuring escaped content."""
|
||||
spec = "POST http://example.com/api\nContent-Type: application/json\n\n{\"prompt\": \"<<PROMPT>>\"}"
|
||||
llm_spec = parse_http_spec(spec)
|
||||
captured = {}
|
||||
|
||||
async def dummy_request(self, method, url, headers, content, timeout):
|
||||
captured['content'] = content
|
||||
return httpx.Response(200, text="ok")
|
||||
|
||||
monkeypatch.setattr(httpx.AsyncClient, "request", dummy_request)
|
||||
test_prompt = 'Hello\nWorld\t"Test"'
|
||||
response = await llm_spec.probe(test_prompt)
|
||||
expected_escaped = escape_special_chars_for_json(test_prompt)
|
||||
assert expected_escaped in captured['content']
|
||||
assert response.status_code == 200
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_verify_both_image_audio(monkeypatch):
|
||||
"""Test verify method when both image and audio placeholders are present.
|
||||
Expect a ValueError because only the image branch is triggered by pattern matching and the missing audio causes validation to fail."""
|
||||
spec = ("POST http://example.com/api\nContent-Type: application/json\n\n"
|
||||
"{\"audio\": \"<<BASE64_AUDIO>>\", \"image\":\"<<BASE64_IMAGE>>\"}")
|
||||
llm_spec = parse_http_spec(spec)
|
||||
# Monkey patch the image encoder to return a dummy value
|
||||
monkeypatch.setattr("agentic_security.http_spec.encode_image_base64_by_url", lambda url="": "dummy_image")
|
||||
with pytest.raises(ValueError, match="Audio is required"):
|
||||
await llm_spec.verify()
|
||||
|
||||
def test_parse_http_spec_invalid_header_format():
|
||||
"""Test that parse_http_spec raises an error when a header line doesn't have the expected 'key: value' format."""
|
||||
invalid_spec = "GET http://example.com/api\nInvalidHeaderWithoutColon\n\nBody with <<PROMPT>>"
|
||||
with pytest.raises(ValueError):
|
||||
parse_http_spec(invalid_spec)
|
||||
|
||||
def test_from_string_valid():
|
||||
"""Test that LLMSpec.from_string returns a valid LLMSpec object when given a proper spec string."""
|
||||
spec = "GET http://example.com/api\nContent-Type: application/json\n\n{ \"prompt\": \"<<PROMPT>>\" }"
|
||||
llm_spec = LLMSpec.from_string(spec)
|
||||
assert llm_spec.method == "GET"
|
||||
assert llm_spec.url == "http://example.com/api"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_parse_http_spec_multiline_body():
|
||||
"""Test parsing an HTTP spec with a multiline body to ensure body concatenation works."""
|
||||
spec = (
|
||||
"PATCH http://example.com/api\n"
|
||||
"Content-Type: application/json\n"
|
||||
"\n"
|
||||
"Line one of body\n"
|
||||
"Line two of body\n"
|
||||
"Line three"
|
||||
)
|
||||
llm_spec = parse_http_spec(spec)
|
||||
# As implemented, the parser concatenates lines without newline delimiters
|
||||
expected_body = "Line one of bodyLine two of bodyLine three"
|
||||
assert llm_spec.body == expected_body
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_encode_image_default_argument(monkeypatch):
|
||||
"""Test that encode_image_base64_by_url works with its default URL argument."""
|
||||
dummy_content = b'default_image'
|
||||
class DummyResponse:
|
||||
def __init__(self, content):
|
||||
self.content = content
|
||||
|
||||
def dummy_get(url):
|
||||
# check that the default URL (which includes 'fluidicon.png') is used
|
||||
assert "fluidicon.png" in url
|
||||
return DummyResponse(dummy_content)
|
||||
|
||||
monkeypatch.setattr(httpx, "get", dummy_get)
|
||||
result = encode_image_base64_by_url()
|
||||
expected = "data:image/jpeg;base64," + base64.b64encode(dummy_content).decode("utf-8")
|
||||
assert result == expected
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_probe_without_prompt_placeholder(monkeypatch):
|
||||
"""Test the probe function when the request body does not include the <<PROMPT>> placeholder."""
|
||||
spec = "POST http://example.com/api\nContent-Type: application/json\n\n{\"message\": \"No placeholder here\"}"
|
||||
llm_spec = parse_http_spec(spec)
|
||||
|
||||
captured = {}
|
||||
|
||||
async def dummy_request(self, method, url, headers, content, timeout):
|
||||
captured['content'] = content
|
||||
return httpx.Response(200, text="ok without placeholder")
|
||||
|
||||
monkeypatch.setattr(httpx.AsyncClient, "request", dummy_request)
|
||||
response = await llm_spec.probe("Ignored prompt")
|
||||
assert "No placeholder here" in captured['content']
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_validate_success():
|
||||
"""Test that LLMSpec.validate does not raise an error when all required data is provided."""
|
||||
# Test case for files: files are provided as required
|
||||
spec_files = "POST http://example.com/api\nContent-Type: multipart/form-data\n\nFile upload"
|
||||
llm_spec_files = parse_http_spec(spec_files)
|
||||
llm_spec_files.validate("some prompt", "dummy_image", "dummy_audio", {"file": ("dummy.txt", b"data")})
|
||||
|
||||
# Test case for image: image is provided as required
|
||||
spec_image = "POST http://example.com/api\nContent-Type: application/json\n\nImage: <<BASE64_IMAGE>>"
|
||||
llm_spec_image = parse_http_spec(spec_image)
|
||||
llm_spec_image.validate("some prompt", "dummy_image", "dummy_audio", {})
|
||||
|
||||
# Test case for audio: audio is provided as required
|
||||
spec_audio = "POST http://example.com/api\nContent-Type: application/json\n\nAudio: <<BASE64_AUDIO>>"
|
||||
llm_spec_audio = parse_http_spec(spec_audio)
|
||||
llm_spec_audio.validate("some prompt", "dummy_image", "dummy_audio", {})
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_probe_invalid_url(monkeypatch):
|
||||
"""Test that probe raises an exception when the HTTP client fails due to an invalid URL."""
|
||||
spec = "GET http://nonexistent_url/api\nContent-Type: application/json\n\n{\"prompt\": \"<<PROMPT>>\"}"
|
||||
llm_spec = parse_http_spec(spec)
|
||||
|
||||
async def dummy_request(self, method, url, headers, content, timeout):
|
||||
raise httpx.RequestError("Invalid URL")
|
||||
|
||||
monkeypatch.setattr(httpx.AsyncClient, "request", dummy_request)
|
||||
with pytest.raises(httpx.RequestError):
|
||||
await llm_spec.probe("Test")
|
||||
@@ -0,0 +1,136 @@
|
||||
import io
|
||||
import string
|
||||
import pytest
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from agentic_security.report_chart import plot_security_report, generate_identifiers
|
||||
|
||||
class TestReportChart:
|
||||
"""Test suite for agentic_security.report_chart module."""
|
||||
|
||||
def test_generate_identifiers_short(self):
|
||||
"""Test generate_identifiers with a small dataset."""
|
||||
df = pd.DataFrame([{'dummy': i} for i in range(5)])
|
||||
identifiers = generate_identifiers(df)
|
||||
expected = ['A1', 'A2', 'A3', 'A4', 'A5']
|
||||
assert identifiers == expected
|
||||
|
||||
def test_generate_identifiers_edge(self):
|
||||
"""Test generate_identifiers with more than 26 items to cover cycling over the alphabet."""
|
||||
n = 30
|
||||
df = pd.DataFrame([{'dummy': i} for i in range(n)])
|
||||
identifiers = generate_identifiers(df)
|
||||
# For i=25, identifier should be A26, and for i=26, identifier should be B1
|
||||
assert identifiers[25] == 'A26'
|
||||
assert identifiers[26] == 'B1'
|
||||
assert len(identifiers) == n
|
||||
|
||||
def test_generate_identifiers_empty(self):
|
||||
"""Test generate_identifiers with an empty dataframe."""
|
||||
df = pd.DataFrame([])
|
||||
identifiers = generate_identifiers(df)
|
||||
assert identifiers == []
|
||||
|
||||
def test_plot_security_report_png_output(self):
|
||||
"""Test plot_security_report returns valid PNG output."""
|
||||
# Create a sample table with required columns
|
||||
table = [
|
||||
{"failureRate": 10, "tokens": 100, "module": "Module1"},
|
||||
{"failureRate": 30, "tokens": 200, "module": "Module2"},
|
||||
{"failureRate": 20, "tokens": 150, "module": "Module3"},
|
||||
]
|
||||
buf = plot_security_report(table)
|
||||
# Check that buf is a BytesIO object and starts with PNG header bytes
|
||||
assert isinstance(buf, io.BytesIO)
|
||||
buf.seek(0)
|
||||
header = buf.read(8)
|
||||
assert header.startswith(b'\x89PNG')
|
||||
|
||||
def test_plot_security_report_ordering(self, monkeypatch):
|
||||
"""Test that the table embedded in the plot contains correctly sorted order by descending failure rate."""
|
||||
table = [
|
||||
{"failureRate": 15, "tokens": 110, "module": "ModuleA"},
|
||||
{"failureRate": 25, "tokens": 210, "module": "ModuleB"},
|
||||
{"failureRate": 5, "tokens": 90, "module": "ModuleC"},
|
||||
]
|
||||
result_holder = {}
|
||||
from matplotlib.axes import Axes
|
||||
original_table = Axes.table
|
||||
def fake_table(self, *args, **kwargs):
|
||||
result_holder['cellText'] = kwargs.get('cellText')
|
||||
return original_table(self, *args, **kwargs)
|
||||
monkeypatch.setattr(Axes, "table", fake_table)
|
||||
plot_security_report(table)
|
||||
cell_text = result_holder.get('cellText')
|
||||
assert cell_text is not None
|
||||
# Verify header row in the table
|
||||
assert cell_text[0] == ["Threat"]
|
||||
# Since the data are sorted (highest failure rate first), ModuleB (25.0%) should appear in one of the rows.
|
||||
found = any("ModuleB (25.0%)" in row[0] for row in cell_text[1:])
|
||||
assert found
|
||||
|
||||
def test_plot_security_report_one_entry(self):
|
||||
"""Test plot_security_report with a single entry."""
|
||||
table = [{"failureRate": 50, "tokens": 300, "module": "OnlyModule"}]
|
||||
buf = plot_security_report(table)
|
||||
assert isinstance(buf, io.BytesIO)
|
||||
buf.seek(0)
|
||||
content = buf.read()
|
||||
assert content.startswith(b'\x89PNG')
|
||||
def test_generate_identifiers_many(self):
|
||||
"""Test generate_identifiers with 52 items to verify identifier sequence."""
|
||||
n = 52
|
||||
df = pd.DataFrame([{'dummy': i} for i in range(n)])
|
||||
identifiers = generate_identifiers(df)
|
||||
assert identifiers[0] == "A1"
|
||||
assert identifiers[25] == "A26"
|
||||
assert identifiers[26] == "B1"
|
||||
assert identifiers[51] == "B26"
|
||||
|
||||
def test_plot_security_report_missing_failureRate(self):
|
||||
"""Test plot_security_report raises KeyError when 'failureRate' column is missing."""
|
||||
table = [{"tokens": 100, "module": "Mod1"}] # Missing 'failureRate'
|
||||
with pytest.raises(KeyError):
|
||||
plot_security_report(table)
|
||||
|
||||
def test_plot_security_report_missing_tokens(self):
|
||||
"""Test plot_security_report raises KeyError when 'tokens' column is missing."""
|
||||
table = [{"failureRate": 10, "module": "Mod1"}] # Missing 'tokens'
|
||||
with pytest.raises(KeyError):
|
||||
plot_security_report(table)
|
||||
|
||||
def test_plot_security_report_empty_table(self):
|
||||
"""Test plot_security_report raises KeyError when the table is empty."""
|
||||
table = []
|
||||
with pytest.raises(KeyError):
|
||||
plot_security_report(table)
|
||||
def test_plot_security_report_missing_module(self):
|
||||
"""Test plot_security_report raises KeyError when 'module' column is missing."""
|
||||
table = [{"failureRate": 10, "tokens": 100}] # Missing 'module'
|
||||
with pytest.raises(KeyError):
|
||||
plot_security_report(table)
|
||||
|
||||
def test_plot_security_report_failure_rate_labels(self, monkeypatch):
|
||||
"""Test that plot_security_report calls ax.text for each failure rate bar label."""
|
||||
table = [
|
||||
{"failureRate": 10, "tokens": 100, "module": "Mod1"},
|
||||
{"failureRate": 20, "tokens": 150, "module": "Mod2"},
|
||||
{"failureRate": 30, "tokens": 200, "module": "Mod3"},
|
||||
]
|
||||
# Count the number of times ax.text is called for drawing failure rate labels.
|
||||
call_count = [0]
|
||||
from matplotlib.axes import Axes
|
||||
original_text = Axes.text
|
||||
def fake_text(self, *args, **kwargs):
|
||||
call_count[0] += 1
|
||||
return original_text(self, *args, **kwargs)
|
||||
monkeypatch.setattr(Axes, "text", fake_text)
|
||||
plot_security_report(table)
|
||||
# The loop inside plot_security_report calls ax.text once for each data point.
|
||||
assert call_count[0] == len(table)
|
||||
|
||||
def test_plot_security_report_non_numeric_failureRate(self):
|
||||
"""Test that plot_security_report raises an exception when failureRate is non-numeric."""
|
||||
table = [{"failureRate": "invalid", "tokens": 100, "module": "ModX"}]
|
||||
with pytest.raises(Exception):
|
||||
plot_security_report(table)
|
||||
@@ -0,0 +1,126 @@
|
||||
import io
|
||||
import asyncio
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from threading import Event
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from agentic_security.routes import scan
|
||||
|
||||
# Dummy LLMSpec for success tests
|
||||
class DummyLLMSpec:
|
||||
def __init__(self, spec_string):
|
||||
self.spec_string = spec_string
|
||||
async def verify(self):
|
||||
class DummyResponse:
|
||||
status_code = 200
|
||||
text = "verification succeeded"
|
||||
elapsed = timedelta(seconds=0.5)
|
||||
return DummyResponse()
|
||||
@classmethod
|
||||
def from_string(cls, spec_string):
|
||||
return DummyLLMSpec(spec_string)
|
||||
|
||||
# Dummy scan_router generator to simulate streaming responses
|
||||
async def dummy_scan_router(request_factory, scan_parameters, tools_inbox, stop_event):
|
||||
for i in range(2):
|
||||
yield f"result {i}"
|
||||
|
||||
# Define a dummy Secrets class for testing purposes.
|
||||
class DummySecrets:
|
||||
def __init__(self):
|
||||
self.secrets = {}
|
||||
|
||||
# Create FastAPI app for testing and include the scan router.
|
||||
@pytest.fixture
|
||||
def app():
|
||||
app = FastAPI()
|
||||
app.include_router(scan.router)
|
||||
return app
|
||||
|
||||
@pytest.fixture
|
||||
def client(app):
|
||||
return TestClient(app)
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def patch_dependencies(monkeypatch):
|
||||
# Patch LLMSpec used in the routes with our dummy implementation.
|
||||
monkeypatch.setattr(scan, "LLMSpec", DummyLLMSpec)
|
||||
# Patch fuzzer.scan_router to use our dummy scanning generator.
|
||||
monkeypatch.setattr(scan.fuzzer, "scan_router", dummy_scan_router)
|
||||
# Patch get_stop_event to return a dummy Event.
|
||||
dummy_event = Event()
|
||||
monkeypatch.setattr(scan, "get_stop_event", lambda: dummy_event)
|
||||
# Patch get_tools_inbox to return None.
|
||||
monkeypatch.setattr(scan, "get_tools_inbox", lambda: None)
|
||||
# Patch set_current_run to be a no-op.
|
||||
monkeypatch.setattr(scan, "set_current_run", lambda x: None)
|
||||
# Patch get_in_memory_secrets to return a DummySecrets instance.
|
||||
monkeypatch.setattr(scan, "get_in_memory_secrets", lambda: DummySecrets())
|
||||
# Ensure Scan.with_secrets is a no-op if not already implemented.
|
||||
if not hasattr(scan.Scan, "with_secrets"):
|
||||
monkeypatch.setattr(scan.Scan, "with_secrets", lambda self, secrets: None)
|
||||
|
||||
def test_verify_success(client):
|
||||
"""Test /verify endpoint for a successful verification."""
|
||||
data = {"spec": "dummy"}
|
||||
response = client.post("/verify", json=data)
|
||||
res_json = response.json()
|
||||
assert response.status_code == 200
|
||||
assert res_json["status_code"] == 200
|
||||
assert res_json["body"] == "verification succeeded"
|
||||
assert "elapsed" in res_json
|
||||
assert "timestamp" in res_json
|
||||
|
||||
def test_verify_failure(client, monkeypatch):
|
||||
"""Test /verify endpoint when verification fails."""
|
||||
class DummyLLMSpecFailure:
|
||||
def __init__(self, spec_string):
|
||||
self.spec_string = spec_string
|
||||
async def verify(self):
|
||||
raise Exception("verification error")
|
||||
@classmethod
|
||||
def from_string(cls, spec_string):
|
||||
return DummyLLMSpecFailure(spec_string)
|
||||
monkeypatch.setattr(scan, "LLMSpec", DummyLLMSpecFailure)
|
||||
data = {"spec": "bad"}
|
||||
response = client.post("/verify", json=data)
|
||||
assert response.status_code == 400
|
||||
assert "verification error" in response.text
|
||||
|
||||
def test_scan(client):
|
||||
"""Test /scan endpoint to ensure streaming response works."""
|
||||
data = {"llmSpec": "dummy", "optimize": False, "maxBudget": 10, "enableMultiStepAttack": False}
|
||||
response = client.post("/scan", json=data)
|
||||
assert response.status_code == 200
|
||||
content = list(response.iter_lines())
|
||||
expected = ["result 0", "result 1"]
|
||||
assert content == expected
|
||||
|
||||
def test_stop_scan(client):
|
||||
"""Test /stop endpoint to ensure scan stopping functionality."""
|
||||
dummy_event = scan.get_stop_event()
|
||||
dummy_event.clear()
|
||||
response = client.post("/stop")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"status": "Scan stopped"}
|
||||
assert dummy_event.is_set()
|
||||
|
||||
def test_scan_csv(client):
|
||||
"""Test /scan-csv endpoint with CSV file and llmSpec upload."""
|
||||
csv_content = b"col1,col2\nvalue1,value2"
|
||||
llm_spec_content = b"dummy"
|
||||
files = {
|
||||
"file": ("dummy.csv", csv_content, "text/csv"),
|
||||
"llmSpec": ("spec.txt", llm_spec_content, "text/plain"),
|
||||
}
|
||||
response = client.post(
|
||||
"/scan-csv",
|
||||
files=files,
|
||||
data={"optimize": "false", "maxBudget": "10", "enableMultiStepAttack": "false"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
content = list(response.iter_lines())
|
||||
expected = ["result 0", "result 1"]
|
||||
assert content == expected
|
||||
Reference in New Issue
Block a user