Spaces:
Sleeping
Sleeping
Simon commited on
Commit ·
369e64d
1
Parent(s): f2fb12f
Simon/basic_url_scan_integreted
Browse files* Add URLScan.io API client and example usage for URL analysis
* Add URLScan.io API client and example usage for URL analysis
- .env.example +7 -0
- docs/urlscan_api.md +169 -0
- examples/urlscan_example.py +72 -0
- pyproject.toml +4 -0
- src/phising_detection/api/__init__.py +5 -0
- src/phising_detection/api/urlscan.py +232 -0
- src/phising_detection/data/load_data.py +1 -3
- tests/test_urlscan.py +275 -0
- uv.lock +188 -0
.env.example
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# URLScan.io API Configuration
|
| 2 |
+
# Get your API key from: https://urlscan.io/user/signup
|
| 3 |
+
URLSCAN_API_KEY=your_api_key_here
|
| 4 |
+
|
| 5 |
+
# Hopsworks Configuration
|
| 6 |
+
HOPSWORKS_API_KEY=your_hopsworks_api_key
|
| 7 |
+
HOPSWORKS_PROJECT=your_project_name
|
docs/urlscan_api.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# URLScan.io API Integration
|
| 2 |
+
|
| 3 |
+
This module provides a Python client for interacting with the URLScan.io API to analyze URLs for phishing detection.
|
| 4 |
+
|
| 5 |
+
## Setup
|
| 6 |
+
|
| 7 |
+
1. **Get an API key:**
|
| 8 |
+
- Sign up at https://urlscan.io/user/signup
|
| 9 |
+
- Get your API key from your account settings
|
| 10 |
+
|
| 11 |
+
2. **Set environment variable:**
|
| 12 |
+
```bash
|
| 13 |
+
export URLSCAN_API_KEY="your_api_key_here"
|
| 14 |
+
```
|
| 15 |
+
|
| 16 |
+
Or create a `.env` file:
|
| 17 |
+
```bash
|
| 18 |
+
cp .env.example .env
|
| 19 |
+
# Edit .env and add your API key
|
| 20 |
+
```
|
| 21 |
+
|
| 22 |
+
## Basic Usage
|
| 23 |
+
|
| 24 |
+
### Initialize Client
|
| 25 |
+
|
| 26 |
+
```python
|
| 27 |
+
from phising_detection.api import URLScanClient
|
| 28 |
+
|
| 29 |
+
# Using environment variable
|
| 30 |
+
client = URLScanClient()
|
| 31 |
+
|
| 32 |
+
# Or pass API key directly
|
| 33 |
+
client = URLScanClient(api_key="your_api_key")
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
### Submit a URL for Scanning
|
| 37 |
+
|
| 38 |
+
```python
|
| 39 |
+
# Submit URL
|
| 40 |
+
result = client.submit_url(
|
| 41 |
+
url="https://suspicious-site.com",
|
| 42 |
+
visibility="public", # or "unlisted" or "private"
|
| 43 |
+
tags=["phishing", "test"]
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
uuid = result["uuid"]
|
| 47 |
+
print(f"Scan UUID: {uuid}")
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
### Retrieve Results
|
| 51 |
+
|
| 52 |
+
```python
|
| 53 |
+
# Get results by UUID
|
| 54 |
+
scan_result = client.get_result(uuid)
|
| 55 |
+
|
| 56 |
+
# Access scan data
|
| 57 |
+
page_title = scan_result["page"]["title"]
|
| 58 |
+
screenshot = scan_result["task"]["screenshotURL"]
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
### Submit and Wait for Results
|
| 62 |
+
|
| 63 |
+
```python
|
| 64 |
+
# Submit and automatically wait for completion
|
| 65 |
+
result = client.submit_and_wait(
|
| 66 |
+
url="https://example.com",
|
| 67 |
+
max_wait=60, # seconds
|
| 68 |
+
poll_interval=5 # seconds between checks
|
| 69 |
+
)
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
### Get Verdict
|
| 73 |
+
|
| 74 |
+
```python
|
| 75 |
+
# Get simple verdict (malicious/safe)
|
| 76 |
+
verdict = client.get_verdict(uuid)
|
| 77 |
+
print(f"Verdict: {verdict}") # "malicious" or "safe"
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
### Search Existing Scans
|
| 81 |
+
|
| 82 |
+
```python
|
| 83 |
+
# Search for scans by domain
|
| 84 |
+
results = client.search(
|
| 85 |
+
query="domain:example.com",
|
| 86 |
+
size=10
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
for scan in results["results"]:
|
| 90 |
+
print(scan["task"]["url"])
|
| 91 |
+
```
|
| 92 |
+
|
| 93 |
+
## API Response Examples
|
| 94 |
+
|
| 95 |
+
### Submission Response
|
| 96 |
+
|
| 97 |
+
```json
|
| 98 |
+
{
|
| 99 |
+
"uuid": "abc123...",
|
| 100 |
+
"result": "https://urlscan.io/result/abc123.../",
|
| 101 |
+
"api": "https://urlscan.io/api/v1/result/abc123.../"
|
| 102 |
+
}
|
| 103 |
+
```
|
| 104 |
+
|
| 105 |
+
### Result Response
|
| 106 |
+
|
| 107 |
+
```json
|
| 108 |
+
{
|
| 109 |
+
"page": {
|
| 110 |
+
"url": "https://example.com",
|
| 111 |
+
"title": "Example Domain",
|
| 112 |
+
"status": "200"
|
| 113 |
+
},
|
| 114 |
+
"verdicts": {
|
| 115 |
+
"overall": {
|
| 116 |
+
"score": 0,
|
| 117 |
+
"malicious": false
|
| 118 |
+
}
|
| 119 |
+
},
|
| 120 |
+
"task": {
|
| 121 |
+
"uuid": "abc123...",
|
| 122 |
+
"time": "2024-01-01T12:00:00.000Z",
|
| 123 |
+
"screenshotURL": "https://..."
|
| 124 |
+
}
|
| 125 |
+
}
|
| 126 |
+
```
|
| 127 |
+
|
| 128 |
+
## Error Handling
|
| 129 |
+
|
| 130 |
+
```python
|
| 131 |
+
from phising_detection.api import URLScanClient, URLScanError
|
| 132 |
+
|
| 133 |
+
try:
|
| 134 |
+
client = URLScanClient()
|
| 135 |
+
result = client.submit_url("https://example.com")
|
| 136 |
+
except URLScanError as e:
|
| 137 |
+
print(f"Error: {e}")
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
## Rate Limits
|
| 141 |
+
|
| 142 |
+
- Free tier: 50 submissions per day
|
| 143 |
+
- Paid tier: Higher limits available
|
| 144 |
+
- The client handles rate limit errors automatically
|
| 145 |
+
|
| 146 |
+
## Integration with Phishing Detection
|
| 147 |
+
|
| 148 |
+
```python
|
| 149 |
+
from phising_detection.api import URLScanClient
|
| 150 |
+
from phising_detection.data import load_phishing_urls
|
| 151 |
+
|
| 152 |
+
# Load your phishing URLs
|
| 153 |
+
df = load_phishing_urls()
|
| 154 |
+
|
| 155 |
+
# Analyze URLs
|
| 156 |
+
client = URLScanClient()
|
| 157 |
+
|
| 158 |
+
for idx, row in df.head(10).iterrows(): # Sample first 10
|
| 159 |
+
try:
|
| 160 |
+
result = client.submit_and_wait(row['url'])
|
| 161 |
+
verdict = client.get_verdict(result['task']['uuid'])
|
| 162 |
+
print(f"{row['url']}: {verdict}")
|
| 163 |
+
except URLScanError as e:
|
| 164 |
+
print(f"Error scanning {row['url']}: {e}")
|
| 165 |
+
```
|
| 166 |
+
|
| 167 |
+
## API Documentation
|
| 168 |
+
|
| 169 |
+
Full API documentation: https://urlscan.io/docs/api/
|
examples/urlscan_example.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Example usage of URLScan.io API client."""
|
| 2 |
+
|
| 3 |
+
import sys
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
# Add src directory to Python path
|
| 7 |
+
project_root = Path(__file__).parent.parent
|
| 8 |
+
sys.path.insert(0, str(project_root / "src"))
|
| 9 |
+
|
| 10 |
+
from phising_detection.api import URLScanClient, URLScanError
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def main():
|
| 14 |
+
"""Demonstrate URLScan.io API usage."""
|
| 15 |
+
|
| 16 |
+
try:
|
| 17 |
+
# Initialize client (reads API key from URLSCAN_API_KEY env var)
|
| 18 |
+
client = URLScanClient()
|
| 19 |
+
|
| 20 |
+
# Example URL to scan
|
| 21 |
+
test_url = "http://000ogxd.wcomhost.com/logssss/customer_center/customer-idpp00c196/myaccount/signin"
|
| 22 |
+
|
| 23 |
+
print(f"Submitting URL for scanning: {test_url}")
|
| 24 |
+
|
| 25 |
+
# Option 1: Submit and get UUID for later retrieval
|
| 26 |
+
"""submission = client.submit_url(
|
| 27 |
+
url=test_url,
|
| 28 |
+
visibility="public",
|
| 29 |
+
tags=["example", "test"]
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
uuid = submission["uuid"]
|
| 33 |
+
result_url = submission["result"]
|
| 34 |
+
|
| 35 |
+
print(f"\n✓ Scan submitted successfully!")
|
| 36 |
+
print(f" UUID: {uuid}")
|
| 37 |
+
print(f" Results URL: {result_url}")
|
| 38 |
+
print(f"\nYou can retrieve results later using:")
|
| 39 |
+
print(f" client.get_result('{uuid}')")"""
|
| 40 |
+
|
| 41 |
+
# Option 2: Submit and wait for results (uncomment to use)
|
| 42 |
+
print("\n\nAlternatively, submit and wait for results:")
|
| 43 |
+
result = client.submit_and_wait(
|
| 44 |
+
url=test_url,
|
| 45 |
+
visibility="public",
|
| 46 |
+
max_wait=60,
|
| 47 |
+
poll_interval=5
|
| 48 |
+
)
|
| 49 |
+
print(f"Scan completed! Page title: {result.get('page', {}).get('title')}")
|
| 50 |
+
|
| 51 |
+
# Option 3: Search for existing scans
|
| 52 |
+
print("\n\nSearching for existing scans of this domain:")
|
| 53 |
+
search_results = client.search(query="domain:example.com", size=5)
|
| 54 |
+
|
| 55 |
+
total = search_results.get("total", 0)
|
| 56 |
+
print(f"Found {total} existing scans")
|
| 57 |
+
|
| 58 |
+
if search_results.get("results"):
|
| 59 |
+
print("\nRecent scans:")
|
| 60 |
+
for i, scan in enumerate(search_results["results"][:3], 1):
|
| 61 |
+
task = scan.get("task", {})
|
| 62 |
+
print(f" {i}. {task.get('url')} - {task.get('time')}")
|
| 63 |
+
|
| 64 |
+
except URLScanError as e:
|
| 65 |
+
print(f"Error: {e}")
|
| 66 |
+
print("\nMake sure you have set URLSCAN_API_KEY environment variable.")
|
| 67 |
+
print("Get your API key from: https://urlscan.io/user/signup")
|
| 68 |
+
sys.exit(1)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
if __name__ == "__main__":
|
| 72 |
+
main()
|
pyproject.toml
CHANGED
|
@@ -6,11 +6,15 @@ requires-python = ">=3.12"
|
|
| 6 |
dependencies = [
|
| 7 |
"pandas>=2.3.3",
|
| 8 |
"pandas-stubs==2.3.3.251219",
|
|
|
|
|
|
|
| 9 |
]
|
| 10 |
|
| 11 |
[dependency-groups]
|
| 12 |
dev = [
|
| 13 |
"pytest>=9.0.2",
|
| 14 |
"pytest-cov>=7.0.0",
|
|
|
|
|
|
|
| 15 |
]
|
| 16 |
|
|
|
|
| 6 |
dependencies = [
|
| 7 |
"pandas>=2.3.3",
|
| 8 |
"pandas-stubs==2.3.3.251219",
|
| 9 |
+
"python-dotenv>=1.0.0",
|
| 10 |
+
"requests>=2.32.5",
|
| 11 |
]
|
| 12 |
|
| 13 |
[dependency-groups]
|
| 14 |
dev = [
|
| 15 |
"pytest>=9.0.2",
|
| 16 |
"pytest-cov>=7.0.0",
|
| 17 |
+
"pytest-mock>=3.15.1",
|
| 18 |
+
"responses>=0.25.8",
|
| 19 |
]
|
| 20 |
|
src/phising_detection/api/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""External API integrations."""
|
| 2 |
+
|
| 3 |
+
from .urlscan import URLScanClient, URLScanError
|
| 4 |
+
|
| 5 |
+
__all__ = ["URLScanClient", "URLScanError"]
|
src/phising_detection/api/urlscan.py
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""URLScan.io API client for URL analysis."""
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import time
|
| 5 |
+
from typing import Dict, Any, Optional
|
| 6 |
+
|
| 7 |
+
from dotenv import load_dotenv
|
| 8 |
+
import requests
|
| 9 |
+
|
| 10 |
+
# Load environment variables from .env file
|
| 11 |
+
load_dotenv()
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class URLScanError(Exception):
|
| 15 |
+
"""Custom exception for URLScan API errors."""
|
| 16 |
+
pass
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class URLScanClient:
|
| 20 |
+
"""Client for interacting with URLScan.io API."""
|
| 21 |
+
|
| 22 |
+
BASE_URL = "https://urlscan.io/api/v1"
|
| 23 |
+
|
| 24 |
+
def __init__(self, api_key: Optional[str] = None):
|
| 25 |
+
"""
|
| 26 |
+
Initialize URLScan client.
|
| 27 |
+
|
| 28 |
+
Args:
|
| 29 |
+
api_key: URLScan.io API key. If not provided, will try to get from
|
| 30 |
+
URLSCAN_API_KEY environment variable.
|
| 31 |
+
|
| 32 |
+
Raises:
|
| 33 |
+
URLScanError: If no API key is provided or found in environment.
|
| 34 |
+
"""
|
| 35 |
+
self.api_key = api_key or os.getenv("URLSCAN_API_KEY")
|
| 36 |
+
if not self.api_key:
|
| 37 |
+
raise URLScanError(
|
| 38 |
+
"No API key provided. Set URLSCAN_API_KEY environment variable "
|
| 39 |
+
"or pass api_key parameter."
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
self.session = requests.Session()
|
| 43 |
+
self.session.headers.update({
|
| 44 |
+
"API-Key": self.api_key,
|
| 45 |
+
"Content-Type": "application/json"
|
| 46 |
+
})
|
| 47 |
+
|
| 48 |
+
def submit_url(
|
| 49 |
+
self,
|
| 50 |
+
url: str,
|
| 51 |
+
visibility: str = "public",
|
| 52 |
+
tags: Optional[list] = None
|
| 53 |
+
) -> Dict[str, Any]:
|
| 54 |
+
"""
|
| 55 |
+
Submit a URL for scanning.
|
| 56 |
+
|
| 57 |
+
Args:
|
| 58 |
+
url: The URL to scan
|
| 59 |
+
visibility: Visibility of the scan ('public', 'unlisted', or 'private')
|
| 60 |
+
tags: Optional list of tags for categorization
|
| 61 |
+
|
| 62 |
+
Returns:
|
| 63 |
+
Dictionary containing scan submission response with 'uuid' and 'api' fields
|
| 64 |
+
|
| 65 |
+
Raises:
|
| 66 |
+
URLScanError: If submission fails
|
| 67 |
+
"""
|
| 68 |
+
endpoint = f"{self.BASE_URL}/scan/"
|
| 69 |
+
|
| 70 |
+
payload = {
|
| 71 |
+
"url": url,
|
| 72 |
+
"visibility": visibility
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
if tags:
|
| 76 |
+
payload["tags"] = tags
|
| 77 |
+
|
| 78 |
+
try:
|
| 79 |
+
response = self.session.post(endpoint, json=payload)
|
| 80 |
+
response.raise_for_status()
|
| 81 |
+
return response.json()
|
| 82 |
+
|
| 83 |
+
except requests.exceptions.HTTPError as e:
|
| 84 |
+
if response.status_code == 429:
|
| 85 |
+
raise URLScanError("Rate limit exceeded. Please wait before retrying.")
|
| 86 |
+
elif response.status_code == 400:
|
| 87 |
+
raise URLScanError(f"Bad request: {response.text}")
|
| 88 |
+
else:
|
| 89 |
+
raise URLScanError(f"HTTP error occurred: {e}")
|
| 90 |
+
except requests.exceptions.RequestException as e:
|
| 91 |
+
raise URLScanError(f"Request failed: {e}")
|
| 92 |
+
|
| 93 |
+
def get_result(self, uuid: str) -> Dict[str, Any]:
|
| 94 |
+
"""
|
| 95 |
+
Get scan results by UUID.
|
| 96 |
+
|
| 97 |
+
Args:
|
| 98 |
+
uuid: The scan UUID returned from submit_url
|
| 99 |
+
|
| 100 |
+
Returns:
|
| 101 |
+
Dictionary containing scan results
|
| 102 |
+
|
| 103 |
+
Raises:
|
| 104 |
+
URLScanError: If retrieval fails
|
| 105 |
+
"""
|
| 106 |
+
endpoint = f"{self.BASE_URL}/result/{uuid}/"
|
| 107 |
+
|
| 108 |
+
try:
|
| 109 |
+
response = self.session.get(endpoint)
|
| 110 |
+
response.raise_for_status()
|
| 111 |
+
return response.json()
|
| 112 |
+
|
| 113 |
+
except requests.exceptions.HTTPError as e:
|
| 114 |
+
if response.status_code == 404:
|
| 115 |
+
raise URLScanError(
|
| 116 |
+
f"Scan not found or not ready yet. UUID: {uuid}"
|
| 117 |
+
)
|
| 118 |
+
else:
|
| 119 |
+
raise URLScanError(f"HTTP error occurred: {e}")
|
| 120 |
+
except requests.exceptions.RequestException as e:
|
| 121 |
+
raise URLScanError(f"Request failed: {e}")
|
| 122 |
+
|
| 123 |
+
def submit_and_wait(
|
| 124 |
+
self,
|
| 125 |
+
url: str,
|
| 126 |
+
visibility: str = "public",
|
| 127 |
+
tags: Optional[list] = None,
|
| 128 |
+
max_wait: int = 60,
|
| 129 |
+
poll_interval: int = 5
|
| 130 |
+
) -> Dict[str, Any]:
|
| 131 |
+
"""
|
| 132 |
+
Submit a URL and wait for results.
|
| 133 |
+
|
| 134 |
+
Args:
|
| 135 |
+
url: The URL to scan
|
| 136 |
+
visibility: Visibility of the scan
|
| 137 |
+
tags: Optional list of tags
|
| 138 |
+
max_wait: Maximum time to wait for results (seconds)
|
| 139 |
+
poll_interval: Time between polling attempts (seconds)
|
| 140 |
+
|
| 141 |
+
Returns:
|
| 142 |
+
Dictionary containing scan results
|
| 143 |
+
|
| 144 |
+
Raises:
|
| 145 |
+
URLScanError: If submission or retrieval fails, or timeout occurs
|
| 146 |
+
"""
|
| 147 |
+
# Submit URL
|
| 148 |
+
submission = self.submit_url(url, visibility, tags)
|
| 149 |
+
uuid = submission.get("uuid")
|
| 150 |
+
|
| 151 |
+
if not uuid:
|
| 152 |
+
raise URLScanError("No UUID returned from submission")
|
| 153 |
+
|
| 154 |
+
# Wait for results
|
| 155 |
+
elapsed = 0
|
| 156 |
+
while elapsed < max_wait:
|
| 157 |
+
try:
|
| 158 |
+
time.sleep(poll_interval)
|
| 159 |
+
elapsed += poll_interval
|
| 160 |
+
|
| 161 |
+
result = self.get_result(uuid)
|
| 162 |
+
return result
|
| 163 |
+
|
| 164 |
+
except URLScanError as e:
|
| 165 |
+
if "not found or not ready" in str(e):
|
| 166 |
+
# Scan not ready yet, continue waiting
|
| 167 |
+
continue
|
| 168 |
+
else:
|
| 169 |
+
# Other error, raise it
|
| 170 |
+
raise
|
| 171 |
+
|
| 172 |
+
raise URLScanError(
|
| 173 |
+
f"Timeout waiting for scan results. UUID: {uuid}. "
|
| 174 |
+
f"You can retrieve results later using get_result('{uuid}')"
|
| 175 |
+
)
|
| 176 |
+
|
| 177 |
+
def search(self, query: str, size: int = 100) -> Dict[str, Any]:
|
| 178 |
+
"""
|
| 179 |
+
Search URLScan.io database.
|
| 180 |
+
|
| 181 |
+
Args:
|
| 182 |
+
query: Search query (e.g., 'domain:example.com')
|
| 183 |
+
size: Number of results to return (max 10000)
|
| 184 |
+
|
| 185 |
+
Returns:
|
| 186 |
+
Dictionary containing search results
|
| 187 |
+
|
| 188 |
+
Raises:
|
| 189 |
+
URLScanError: If search fails
|
| 190 |
+
"""
|
| 191 |
+
endpoint = f"{self.BASE_URL}/search/"
|
| 192 |
+
|
| 193 |
+
params = {
|
| 194 |
+
"q": query,
|
| 195 |
+
"size": min(size, 10000)
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
try:
|
| 199 |
+
response = self.session.get(endpoint, params=params)
|
| 200 |
+
response.raise_for_status()
|
| 201 |
+
return response.json()
|
| 202 |
+
|
| 203 |
+
except requests.exceptions.HTTPError as e:
|
| 204 |
+
raise URLScanError(f"HTTP error occurred: {e}")
|
| 205 |
+
except requests.exceptions.RequestException as e:
|
| 206 |
+
raise URLScanError(f"Request failed: {e}")
|
| 207 |
+
|
| 208 |
+
def get_verdict(self, uuid: str) -> Optional[str]:
|
| 209 |
+
"""
|
| 210 |
+
Get the verdict (malicious/safe) for a scan.
|
| 211 |
+
|
| 212 |
+
Args:
|
| 213 |
+
uuid: The scan UUID
|
| 214 |
+
|
| 215 |
+
Returns:
|
| 216 |
+
Verdict string ('malicious', 'safe', or None if not available)
|
| 217 |
+
|
| 218 |
+
Raises:
|
| 219 |
+
URLScanError: If retrieval fails
|
| 220 |
+
"""
|
| 221 |
+
result = self.get_result(uuid)
|
| 222 |
+
|
| 223 |
+
# Extract verdict from results
|
| 224 |
+
verdicts = result.get("verdicts", {})
|
| 225 |
+
overall = verdicts.get("overall", {})
|
| 226 |
+
|
| 227 |
+
if overall.get("malicious", False):
|
| 228 |
+
return "malicious"
|
| 229 |
+
elif overall.get("score", 0) == 0:
|
| 230 |
+
return "safe"
|
| 231 |
+
else:
|
| 232 |
+
return None
|
src/phising_detection/data/load_data.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
|
| 3 |
import pandas as pd
|
| 4 |
from pathlib import Path
|
| 5 |
-
from typing import
|
| 6 |
|
| 7 |
|
| 8 |
def load_phishing_urls(
|
|
@@ -40,8 +40,6 @@ def load_phishing_urls(
|
|
| 40 |
'is_phishing': int(is_phishing)
|
| 41 |
})
|
| 42 |
|
| 43 |
-
|
| 44 |
-
|
| 45 |
return df
|
| 46 |
|
| 47 |
|
|
|
|
| 2 |
|
| 3 |
import pandas as pd
|
| 4 |
from pathlib import Path
|
| 5 |
+
from typing import Union
|
| 6 |
|
| 7 |
|
| 8 |
def load_phishing_urls(
|
|
|
|
| 40 |
'is_phishing': int(is_phishing)
|
| 41 |
})
|
| 42 |
|
|
|
|
|
|
|
| 43 |
return df
|
| 44 |
|
| 45 |
|
tests/test_urlscan.py
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for URLScan.io API client."""
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
import responses
|
| 5 |
+
from unittest.mock import patch
|
| 6 |
+
import sys
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
| 10 |
+
|
| 11 |
+
from phising_detection.api import URLScanClient, URLScanError
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class TestURLScanClient:
|
| 15 |
+
"""Tests for URLScanClient class."""
|
| 16 |
+
|
| 17 |
+
def test_init_with_api_key(self):
|
| 18 |
+
"""Test initialization with API key provided."""
|
| 19 |
+
client = URLScanClient(api_key="test_key")
|
| 20 |
+
assert client.api_key == "test_key"
|
| 21 |
+
|
| 22 |
+
def test_init_with_env_var(self, monkeypatch):
|
| 23 |
+
"""Test initialization with API key from environment."""
|
| 24 |
+
monkeypatch.setenv("URLSCAN_API_KEY", "env_key")
|
| 25 |
+
client = URLScanClient()
|
| 26 |
+
assert client.api_key == "env_key"
|
| 27 |
+
|
| 28 |
+
def test_init_without_api_key(self, monkeypatch):
|
| 29 |
+
"""Test initialization fails without API key."""
|
| 30 |
+
monkeypatch.delenv("URLSCAN_API_KEY", raising=False)
|
| 31 |
+
with pytest.raises(URLScanError) as exc_info:
|
| 32 |
+
URLScanClient()
|
| 33 |
+
assert "No API key provided" in str(exc_info.value)
|
| 34 |
+
|
| 35 |
+
@responses.activate
|
| 36 |
+
def test_submit_url_success(self):
|
| 37 |
+
"""Test successful URL submission."""
|
| 38 |
+
# Mock API response
|
| 39 |
+
responses.add(
|
| 40 |
+
responses.POST,
|
| 41 |
+
"https://urlscan.io/api/v1/scan/",
|
| 42 |
+
json={
|
| 43 |
+
"uuid": "test-uuid-123",
|
| 44 |
+
"result": "https://urlscan.io/result/test-uuid-123/",
|
| 45 |
+
"api": "https://urlscan.io/api/v1/result/test-uuid-123/"
|
| 46 |
+
},
|
| 47 |
+
status=200
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
client = URLScanClient(api_key="test_key")
|
| 51 |
+
result = client.submit_url("https://example.com")
|
| 52 |
+
|
| 53 |
+
assert result["uuid"] == "test-uuid-123"
|
| 54 |
+
assert "result" in result
|
| 55 |
+
assert len(responses.calls) == 1
|
| 56 |
+
assert responses.calls[0].request.url == "https://urlscan.io/api/v1/scan/"
|
| 57 |
+
|
| 58 |
+
@responses.activate
|
| 59 |
+
def test_submit_url_with_tags(self):
|
| 60 |
+
"""Test URL submission with tags."""
|
| 61 |
+
responses.add(
|
| 62 |
+
responses.POST,
|
| 63 |
+
"https://urlscan.io/api/v1/scan/",
|
| 64 |
+
json={"uuid": "test-uuid", "result": "url"},
|
| 65 |
+
status=200
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
client = URLScanClient(api_key="test_key")
|
| 69 |
+
client.submit_url(
|
| 70 |
+
"https://example.com",
|
| 71 |
+
visibility="unlisted",
|
| 72 |
+
tags=["phishing", "test"]
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
request_body = responses.calls[0].request.body
|
| 76 |
+
assert b"phishing" in request_body
|
| 77 |
+
assert b"test" in request_body
|
| 78 |
+
|
| 79 |
+
@responses.activate
|
| 80 |
+
def test_submit_url_rate_limit(self):
|
| 81 |
+
"""Test rate limit error handling."""
|
| 82 |
+
responses.add(
|
| 83 |
+
responses.POST,
|
| 84 |
+
"https://urlscan.io/api/v1/scan/",
|
| 85 |
+
status=429
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
client = URLScanClient(api_key="test_key")
|
| 89 |
+
with pytest.raises(URLScanError) as exc_info:
|
| 90 |
+
client.submit_url("https://example.com")
|
| 91 |
+
assert "Rate limit exceeded" in str(exc_info.value)
|
| 92 |
+
|
| 93 |
+
@responses.activate
|
| 94 |
+
def test_submit_url_bad_request(self):
|
| 95 |
+
"""Test bad request error handling."""
|
| 96 |
+
responses.add(
|
| 97 |
+
responses.POST,
|
| 98 |
+
"https://urlscan.io/api/v1/scan/",
|
| 99 |
+
body="Invalid URL",
|
| 100 |
+
status=400
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
client = URLScanClient(api_key="test_key")
|
| 104 |
+
with pytest.raises(URLScanError) as exc_info:
|
| 105 |
+
client.submit_url("not-a-valid-url")
|
| 106 |
+
assert "Bad request" in str(exc_info.value)
|
| 107 |
+
|
| 108 |
+
@responses.activate
|
| 109 |
+
def test_get_result_success(self):
|
| 110 |
+
"""Test successful result retrieval."""
|
| 111 |
+
responses.add(
|
| 112 |
+
responses.GET,
|
| 113 |
+
"https://urlscan.io/api/v1/result/test-uuid/",
|
| 114 |
+
json={
|
| 115 |
+
"page": {
|
| 116 |
+
"url": "https://example.com",
|
| 117 |
+
"title": "Example Domain"
|
| 118 |
+
},
|
| 119 |
+
"verdicts": {
|
| 120 |
+
"overall": {
|
| 121 |
+
"score": 0,
|
| 122 |
+
"malicious": False
|
| 123 |
+
}
|
| 124 |
+
},
|
| 125 |
+
"task": {"uuid": "test-uuid"}
|
| 126 |
+
},
|
| 127 |
+
status=200
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
client = URLScanClient(api_key="test_key")
|
| 131 |
+
result = client.get_result("test-uuid")
|
| 132 |
+
|
| 133 |
+
assert result["page"]["url"] == "https://example.com"
|
| 134 |
+
assert result["verdicts"]["overall"]["malicious"] is False
|
| 135 |
+
|
| 136 |
+
@responses.activate
|
| 137 |
+
def test_get_result_not_found(self):
|
| 138 |
+
"""Test result not found error."""
|
| 139 |
+
responses.add(
|
| 140 |
+
responses.GET,
|
| 141 |
+
"https://urlscan.io/api/v1/result/test-uuid/",
|
| 142 |
+
status=404
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
client = URLScanClient(api_key="test_key")
|
| 146 |
+
with pytest.raises(URLScanError) as exc_info:
|
| 147 |
+
client.get_result("test-uuid")
|
| 148 |
+
assert "not found or not ready" in str(exc_info.value)
|
| 149 |
+
|
| 150 |
+
@responses.activate
|
| 151 |
+
def test_search_success(self):
|
| 152 |
+
"""Test search functionality."""
|
| 153 |
+
responses.add(
|
| 154 |
+
responses.GET,
|
| 155 |
+
"https://urlscan.io/api/v1/search/",
|
| 156 |
+
json={
|
| 157 |
+
"total": 2,
|
| 158 |
+
"results": [
|
| 159 |
+
{"task": {"url": "https://example.com"}},
|
| 160 |
+
{"task": {"url": "https://example.org"}}
|
| 161 |
+
]
|
| 162 |
+
},
|
| 163 |
+
status=200
|
| 164 |
+
)
|
| 165 |
+
|
| 166 |
+
client = URLScanClient(api_key="test_key")
|
| 167 |
+
results = client.search(query="domain:example.com", size=10)
|
| 168 |
+
|
| 169 |
+
assert results["total"] == 2
|
| 170 |
+
assert len(results["results"]) == 2
|
| 171 |
+
|
| 172 |
+
@responses.activate
|
| 173 |
+
def test_get_verdict_malicious(self):
|
| 174 |
+
"""Test verdict extraction for malicious URL."""
|
| 175 |
+
responses.add(
|
| 176 |
+
responses.GET,
|
| 177 |
+
"https://urlscan.io/api/v1/result/test-uuid/",
|
| 178 |
+
json={
|
| 179 |
+
"verdicts": {
|
| 180 |
+
"overall": {
|
| 181 |
+
"score": 100,
|
| 182 |
+
"malicious": True
|
| 183 |
+
}
|
| 184 |
+
}
|
| 185 |
+
},
|
| 186 |
+
status=200
|
| 187 |
+
)
|
| 188 |
+
|
| 189 |
+
client = URLScanClient(api_key="test_key")
|
| 190 |
+
verdict = client.get_verdict("test-uuid")
|
| 191 |
+
|
| 192 |
+
assert verdict == "malicious"
|
| 193 |
+
|
| 194 |
+
@responses.activate
|
| 195 |
+
def test_get_verdict_safe(self):
|
| 196 |
+
"""Test verdict extraction for safe URL."""
|
| 197 |
+
responses.add(
|
| 198 |
+
responses.GET,
|
| 199 |
+
"https://urlscan.io/api/v1/result/test-uuid/",
|
| 200 |
+
json={
|
| 201 |
+
"verdicts": {
|
| 202 |
+
"overall": {
|
| 203 |
+
"score": 0,
|
| 204 |
+
"malicious": False
|
| 205 |
+
}
|
| 206 |
+
}
|
| 207 |
+
},
|
| 208 |
+
status=200
|
| 209 |
+
)
|
| 210 |
+
|
| 211 |
+
client = URLScanClient(api_key="test_key")
|
| 212 |
+
verdict = client.get_verdict("test-uuid")
|
| 213 |
+
|
| 214 |
+
assert verdict == "safe"
|
| 215 |
+
|
| 216 |
+
@responses.activate
|
| 217 |
+
def test_submit_and_wait_success(self):
|
| 218 |
+
"""Test submit and wait for results."""
|
| 219 |
+
# Mock submission
|
| 220 |
+
responses.add(
|
| 221 |
+
responses.POST,
|
| 222 |
+
"https://urlscan.io/api/v1/scan/",
|
| 223 |
+
json={
|
| 224 |
+
"uuid": "test-uuid",
|
| 225 |
+
"result": "https://urlscan.io/result/test-uuid/"
|
| 226 |
+
},
|
| 227 |
+
status=200
|
| 228 |
+
)
|
| 229 |
+
|
| 230 |
+
# Mock result retrieval
|
| 231 |
+
responses.add(
|
| 232 |
+
responses.GET,
|
| 233 |
+
"https://urlscan.io/api/v1/result/test-uuid/",
|
| 234 |
+
json={
|
| 235 |
+
"page": {"title": "Example"},
|
| 236 |
+
"task": {"uuid": "test-uuid"}
|
| 237 |
+
},
|
| 238 |
+
status=200
|
| 239 |
+
)
|
| 240 |
+
|
| 241 |
+
client = URLScanClient(api_key="test_key")
|
| 242 |
+
result = client.submit_and_wait(
|
| 243 |
+
"https://example.com",
|
| 244 |
+
max_wait=10,
|
| 245 |
+
poll_interval=1
|
| 246 |
+
)
|
| 247 |
+
|
| 248 |
+
assert result["page"]["title"] == "Example"
|
| 249 |
+
|
| 250 |
+
@responses.activate
|
| 251 |
+
def test_submit_and_wait_timeout(self):
|
| 252 |
+
"""Test timeout in submit_and_wait."""
|
| 253 |
+
# Mock submission
|
| 254 |
+
responses.add(
|
| 255 |
+
responses.POST,
|
| 256 |
+
"https://urlscan.io/api/v1/scan/",
|
| 257 |
+
json={"uuid": "test-uuid"},
|
| 258 |
+
status=200
|
| 259 |
+
)
|
| 260 |
+
|
| 261 |
+
# Mock result always returning 404
|
| 262 |
+
responses.add(
|
| 263 |
+
responses.GET,
|
| 264 |
+
"https://urlscan.io/api/v1/result/test-uuid/",
|
| 265 |
+
status=404
|
| 266 |
+
)
|
| 267 |
+
|
| 268 |
+
client = URLScanClient(api_key="test_key")
|
| 269 |
+
with pytest.raises(URLScanError) as exc_info:
|
| 270 |
+
client.submit_and_wait(
|
| 271 |
+
"https://example.com",
|
| 272 |
+
max_wait=3,
|
| 273 |
+
poll_interval=1
|
| 274 |
+
)
|
| 275 |
+
assert "Timeout" in str(exc_info.value)
|
uv.lock
CHANGED
|
@@ -2,6 +2,72 @@ version = 1
|
|
| 2 |
revision = 3
|
| 3 |
requires-python = ">=3.12"
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
[[package]]
|
| 6 |
name = "colorama"
|
| 7 |
version = "0.4.6"
|
|
@@ -85,6 +151,15 @@ wheels = [
|
|
| 85 |
{ url = "https://files.pythonhosted.org/packages/8d/4c/1968f32fb9a2604645827e11ff84a31e59d532e01995f904723b4f5328b3/coverage-7.13.0-py3-none-any.whl", hash = "sha256:850d2998f380b1e266459ca5b47bc9e7daf9af1d070f66317972f382d46f1904", size = 210068, upload-time = "2025-12-08T13:14:36.236Z" },
|
| 86 |
]
|
| 87 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
[[package]]
|
| 89 |
name = "iniconfig"
|
| 90 |
version = "2.3.0"
|
|
@@ -233,24 +308,32 @@ source = { virtual = "." }
|
|
| 233 |
dependencies = [
|
| 234 |
{ name = "pandas" },
|
| 235 |
{ name = "pandas-stubs" },
|
|
|
|
|
|
|
| 236 |
]
|
| 237 |
|
| 238 |
[package.dev-dependencies]
|
| 239 |
dev = [
|
| 240 |
{ name = "pytest" },
|
| 241 |
{ name = "pytest-cov" },
|
|
|
|
|
|
|
| 242 |
]
|
| 243 |
|
| 244 |
[package.metadata]
|
| 245 |
requires-dist = [
|
| 246 |
{ name = "pandas", specifier = ">=2.3.3" },
|
| 247 |
{ name = "pandas-stubs", specifier = "==2.3.3.251219" },
|
|
|
|
|
|
|
| 248 |
]
|
| 249 |
|
| 250 |
[package.metadata.requires-dev]
|
| 251 |
dev = [
|
| 252 |
{ name = "pytest", specifier = ">=9.0.2" },
|
| 253 |
{ name = "pytest-cov", specifier = ">=7.0.0" },
|
|
|
|
|
|
|
| 254 |
]
|
| 255 |
|
| 256 |
[[package]]
|
|
@@ -301,6 +384,18 @@ wheels = [
|
|
| 301 |
{ url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" },
|
| 302 |
]
|
| 303 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 304 |
[[package]]
|
| 305 |
name = "python-dateutil"
|
| 306 |
version = "2.9.0.post0"
|
|
@@ -313,6 +408,15 @@ wheels = [
|
|
| 313 |
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
|
| 314 |
]
|
| 315 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
[[package]]
|
| 317 |
name = "pytz"
|
| 318 |
version = "2025.2"
|
|
@@ -322,6 +426,81 @@ wheels = [
|
|
| 322 |
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
|
| 323 |
]
|
| 324 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 325 |
[[package]]
|
| 326 |
name = "six"
|
| 327 |
version = "1.17.0"
|
|
@@ -348,3 +527,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf3
|
|
| 348 |
wheels = [
|
| 349 |
{ url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" },
|
| 350 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
revision = 3
|
| 3 |
requires-python = ">=3.12"
|
| 4 |
|
| 5 |
+
[[package]]
|
| 6 |
+
name = "certifi"
|
| 7 |
+
version = "2025.11.12"
|
| 8 |
+
source = { registry = "https://pypi.org/simple" }
|
| 9 |
+
sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" }
|
| 10 |
+
wheels = [
|
| 11 |
+
{ url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" },
|
| 12 |
+
]
|
| 13 |
+
|
| 14 |
+
[[package]]
|
| 15 |
+
name = "charset-normalizer"
|
| 16 |
+
version = "3.4.4"
|
| 17 |
+
source = { registry = "https://pypi.org/simple" }
|
| 18 |
+
sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
|
| 19 |
+
wheels = [
|
| 20 |
+
{ url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" },
|
| 21 |
+
{ url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" },
|
| 22 |
+
{ url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" },
|
| 23 |
+
{ url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" },
|
| 24 |
+
{ url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" },
|
| 25 |
+
{ url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" },
|
| 26 |
+
{ url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" },
|
| 27 |
+
{ url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" },
|
| 28 |
+
{ url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" },
|
| 29 |
+
{ url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" },
|
| 30 |
+
{ url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" },
|
| 31 |
+
{ url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" },
|
| 32 |
+
{ url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" },
|
| 33 |
+
{ url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" },
|
| 34 |
+
{ url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" },
|
| 35 |
+
{ url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" },
|
| 36 |
+
{ url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
|
| 37 |
+
{ url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
|
| 38 |
+
{ url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
|
| 39 |
+
{ url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
|
| 40 |
+
{ url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
|
| 41 |
+
{ url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
|
| 42 |
+
{ url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
|
| 43 |
+
{ url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
|
| 44 |
+
{ url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
|
| 45 |
+
{ url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
|
| 46 |
+
{ url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
|
| 47 |
+
{ url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
|
| 48 |
+
{ url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
|
| 49 |
+
{ url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
|
| 50 |
+
{ url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
|
| 51 |
+
{ url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
|
| 52 |
+
{ url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
|
| 53 |
+
{ url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
|
| 54 |
+
{ url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
|
| 55 |
+
{ url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
|
| 56 |
+
{ url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
|
| 57 |
+
{ url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
|
| 58 |
+
{ url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
|
| 59 |
+
{ url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
|
| 60 |
+
{ url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
|
| 61 |
+
{ url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
|
| 62 |
+
{ url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
|
| 63 |
+
{ url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
|
| 64 |
+
{ url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
|
| 65 |
+
{ url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
|
| 66 |
+
{ url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
|
| 67 |
+
{ url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
|
| 68 |
+
{ url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
|
| 69 |
+
]
|
| 70 |
+
|
| 71 |
[[package]]
|
| 72 |
name = "colorama"
|
| 73 |
version = "0.4.6"
|
|
|
|
| 151 |
{ url = "https://files.pythonhosted.org/packages/8d/4c/1968f32fb9a2604645827e11ff84a31e59d532e01995f904723b4f5328b3/coverage-7.13.0-py3-none-any.whl", hash = "sha256:850d2998f380b1e266459ca5b47bc9e7daf9af1d070f66317972f382d46f1904", size = 210068, upload-time = "2025-12-08T13:14:36.236Z" },
|
| 152 |
]
|
| 153 |
|
| 154 |
+
[[package]]
|
| 155 |
+
name = "idna"
|
| 156 |
+
version = "3.11"
|
| 157 |
+
source = { registry = "https://pypi.org/simple" }
|
| 158 |
+
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
|
| 159 |
+
wheels = [
|
| 160 |
+
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
| 161 |
+
]
|
| 162 |
+
|
| 163 |
[[package]]
|
| 164 |
name = "iniconfig"
|
| 165 |
version = "2.3.0"
|
|
|
|
| 308 |
dependencies = [
|
| 309 |
{ name = "pandas" },
|
| 310 |
{ name = "pandas-stubs" },
|
| 311 |
+
{ name = "python-dotenv" },
|
| 312 |
+
{ name = "requests" },
|
| 313 |
]
|
| 314 |
|
| 315 |
[package.dev-dependencies]
|
| 316 |
dev = [
|
| 317 |
{ name = "pytest" },
|
| 318 |
{ name = "pytest-cov" },
|
| 319 |
+
{ name = "pytest-mock" },
|
| 320 |
+
{ name = "responses" },
|
| 321 |
]
|
| 322 |
|
| 323 |
[package.metadata]
|
| 324 |
requires-dist = [
|
| 325 |
{ name = "pandas", specifier = ">=2.3.3" },
|
| 326 |
{ name = "pandas-stubs", specifier = "==2.3.3.251219" },
|
| 327 |
+
{ name = "python-dotenv", specifier = ">=1.0.0" },
|
| 328 |
+
{ name = "requests", specifier = ">=2.32.5" },
|
| 329 |
]
|
| 330 |
|
| 331 |
[package.metadata.requires-dev]
|
| 332 |
dev = [
|
| 333 |
{ name = "pytest", specifier = ">=9.0.2" },
|
| 334 |
{ name = "pytest-cov", specifier = ">=7.0.0" },
|
| 335 |
+
{ name = "pytest-mock", specifier = ">=3.15.1" },
|
| 336 |
+
{ name = "responses", specifier = ">=0.25.8" },
|
| 337 |
]
|
| 338 |
|
| 339 |
[[package]]
|
|
|
|
| 384 |
{ url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" },
|
| 385 |
]
|
| 386 |
|
| 387 |
+
[[package]]
|
| 388 |
+
name = "pytest-mock"
|
| 389 |
+
version = "3.15.1"
|
| 390 |
+
source = { registry = "https://pypi.org/simple" }
|
| 391 |
+
dependencies = [
|
| 392 |
+
{ name = "pytest" },
|
| 393 |
+
]
|
| 394 |
+
sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" }
|
| 395 |
+
wheels = [
|
| 396 |
+
{ url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" },
|
| 397 |
+
]
|
| 398 |
+
|
| 399 |
[[package]]
|
| 400 |
name = "python-dateutil"
|
| 401 |
version = "2.9.0.post0"
|
|
|
|
| 408 |
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
|
| 409 |
]
|
| 410 |
|
| 411 |
+
[[package]]
|
| 412 |
+
name = "python-dotenv"
|
| 413 |
+
version = "1.2.1"
|
| 414 |
+
source = { registry = "https://pypi.org/simple" }
|
| 415 |
+
sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" }
|
| 416 |
+
wheels = [
|
| 417 |
+
{ url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" },
|
| 418 |
+
]
|
| 419 |
+
|
| 420 |
[[package]]
|
| 421 |
name = "pytz"
|
| 422 |
version = "2025.2"
|
|
|
|
| 426 |
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
|
| 427 |
]
|
| 428 |
|
| 429 |
+
[[package]]
|
| 430 |
+
name = "pyyaml"
|
| 431 |
+
version = "6.0.3"
|
| 432 |
+
source = { registry = "https://pypi.org/simple" }
|
| 433 |
+
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
|
| 434 |
+
wheels = [
|
| 435 |
+
{ url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
|
| 436 |
+
{ url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
|
| 437 |
+
{ url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
|
| 438 |
+
{ url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
|
| 439 |
+
{ url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
|
| 440 |
+
{ url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
|
| 441 |
+
{ url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
|
| 442 |
+
{ url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
|
| 443 |
+
{ url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
|
| 444 |
+
{ url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
|
| 445 |
+
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
|
| 446 |
+
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
|
| 447 |
+
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
|
| 448 |
+
{ url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
|
| 449 |
+
{ url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
|
| 450 |
+
{ url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
|
| 451 |
+
{ url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
|
| 452 |
+
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
|
| 453 |
+
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
|
| 454 |
+
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
|
| 455 |
+
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
|
| 456 |
+
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
|
| 457 |
+
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
|
| 458 |
+
{ url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
|
| 459 |
+
{ url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
|
| 460 |
+
{ url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
|
| 461 |
+
{ url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
|
| 462 |
+
{ url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
|
| 463 |
+
{ url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
|
| 464 |
+
{ url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
|
| 465 |
+
{ url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
|
| 466 |
+
{ url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
|
| 467 |
+
{ url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
|
| 468 |
+
{ url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
|
| 469 |
+
{ url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
|
| 470 |
+
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
|
| 471 |
+
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
|
| 472 |
+
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
| 473 |
+
]
|
| 474 |
+
|
| 475 |
+
[[package]]
|
| 476 |
+
name = "requests"
|
| 477 |
+
version = "2.32.5"
|
| 478 |
+
source = { registry = "https://pypi.org/simple" }
|
| 479 |
+
dependencies = [
|
| 480 |
+
{ name = "certifi" },
|
| 481 |
+
{ name = "charset-normalizer" },
|
| 482 |
+
{ name = "idna" },
|
| 483 |
+
{ name = "urllib3" },
|
| 484 |
+
]
|
| 485 |
+
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
|
| 486 |
+
wheels = [
|
| 487 |
+
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
|
| 488 |
+
]
|
| 489 |
+
|
| 490 |
+
[[package]]
|
| 491 |
+
name = "responses"
|
| 492 |
+
version = "0.25.8"
|
| 493 |
+
source = { registry = "https://pypi.org/simple" }
|
| 494 |
+
dependencies = [
|
| 495 |
+
{ name = "pyyaml" },
|
| 496 |
+
{ name = "requests" },
|
| 497 |
+
{ name = "urllib3" },
|
| 498 |
+
]
|
| 499 |
+
sdist = { url = "https://files.pythonhosted.org/packages/0e/95/89c054ad70bfef6da605338b009b2e283485835351a9935c7bfbfaca7ffc/responses-0.25.8.tar.gz", hash = "sha256:9374d047a575c8f781b94454db5cab590b6029505f488d12899ddb10a4af1cf4", size = 79320, upload-time = "2025-08-08T19:01:46.709Z" }
|
| 500 |
+
wheels = [
|
| 501 |
+
{ url = "https://files.pythonhosted.org/packages/1c/4c/cc276ce57e572c102d9542d383b2cfd551276581dc60004cb94fe8774c11/responses-0.25.8-py3-none-any.whl", hash = "sha256:0c710af92def29c8352ceadff0c3fe340ace27cf5af1bbe46fb71275bcd2831c", size = 34769, upload-time = "2025-08-08T19:01:45.018Z" },
|
| 502 |
+
]
|
| 503 |
+
|
| 504 |
[[package]]
|
| 505 |
name = "six"
|
| 506 |
version = "1.17.0"
|
|
|
|
| 527 |
wheels = [
|
| 528 |
{ url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" },
|
| 529 |
]
|
| 530 |
+
|
| 531 |
+
[[package]]
|
| 532 |
+
name = "urllib3"
|
| 533 |
+
version = "2.6.2"
|
| 534 |
+
source = { registry = "https://pypi.org/simple" }
|
| 535 |
+
sdist = { url = "https://files.pythonhosted.org/packages/1e/24/a2a2ed9addd907787d7aa0355ba36a6cadf1768b934c652ea78acbd59dcd/urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797", size = 432930, upload-time = "2025-12-11T15:56:40.252Z" }
|
| 536 |
+
wheels = [
|
| 537 |
+
{ url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182, upload-time = "2025-12-11T15:56:38.584Z" },
|
| 538 |
+
]
|