feat: implement EVE Forum Moderator CLI tool
- Add structured forum rules from EVE moderation policy - Implement cookie-based Discourse authentication - Add async Discourse API client with rate limiting - Implement Claude-powered rule violation analyzer - Add rich terminal and Discourse-compatible markdown reports - Create Click CLI with review and rules commands
This commit is contained in:
9
.env.example
Normal file
9
.env.example
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# LLM Proxy Configuration
|
||||||
|
# Get your API key from the llm-proxy admin
|
||||||
|
LLM_PROXY_KEY=your_api_key_here
|
||||||
|
|
||||||
|
# Optional: Override the default proxy URL
|
||||||
|
# LLM_PROXY_URL=https://llm-proxy.ext.ben.io/v1
|
||||||
|
|
||||||
|
# Optional: Override the default model
|
||||||
|
# LLM_MODEL=claude-sonnet-4-5-20250514
|
||||||
39
.gitignore
vendored
Normal file
39
.gitignore
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Environment and secrets
|
||||||
|
.env
|
||||||
|
cookies.env
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Reports (contain user data)
|
||||||
|
reports/*.md
|
||||||
161
README.md
161
README.md
@@ -1,3 +1,160 @@
|
|||||||
# eve-forum-moderator
|
# EVE Forum Moderator
|
||||||
|
|
||||||
AI-assisted EVE Online forum moderation tool - analyzes user posting history for rule violations
|
AI-assisted EVE Online forum moderation tool - analyzes user posting history for rule violations.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Fetches user post history from the EVE Online forums (Discourse)
|
||||||
|
- Analyzes posts using Claude AI for potential rule violations
|
||||||
|
- Generates Discourse-compatible markdown reports
|
||||||
|
- Supports configurable time ranges and output options
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Requires Python 3.11+ and [uv](https://docs.astral.sh/uv/).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone the repository
|
||||||
|
git clone https://gitea.ext.ben.io/b3nw/eve-forum-moderator.git
|
||||||
|
cd eve-forum-moderator
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
uv sync
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### 1. Cookie Setup
|
||||||
|
|
||||||
|
Export your browser cookies after logging into forums.eveonline.com:
|
||||||
|
|
||||||
|
1. Log into https://forums.eveonline.com with your moderator account
|
||||||
|
2. Open Developer Tools (F12) → Application → Cookies → forums.eveonline.com
|
||||||
|
3. Copy the values for `_forum_session` and `_t`
|
||||||
|
4. Create a `cookies.env` file:
|
||||||
|
|
||||||
|
```env
|
||||||
|
_forum_session=your_session_cookie_value
|
||||||
|
_t=your_auth_token_value
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, use a browser extension like "cookies.txt" to export in Netscape format.
|
||||||
|
|
||||||
|
### 2. LLM API Setup
|
||||||
|
|
||||||
|
Create a `.env` file with your LLM proxy API key:
|
||||||
|
|
||||||
|
```env
|
||||||
|
LLM_PROXY_KEY=your_api_key_here
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Review a User
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Basic usage - analyze last 30 days
|
||||||
|
uv run eve-mod review "SomePlayer"
|
||||||
|
|
||||||
|
# Specify time range
|
||||||
|
uv run eve-mod review "SomePlayer" --days 14
|
||||||
|
|
||||||
|
# Verbose output with detailed analysis
|
||||||
|
uv run eve-mod review "SomePlayer" --verbose
|
||||||
|
|
||||||
|
# Custom cookie file and output path
|
||||||
|
uv run eve-mod review "SomePlayer" --cookies ./my-cookies.env --output ./report.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### View Forum Rules
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run eve-mod rules
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `--days, -d` | Number of days to analyze (default: 30) |
|
||||||
|
| `--cookies, -c` | Path to cookies file (default: cookies.env) |
|
||||||
|
| `--output, -o` | Custom output path for markdown report |
|
||||||
|
| `--verbose, -v` | Show detailed violation analysis |
|
||||||
|
| `--max-posts` | Maximum posts to fetch (default: 200) |
|
||||||
|
| `--enrich/--no-enrich` | Fetch full post content (default: enabled) |
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
Reports are generated in two formats:
|
||||||
|
|
||||||
|
1. **Terminal** - Rich-formatted summary with color-coded severity
|
||||||
|
2. **Markdown** - Discourse-compatible file saved to `reports/` directory
|
||||||
|
|
||||||
|
### Sample Report Format
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## User Review: SomePlayer
|
||||||
|
**Period:** Last 30 days
|
||||||
|
**Posts Analyzed:** 47
|
||||||
|
**Posts with Violations:** 3
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
| Date | Topic | Rule | Severity | Confidence |
|
||||||
|
|------|-------|------|----------|------------|
|
||||||
|
| 2026-01-03 | [Thread Title](url) | 1.2: Flaming | [MED] | 85% |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Detailed Analysis
|
||||||
|
|
||||||
|
#### 1. [Thread Title](url)
|
||||||
|
**Date:** 2026-01-03 | **Category:** General Discussion
|
||||||
|
|
||||||
|
**[MED] Rule 1.2 - Flaming** (Confidence: 85%)
|
||||||
|
|
||||||
|
> "Relevant quote from the post"
|
||||||
|
|
||||||
|
*Explanation of why this violates the rule*
|
||||||
|
```
|
||||||
|
|
||||||
|
## Forum Rules
|
||||||
|
|
||||||
|
The tool checks against the official [EVE Forum Moderation Policy](https://support.eveonline.com/hc/en-us/articles/8563133115932-Forum-Moderation-Policy):
|
||||||
|
|
||||||
|
### Prohibited Conduct (1.x)
|
||||||
|
- Trolling, Flaming, Ranting, Personal Attacks
|
||||||
|
- Harassment, Doxxing
|
||||||
|
- Racism & Discrimination, Hate Speech, Sexism
|
||||||
|
- Spamming, Bumping, Off-Topic Posting
|
||||||
|
- New Player Bashing, Impersonation, Advertising
|
||||||
|
|
||||||
|
### Prohibited Content (2.x)
|
||||||
|
- Pornography, Profanity
|
||||||
|
- Real Money Trading (RMT)
|
||||||
|
- Discussion of Warnings/Bans/Moderation
|
||||||
|
- Private CCP Communications
|
||||||
|
- In-Game Bugs & Exploits
|
||||||
|
- Real World Religion/Politics
|
||||||
|
|
||||||
|
### Other Rules (3-9)
|
||||||
|
- Non-Constructive Posting
|
||||||
|
- Abuse of Forum Tools
|
||||||
|
- Re-Opening Locked Topics
|
||||||
|
- Attacking CCP/ISD Staff
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run with verbose logging
|
||||||
|
uv run eve-mod review "TestUser" -v
|
||||||
|
|
||||||
|
# Check installed version
|
||||||
|
uv run eve-mod --version
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Private - for authorized moderator use only.
|
||||||
|
|||||||
19
cookies.env.example
Normal file
19
cookies.env.example
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# EVE Forum Cookie Configuration
|
||||||
|
#
|
||||||
|
# Export cookies from your browser after logging into forums.eveonline.com
|
||||||
|
#
|
||||||
|
# Option 1: Simple key=value format
|
||||||
|
# Copy the cookie values from your browser's developer tools (F12 -> Application -> Cookies)
|
||||||
|
#
|
||||||
|
# Option 2: Netscape/curl format
|
||||||
|
# Use a browser extension like "cookies.txt" to export in Netscape format
|
||||||
|
#
|
||||||
|
# Required cookies:
|
||||||
|
# _forum_session - Your Discourse session token
|
||||||
|
# _t - Your authentication token
|
||||||
|
#
|
||||||
|
# Optional (may help with Cloudflare):
|
||||||
|
# _cf_clearance - Cloudflare clearance cookie
|
||||||
|
|
||||||
|
_forum_session=YOUR_SESSION_COOKIE_HERE
|
||||||
|
_t=YOUR_AUTH_TOKEN_HERE
|
||||||
24
pyproject.toml
Normal file
24
pyproject.toml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[project]
|
||||||
|
name = "eve-forum-moderator"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "AI-assisted EVE Online forum moderation tool - analyzes user posting history for rule violations"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
dependencies = [
|
||||||
|
"httpx>=0.27",
|
||||||
|
"click>=8.1",
|
||||||
|
"rich>=13.0",
|
||||||
|
"anthropic>=0.30",
|
||||||
|
"python-dateutil>=2.9",
|
||||||
|
"python-dotenv>=1.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
eve-mod = "eve_mod.cli:cli"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["src/eve_mod"]
|
||||||
3
reports/.gitignore
vendored
Normal file
3
reports/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Generated reports (user-specific, may contain PII)
|
||||||
|
*.md
|
||||||
|
!.gitkeep
|
||||||
126
rules/forum_policy.md
Normal file
126
rules/forum_policy.md
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
# EVE Online Forum Moderation Policy
|
||||||
|
|
||||||
|
Source: https://support.eveonline.com/hc/en-us/articles/8563133115932-Forum-Moderation-Policy
|
||||||
|
|
||||||
|
## 1. Prohibited Conduct
|
||||||
|
|
||||||
|
The following behaviors are not permitted on the EVE Online forums:
|
||||||
|
|
||||||
|
### 1.1 Trolling
|
||||||
|
Posting inflammatory, extraneous, or off-topic messages with the intent of provoking an emotional response or disrupting normal discussion.
|
||||||
|
|
||||||
|
### 1.2 Flaming
|
||||||
|
Hostile and insulting interaction between forum users. This includes direct insults, name-calling, and aggressive language directed at other players.
|
||||||
|
|
||||||
|
### 1.3 Ranting
|
||||||
|
Posting lengthy, emotional complaints that do not contribute to constructive discussion.
|
||||||
|
|
||||||
|
### 1.4 Personal Attacks
|
||||||
|
Attacking another player personally rather than addressing their arguments or ideas.
|
||||||
|
|
||||||
|
### 1.5 Harassment
|
||||||
|
Repeated unwanted contact or attention directed at a specific player, including following them across threads to attack or belittle them.
|
||||||
|
|
||||||
|
### 1.6 Doxxing
|
||||||
|
Revealing or threatening to reveal real-life personal information about another player without their consent.
|
||||||
|
|
||||||
|
### 1.7 Racism & Discrimination
|
||||||
|
Any form of discrimination based on race, ethnicity, national origin, or similar characteristics.
|
||||||
|
|
||||||
|
### 1.8 Hate Speech
|
||||||
|
Speech that attacks a person or group on the basis of protected attributes such as race, religion, ethnic origin, national origin, sex, disability, sexual orientation, or gender identity.
|
||||||
|
|
||||||
|
### 1.9 Sexism
|
||||||
|
Discrimination or prejudice based on sex or gender.
|
||||||
|
|
||||||
|
### 1.10 Spamming
|
||||||
|
Posting the same or similar content repeatedly, or posting content with no meaningful contribution to discussion.
|
||||||
|
|
||||||
|
### 1.11 Bumping
|
||||||
|
Posting simply to move a thread to the top of the forum without adding meaningful content.
|
||||||
|
|
||||||
|
### 1.12 Off-Topic Posting
|
||||||
|
Posting content that is not relevant to the thread topic or forum section.
|
||||||
|
|
||||||
|
### 1.13 Pyramid Quoting
|
||||||
|
Excessive nested quoting that makes posts difficult to read and disrupts discussion flow.
|
||||||
|
|
||||||
|
### 1.14 Rumor Mongering
|
||||||
|
Spreading unverified information or speculation as fact, particularly regarding CCP decisions, policies, or future plans.
|
||||||
|
|
||||||
|
### 1.15 New Player Bashing
|
||||||
|
Hostile or dismissive behavior toward new players asking questions or learning the game.
|
||||||
|
|
||||||
|
### 1.16 Impersonation
|
||||||
|
Pretending to be another player, CCP employee, or ISD volunteer.
|
||||||
|
|
||||||
|
### 1.17 Advertising
|
||||||
|
Promoting products, services, or websites unrelated to EVE Online.
|
||||||
|
|
||||||
|
## 2. Prohibited Content
|
||||||
|
|
||||||
|
The following content is not permitted on the EVE Online forums:
|
||||||
|
|
||||||
|
### 2.1 Pornography
|
||||||
|
Sexually explicit images or content.
|
||||||
|
|
||||||
|
### 2.2 Profanity
|
||||||
|
Excessive or extreme profanity, particularly when directed at others.
|
||||||
|
|
||||||
|
### 2.3 Real Money Trading (RMT)
|
||||||
|
Discussion of, solicitation for, or promotion of real money trading of in-game items, ISK, or accounts.
|
||||||
|
|
||||||
|
### 2.4 Discussion of Warnings & Bans
|
||||||
|
Public discussion of warnings, bans, or other disciplinary actions taken against any player.
|
||||||
|
|
||||||
|
### 2.5 Discussion of Moderation
|
||||||
|
Publicly discussing or disputing moderation decisions. Appeals should be handled through proper support channels.
|
||||||
|
|
||||||
|
### 2.6 Private Communications with CCP
|
||||||
|
Sharing private communications with CCP staff without permission.
|
||||||
|
|
||||||
|
### 2.7 In-Game Bugs & Exploits
|
||||||
|
Public discussion of bugs or exploits that could be abused. These should be reported through proper bug reporting channels.
|
||||||
|
|
||||||
|
### 2.8 Real World Religion
|
||||||
|
Discussion of real-world religious beliefs or practices.
|
||||||
|
|
||||||
|
### 2.9 Real World Politics
|
||||||
|
Discussion of real-world political topics, parties, or figures.
|
||||||
|
|
||||||
|
### 2.10 Layout-Distorting Content
|
||||||
|
Images, text, or formatting that disrupts the normal forum layout.
|
||||||
|
|
||||||
|
### 2.11 Advertising Other Games/Services
|
||||||
|
Promoting other games or competing services.
|
||||||
|
|
||||||
|
### 2.12 Real Financial Transfer Requests
|
||||||
|
Soliciting real money from other players.
|
||||||
|
|
||||||
|
## 3. Non-Constructive Posting
|
||||||
|
|
||||||
|
Posts should contribute positively to discussion. Posts that exist solely to complain, mock, or derail without offering constructive feedback may be moderated.
|
||||||
|
|
||||||
|
## 4. Abuse of Forum Tools
|
||||||
|
|
||||||
|
Misusing forum features such as flagging, voting, or reporting in bad faith.
|
||||||
|
|
||||||
|
## 5. Re-Opening Locked Topics
|
||||||
|
|
||||||
|
Creating new threads to continue discussion from threads that have been locked by moderators.
|
||||||
|
|
||||||
|
## 6. Language Requirements
|
||||||
|
|
||||||
|
Posts in English-language forum sections must be in English. Other language sections have their own requirements.
|
||||||
|
|
||||||
|
## 7. Attacking CCP Employees or ISD Volunteers
|
||||||
|
|
||||||
|
Personal attacks or harassment directed at CCP staff or ISD volunteers is strictly prohibited.
|
||||||
|
|
||||||
|
## 8. Section-Specific Rules
|
||||||
|
|
||||||
|
Individual forum sections may have additional rules that must be followed.
|
||||||
|
|
||||||
|
## 9. Killmail/Chatlog Abuse
|
||||||
|
|
||||||
|
Using killmails or chat logs primarily to troll, flame, or harass other players rather than for legitimate discussion.
|
||||||
3
src/eve_mod/__init__.py
Normal file
3
src/eve_mod/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
"""EVE Forum Moderator - AI-assisted forum moderation tool."""
|
||||||
|
|
||||||
|
__version__ = "0.1.0"
|
||||||
333
src/eve_mod/analyzer.py
Normal file
333
src/eve_mod/analyzer.py
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
"""LLM-powered rule violation analyzer using Claude via llm-proxy."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
from anthropic import Anthropic
|
||||||
|
from anthropic.types import TextBlock
|
||||||
|
|
||||||
|
from .discourse import UserPost
|
||||||
|
from .rules import ALL_RULES, RULES_BY_ID, Rule, Severity, format_rules_for_prompt
|
||||||
|
|
||||||
|
# Default model to use
|
||||||
|
DEFAULT_MODEL = "claude-sonnet-4-5-20250514"
|
||||||
|
|
||||||
|
# Batch size for analyzing posts
|
||||||
|
BATCH_SIZE = 5
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Violation:
|
||||||
|
"""A detected rule violation in a post."""
|
||||||
|
|
||||||
|
rule_id: str
|
||||||
|
rule_name: str
|
||||||
|
severity: str
|
||||||
|
confidence: float # 0.0-1.0
|
||||||
|
explanation: str
|
||||||
|
quote: str # Relevant excerpt from the post
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rule(self) -> Rule | None:
|
||||||
|
"""Get the full Rule object."""
|
||||||
|
return RULES_BY_ID.get(self.rule_id)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PostAnalysis:
|
||||||
|
"""Analysis result for a single post."""
|
||||||
|
|
||||||
|
post: UserPost
|
||||||
|
violations: list[Violation] = field(default_factory=list)
|
||||||
|
clean: bool = True
|
||||||
|
error: str | None = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_violations(self) -> bool:
|
||||||
|
"""Check if any violations were found."""
|
||||||
|
return len(self.violations) > 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_severity(self) -> str:
|
||||||
|
"""Get the highest severity among violations."""
|
||||||
|
if not self.violations:
|
||||||
|
return "none"
|
||||||
|
|
||||||
|
severity_order = {"low": 0, "medium": 1, "high": 2, "critical": 3}
|
||||||
|
max_sev = max(self.violations, key=lambda v: severity_order.get(v.severity, 0))
|
||||||
|
return max_sev.severity
|
||||||
|
|
||||||
|
|
||||||
|
class AnalyzerError(Exception):
|
||||||
|
"""Raised when analysis fails."""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
SYSTEM_PROMPT = """You are an EVE Online forum moderator assistant. Your role is to analyze forum posts and identify potential violations of the forum moderation policy.
|
||||||
|
|
||||||
|
{rules}
|
||||||
|
|
||||||
|
## Important Context
|
||||||
|
|
||||||
|
EVE Online is a competitive PvP MMO where trash talk between rival corporations and alliances is part of the culture. However, there are limits:
|
||||||
|
- In-game rivalry and competitive banter is generally acceptable
|
||||||
|
- Personal attacks that go beyond the game are not acceptable
|
||||||
|
- Discrimination, hate speech, and harassment are never acceptable regardless of "roleplay"
|
||||||
|
- Criticism of game mechanics or CCP decisions is allowed if constructive
|
||||||
|
|
||||||
|
## Your Task
|
||||||
|
|
||||||
|
For each post provided, analyze whether it violates any forum rules. Consider:
|
||||||
|
1. The context of EVE Online's competitive culture
|
||||||
|
2. Whether the post is directed at in-game entities (acceptable) vs. real people (less acceptable)
|
||||||
|
3. The severity of the violation if one exists
|
||||||
|
4. Your confidence level in the assessment
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
Respond with valid JSON in this exact format:
|
||||||
|
{{
|
||||||
|
"analyses": [
|
||||||
|
{{
|
||||||
|
"post_id": <number>,
|
||||||
|
"violations": [
|
||||||
|
{{
|
||||||
|
"rule_id": "<string>",
|
||||||
|
"severity": "low|medium|high|critical",
|
||||||
|
"confidence": <0.0-1.0>,
|
||||||
|
"explanation": "<brief explanation>",
|
||||||
|
"quote": "<relevant excerpt from the post>"
|
||||||
|
}}
|
||||||
|
],
|
||||||
|
"clean": <true if no violations, false otherwise>
|
||||||
|
}}
|
||||||
|
]
|
||||||
|
}}
|
||||||
|
|
||||||
|
If a post has no violations, return an empty violations array and clean=true.
|
||||||
|
Be conservative - only flag clear violations. Borderline cases should have lower confidence scores.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Analyzer:
|
||||||
|
"""Analyzes forum posts for rule violations using Claude."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
api_key: str | None = None,
|
||||||
|
base_url: str | None = None,
|
||||||
|
model: str = DEFAULT_MODEL,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Initialize the analyzer.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
api_key: LLM proxy API key (defaults to LLM_PROXY_KEY env var)
|
||||||
|
base_url: LLM proxy base URL (defaults to https://llm-proxy.ext.ben.io/v1)
|
||||||
|
model: Model to use for analysis
|
||||||
|
"""
|
||||||
|
self.api_key = api_key or os.environ.get("LLM_PROXY_KEY")
|
||||||
|
if not self.api_key:
|
||||||
|
raise AnalyzerError(
|
||||||
|
"No API key provided. Set LLM_PROXY_KEY environment variable "
|
||||||
|
"or pass api_key parameter."
|
||||||
|
)
|
||||||
|
|
||||||
|
self.base_url = base_url or os.environ.get(
|
||||||
|
"LLM_PROXY_URL", "https://llm-proxy.ext.ben.io/v1"
|
||||||
|
)
|
||||||
|
self.model = model
|
||||||
|
|
||||||
|
# Initialize Anthropic client with custom base URL
|
||||||
|
self.client = Anthropic(
|
||||||
|
api_key=self.api_key,
|
||||||
|
base_url=self.base_url,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _build_system_prompt(self) -> str:
|
||||||
|
"""Build the system prompt with rule definitions."""
|
||||||
|
rules = format_rules_for_prompt()
|
||||||
|
return SYSTEM_PROMPT.format(rules=rules)
|
||||||
|
|
||||||
|
def _format_posts_for_analysis(self, posts: list[UserPost]) -> str:
|
||||||
|
"""Format posts for the LLM prompt."""
|
||||||
|
formatted = []
|
||||||
|
for post in posts:
|
||||||
|
formatted.append(
|
||||||
|
f"""---
|
||||||
|
POST ID: {post.post_id}
|
||||||
|
TOPIC: {post.topic_title}
|
||||||
|
CATEGORY: {post.category_name or "Unknown"}
|
||||||
|
DATE: {post.created_at.strftime("%Y-%m-%d %H:%M UTC")}
|
||||||
|
URL: {post.url}
|
||||||
|
|
||||||
|
CONTENT:
|
||||||
|
{post.content_text}
|
||||||
|
---"""
|
||||||
|
)
|
||||||
|
return "\n\n".join(formatted)
|
||||||
|
|
||||||
|
def _parse_response(
|
||||||
|
self, response_text: str, posts: list[UserPost]
|
||||||
|
) -> list[PostAnalysis]:
|
||||||
|
"""Parse the LLM response into PostAnalysis objects."""
|
||||||
|
try:
|
||||||
|
# Try to extract JSON from the response
|
||||||
|
# Sometimes the model wraps it in markdown code blocks
|
||||||
|
json_match = response_text
|
||||||
|
if "```json" in response_text:
|
||||||
|
start = response_text.find("```json") + 7
|
||||||
|
end = response_text.find("```", start)
|
||||||
|
json_match = response_text[start:end].strip()
|
||||||
|
elif "```" in response_text:
|
||||||
|
start = response_text.find("```") + 3
|
||||||
|
end = response_text.find("```", start)
|
||||||
|
json_match = response_text[start:end].strip()
|
||||||
|
|
||||||
|
data = json.loads(json_match)
|
||||||
|
analyses = data.get("analyses", [])
|
||||||
|
|
||||||
|
# Create lookup for posts
|
||||||
|
post_lookup = {p.post_id: p for p in posts}
|
||||||
|
|
||||||
|
results: list[PostAnalysis] = []
|
||||||
|
for analysis in analyses:
|
||||||
|
post_id = analysis.get("post_id")
|
||||||
|
post = post_lookup.get(post_id)
|
||||||
|
|
||||||
|
if not post:
|
||||||
|
continue
|
||||||
|
|
||||||
|
violations = []
|
||||||
|
for v in analysis.get("violations", []):
|
||||||
|
rule_id = v.get("rule_id", "")
|
||||||
|
rule = RULES_BY_ID.get(rule_id)
|
||||||
|
|
||||||
|
violations.append(
|
||||||
|
Violation(
|
||||||
|
rule_id=rule_id,
|
||||||
|
rule_name=rule.name if rule else "Unknown",
|
||||||
|
severity=v.get("severity", "medium"),
|
||||||
|
confidence=v.get("confidence", 0.5),
|
||||||
|
explanation=v.get("explanation", ""),
|
||||||
|
quote=v.get("quote", ""),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
results.append(
|
||||||
|
PostAnalysis(
|
||||||
|
post=post,
|
||||||
|
violations=violations,
|
||||||
|
clean=analysis.get("clean", len(violations) == 0),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add any posts that weren't in the response
|
||||||
|
analyzed_ids = {r.post.post_id for r in results}
|
||||||
|
for post in posts:
|
||||||
|
if post.post_id not in analyzed_ids:
|
||||||
|
results.append(
|
||||||
|
PostAnalysis(
|
||||||
|
post=post,
|
||||||
|
violations=[],
|
||||||
|
clean=True,
|
||||||
|
error="Not included in LLM response",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
# If JSON parsing fails, return error results for all posts
|
||||||
|
return [
|
||||||
|
PostAnalysis(
|
||||||
|
post=post,
|
||||||
|
violations=[],
|
||||||
|
clean=True,
|
||||||
|
error=f"Failed to parse LLM response: {e}",
|
||||||
|
)
|
||||||
|
for post in posts
|
||||||
|
]
|
||||||
|
|
||||||
|
def analyze_batch(self, posts: list[UserPost]) -> list[PostAnalysis]:
|
||||||
|
"""
|
||||||
|
Analyze a batch of posts for rule violations.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
posts: List of posts to analyze
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of PostAnalysis results
|
||||||
|
"""
|
||||||
|
if not posts:
|
||||||
|
return []
|
||||||
|
|
||||||
|
system_prompt = self._build_system_prompt()
|
||||||
|
user_message = f"""Please analyze the following {len(posts)} forum posts for potential rule violations:
|
||||||
|
|
||||||
|
{self._format_posts_for_analysis(posts)}
|
||||||
|
|
||||||
|
Analyze each post and return your findings in the specified JSON format."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = self.client.messages.create(
|
||||||
|
model=self.model,
|
||||||
|
max_tokens=4096,
|
||||||
|
system=system_prompt,
|
||||||
|
messages=[{"role": "user", "content": user_message}],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extract text from the response (handle different content block types)
|
||||||
|
text_block = next(
|
||||||
|
(block for block in response.content if isinstance(block, TextBlock)),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if text_block is None:
|
||||||
|
raise AnalyzerError("No text content in LLM response")
|
||||||
|
response_text = text_block.text
|
||||||
|
return self._parse_response(response_text, posts)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Return error results for all posts
|
||||||
|
return [
|
||||||
|
PostAnalysis(
|
||||||
|
post=post,
|
||||||
|
violations=[],
|
||||||
|
clean=True,
|
||||||
|
error=f"Analysis failed: {e}",
|
||||||
|
)
|
||||||
|
for post in posts
|
||||||
|
]
|
||||||
|
|
||||||
|
def analyze_all(
|
||||||
|
self,
|
||||||
|
posts: list[UserPost],
|
||||||
|
batch_size: int = BATCH_SIZE,
|
||||||
|
progress_callback: "Callable[[int, int], None] | None" = None,
|
||||||
|
) -> list[PostAnalysis]:
|
||||||
|
"""
|
||||||
|
Analyze all posts, processing in batches.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
posts: All posts to analyze
|
||||||
|
batch_size: Number of posts per batch
|
||||||
|
progress_callback: Optional callback(current, total) for progress updates
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of all PostAnalysis results
|
||||||
|
"""
|
||||||
|
all_results: list[PostAnalysis] = []
|
||||||
|
total = len(posts)
|
||||||
|
|
||||||
|
for i in range(0, total, batch_size):
|
||||||
|
batch = posts[i : i + batch_size]
|
||||||
|
results = self.analyze_batch(batch)
|
||||||
|
all_results.extend(results)
|
||||||
|
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(min(i + batch_size, total), total)
|
||||||
|
|
||||||
|
return all_results
|
||||||
133
src/eve_mod/auth.py
Normal file
133
src/eve_mod/auth.py
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
"""Authentication utilities for EVE Forums (Discourse)."""
|
||||||
|
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Required cookies for Discourse authentication
|
||||||
|
REQUIRED_COOKIES = ("_forum_session", "_t")
|
||||||
|
|
||||||
|
# Optional but useful cookies
|
||||||
|
OPTIONAL_COOKIES = ("_cf_clearance", "cf_clearance")
|
||||||
|
|
||||||
|
|
||||||
|
class AuthError(Exception):
|
||||||
|
"""Raised when authentication fails or cookies are invalid."""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def parse_cookies_env(content: str) -> dict[str, str]:
|
||||||
|
"""
|
||||||
|
Parse cookies from a cookies.env file.
|
||||||
|
|
||||||
|
Supports two formats:
|
||||||
|
|
||||||
|
1. Simple key=value (one per line):
|
||||||
|
_forum_session=abc123...
|
||||||
|
_t=xyz789...
|
||||||
|
|
||||||
|
2. Netscape/curl cookie format:
|
||||||
|
# Netscape HTTP Cookie File
|
||||||
|
.eveonline.com TRUE / TRUE 0 _forum_session abc123...
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: The raw file content
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary of cookie name -> value
|
||||||
|
"""
|
||||||
|
cookies: dict[str, str] = {}
|
||||||
|
|
||||||
|
for line in content.strip().splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
|
||||||
|
# Skip empty lines and comments
|
||||||
|
if not line or line.startswith("#"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Try Netscape format first (tab-separated, 7 fields)
|
||||||
|
if "\t" in line:
|
||||||
|
parts = line.split("\t")
|
||||||
|
if len(parts) >= 7:
|
||||||
|
# Netscape format: domain, flag, path, secure, expiry, name, value
|
||||||
|
name = parts[5]
|
||||||
|
value = parts[6]
|
||||||
|
cookies[name] = value
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Try simple key=value format
|
||||||
|
if "=" in line:
|
||||||
|
# Handle values that might contain '='
|
||||||
|
key, _, value = line.partition("=")
|
||||||
|
key = key.strip()
|
||||||
|
value = value.strip()
|
||||||
|
|
||||||
|
# Remove quotes if present
|
||||||
|
if value.startswith('"') and value.endswith('"'):
|
||||||
|
value = value[1:-1]
|
||||||
|
elif value.startswith("'") and value.endswith("'"):
|
||||||
|
value = value[1:-1]
|
||||||
|
|
||||||
|
if key and value:
|
||||||
|
cookies[key] = value
|
||||||
|
|
||||||
|
return cookies
|
||||||
|
|
||||||
|
|
||||||
|
def load_cookies(cookie_path: Path | str) -> dict[str, str]:
|
||||||
|
"""
|
||||||
|
Load and validate cookies from a file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cookie_path: Path to the cookies file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary of cookie name -> value
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AuthError: If file not found or required cookies missing
|
||||||
|
"""
|
||||||
|
path = Path(cookie_path)
|
||||||
|
|
||||||
|
if not path.exists():
|
||||||
|
raise AuthError(
|
||||||
|
f"Cookie file not found: {path}\n"
|
||||||
|
f"Please export your browser cookies to {path}\n"
|
||||||
|
f"Required cookies: {', '.join(REQUIRED_COOKIES)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
content = path.read_text()
|
||||||
|
cookies = parse_cookies_env(content)
|
||||||
|
|
||||||
|
# Check for required cookies
|
||||||
|
missing = [name for name in REQUIRED_COOKIES if name not in cookies]
|
||||||
|
if missing:
|
||||||
|
raise AuthError(
|
||||||
|
f"Missing required cookies: {', '.join(missing)}\n"
|
||||||
|
f"Found cookies: {', '.join(cookies.keys())}\n"
|
||||||
|
f"Please ensure you're logged into forums.eveonline.com before exporting cookies."
|
||||||
|
)
|
||||||
|
|
||||||
|
return cookies
|
||||||
|
|
||||||
|
|
||||||
|
def validate_session_cookie(session_value: str) -> bool:
|
||||||
|
"""
|
||||||
|
Basic validation of the session cookie format.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session_value: The _forum_session cookie value
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if the cookie appears valid
|
||||||
|
"""
|
||||||
|
# Discourse session cookies are typically URL-encoded strings
|
||||||
|
# with a specific structure. Basic length check.
|
||||||
|
if not session_value or len(session_value) < 20:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Should contain URL-safe characters
|
||||||
|
if not re.match(r"^[A-Za-z0-9%_\-\.]+$", session_value):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
227
src/eve_mod/cli.py
Normal file
227
src/eve_mod/cli.py
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
"""CLI entrypoint for EVE Forum Moderator."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import click
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.progress import Progress, SpinnerColumn, TextColumn
|
||||||
|
|
||||||
|
from .analyzer import Analyzer, AnalyzerError
|
||||||
|
from .auth import AuthError, load_cookies
|
||||||
|
from .discourse import DiscourseClient, DiscourseError, UserNotFoundError
|
||||||
|
from .report import ReportGenerator
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
|
@click.group()
|
||||||
|
@click.version_option(version="0.1.0")
|
||||||
|
def cli() -> None:
|
||||||
|
"""EVE Forum Moderator - AI-assisted forum moderation tool."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.argument("username")
|
||||||
|
@click.option(
|
||||||
|
"--days",
|
||||||
|
"-d",
|
||||||
|
default=30,
|
||||||
|
help="Number of days to analyze (default: 30)",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--cookies",
|
||||||
|
"-c",
|
||||||
|
type=click.Path(exists=False),
|
||||||
|
default="cookies.env",
|
||||||
|
help="Path to cookies file (default: cookies.env)",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--output",
|
||||||
|
"-o",
|
||||||
|
type=click.Path(),
|
||||||
|
default=None,
|
||||||
|
help="Custom output path for markdown report",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--verbose",
|
||||||
|
"-v",
|
||||||
|
is_flag=True,
|
||||||
|
help="Show verbose output including detailed analysis",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--max-posts",
|
||||||
|
default=200,
|
||||||
|
help="Maximum number of posts to fetch (default: 200)",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--enrich/--no-enrich",
|
||||||
|
default=True,
|
||||||
|
help="Fetch full post content (slower but more accurate)",
|
||||||
|
)
|
||||||
|
def review(
|
||||||
|
username: str,
|
||||||
|
days: int,
|
||||||
|
cookies: str,
|
||||||
|
output: str | None,
|
||||||
|
verbose: bool,
|
||||||
|
max_posts: int,
|
||||||
|
enrich: bool,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Review a user's forum posts for potential rule violations.
|
||||||
|
|
||||||
|
USERNAME is the EVE Forum username to review.
|
||||||
|
"""
|
||||||
|
# Load environment variables (for LLM_PROXY_KEY)
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Load cookies
|
||||||
|
with console.status("[bold blue]Loading authentication cookies..."):
|
||||||
|
cookie_dict = load_cookies(Path(cookies))
|
||||||
|
console.print(f"[green]Loaded {len(cookie_dict)} cookies[/green]")
|
||||||
|
|
||||||
|
# Run async operations
|
||||||
|
asyncio.run(
|
||||||
|
_review_user(
|
||||||
|
username=username,
|
||||||
|
days=days,
|
||||||
|
cookies=cookie_dict,
|
||||||
|
output=output,
|
||||||
|
verbose=verbose,
|
||||||
|
max_posts=max_posts,
|
||||||
|
enrich=enrich,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
except AuthError as e:
|
||||||
|
console.print(f"[red]Authentication Error:[/red] {e}")
|
||||||
|
raise SystemExit(1)
|
||||||
|
except AnalyzerError as e:
|
||||||
|
console.print(f"[red]Analysis Error:[/red] {e}")
|
||||||
|
raise SystemExit(1)
|
||||||
|
except Exception as e:
|
||||||
|
console.print(f"[red]Unexpected Error:[/red] {e}")
|
||||||
|
if verbose:
|
||||||
|
console.print_exception()
|
||||||
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
|
||||||
|
async def _review_user(
|
||||||
|
username: str,
|
||||||
|
days: int,
|
||||||
|
cookies: dict[str, str],
|
||||||
|
output: str | None,
|
||||||
|
verbose: bool,
|
||||||
|
max_posts: int,
|
||||||
|
enrich: bool,
|
||||||
|
) -> None:
|
||||||
|
"""Async implementation of user review."""
|
||||||
|
try:
|
||||||
|
async with DiscourseClient(cookies) as client:
|
||||||
|
# Fetch user profile
|
||||||
|
with console.status(f"[bold blue]Fetching profile for {username}..."):
|
||||||
|
try:
|
||||||
|
user = await client.get_user(username)
|
||||||
|
console.print(
|
||||||
|
f"[green]Found user:[/green] {user.username} "
|
||||||
|
f"(Trust Level {user.trust_level}, {user.post_count} posts)"
|
||||||
|
)
|
||||||
|
except UserNotFoundError:
|
||||||
|
console.print(f"[red]User not found:[/red] {username}")
|
||||||
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
# Fetch posts
|
||||||
|
with console.status(f"[bold blue]Fetching posts from last {days} days..."):
|
||||||
|
posts = await client.get_user_posts(
|
||||||
|
username=username,
|
||||||
|
days=days,
|
||||||
|
max_posts=max_posts,
|
||||||
|
)
|
||||||
|
console.print(f"[green]Found {len(posts)} posts[/green]")
|
||||||
|
|
||||||
|
if not posts:
|
||||||
|
console.print(
|
||||||
|
"[yellow]No posts found in the specified time period.[/yellow]"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Enrich posts with full content
|
||||||
|
if enrich:
|
||||||
|
with console.status("[bold blue]Fetching full post content..."):
|
||||||
|
posts = await client.enrich_posts(posts)
|
||||||
|
console.print("[green]Enriched posts with full content[/green]")
|
||||||
|
|
||||||
|
except DiscourseError as e:
|
||||||
|
console.print(f"[red]Forum Error:[/red] {e}")
|
||||||
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
# Analyze posts
|
||||||
|
console.print("[bold blue]Analyzing posts for rule violations...[/bold blue]")
|
||||||
|
|
||||||
|
try:
|
||||||
|
analyzer = Analyzer()
|
||||||
|
except AnalyzerError as e:
|
||||||
|
console.print(f"[red]Analyzer Error:[/red] {e}")
|
||||||
|
console.print(
|
||||||
|
"[dim]Hint: Set LLM_PROXY_KEY environment variable or add it to .env[/dim]"
|
||||||
|
)
|
||||||
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
with Progress(
|
||||||
|
SpinnerColumn(),
|
||||||
|
TextColumn("[progress.description]{task.description}"),
|
||||||
|
console=console,
|
||||||
|
) as progress:
|
||||||
|
task = progress.add_task("Analyzing...", total=len(posts))
|
||||||
|
|
||||||
|
def update_progress(current: int, total: int) -> None:
|
||||||
|
progress.update(task, completed=current)
|
||||||
|
|
||||||
|
analyses = analyzer.analyze_all(
|
||||||
|
posts=posts,
|
||||||
|
progress_callback=update_progress,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Generate report
|
||||||
|
report_gen = ReportGenerator()
|
||||||
|
|
||||||
|
# Print to terminal
|
||||||
|
report_gen.print_summary(user, analyses, days)
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
report_gen.print_detailed(analyses)
|
||||||
|
|
||||||
|
# Save to file
|
||||||
|
if output:
|
||||||
|
filepath = Path(output)
|
||||||
|
filepath.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
filepath.write_text(report_gen.generate_markdown(user, analyses, days))
|
||||||
|
else:
|
||||||
|
filepath = report_gen.save_report(user, analyses, days)
|
||||||
|
|
||||||
|
console.print(f"\n[green]Report saved to:[/green] {filepath}")
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
def rules() -> None:
|
||||||
|
"""Display all forum moderation rules."""
|
||||||
|
from .rules import ALL_RULES, RuleCategory
|
||||||
|
|
||||||
|
console.print("\n[bold]EVE Online Forum Moderation Rules[/bold]\n")
|
||||||
|
|
||||||
|
current_category = None
|
||||||
|
for rule in ALL_RULES:
|
||||||
|
if rule.category != current_category:
|
||||||
|
current_category = rule.category
|
||||||
|
console.print(f"\n[bold cyan]{current_category.value.upper()}[/bold cyan]")
|
||||||
|
|
||||||
|
console.print(f" [yellow]{rule.id}[/yellow] - [bold]{rule.name}[/bold]")
|
||||||
|
console.print(f" [dim]{rule.description[:80]}...[/dim]")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
cli()
|
||||||
340
src/eve_mod/discourse.py
Normal file
340
src/eve_mod/discourse.py
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
"""Discourse API client for EVE Online Forums."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import html
|
||||||
|
import re
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from dateutil.parser import parse as parse_date
|
||||||
|
|
||||||
|
# EVE Forums base URL
|
||||||
|
BASE_URL = "https://forums.eveonline.com"
|
||||||
|
|
||||||
|
# Rate limiting settings
|
||||||
|
REQUEST_DELAY = 0.5 # seconds between requests
|
||||||
|
MAX_RETRIES = 3
|
||||||
|
RETRY_BACKOFF = 2.0 # exponential backoff multiplier
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class UserPost:
|
||||||
|
"""Represents a single forum post by a user."""
|
||||||
|
|
||||||
|
post_id: int
|
||||||
|
post_number: int
|
||||||
|
topic_id: int
|
||||||
|
topic_title: str
|
||||||
|
topic_slug: str
|
||||||
|
content_raw: str
|
||||||
|
content_cooked: str # HTML version
|
||||||
|
created_at: datetime
|
||||||
|
category_name: str | None = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url(self) -> str:
|
||||||
|
"""Get the direct URL to this post."""
|
||||||
|
return f"{BASE_URL}/t/{self.topic_slug}/{self.topic_id}/{self.post_number}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def content_text(self) -> str:
|
||||||
|
"""Get plain text content (HTML stripped)."""
|
||||||
|
# Remove HTML tags
|
||||||
|
text = re.sub(r"<[^>]+>", " ", self.content_cooked)
|
||||||
|
# Decode HTML entities
|
||||||
|
text = html.unescape(text)
|
||||||
|
# Normalize whitespace
|
||||||
|
text = re.sub(r"\s+", " ", text).strip()
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class UserProfile:
|
||||||
|
"""Basic user profile information."""
|
||||||
|
|
||||||
|
username: str
|
||||||
|
name: str | None
|
||||||
|
created_at: datetime
|
||||||
|
trust_level: int
|
||||||
|
post_count: int
|
||||||
|
topics_entered: int
|
||||||
|
time_read: int # seconds
|
||||||
|
|
||||||
|
|
||||||
|
class DiscourseError(Exception):
|
||||||
|
"""Base exception for Discourse API errors."""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UserNotFoundError(DiscourseError):
|
||||||
|
"""Raised when a user is not found."""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AuthenticationError(DiscourseError):
|
||||||
|
"""Raised when authentication fails."""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RateLimitError(DiscourseError):
|
||||||
|
"""Raised when rate limited by the API."""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DiscourseClient:
|
||||||
|
"""Async client for the EVE Online Discourse forums."""
|
||||||
|
|
||||||
|
def __init__(self, cookies: dict[str, str], base_url: str = BASE_URL):
|
||||||
|
"""
|
||||||
|
Initialize the Discourse client.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cookies: Dictionary of authentication cookies
|
||||||
|
base_url: Forum base URL
|
||||||
|
"""
|
||||||
|
self.base_url = base_url.rstrip("/")
|
||||||
|
self.cookies = cookies
|
||||||
|
self._client: httpx.AsyncClient | None = None
|
||||||
|
self._last_request: float = 0
|
||||||
|
|
||||||
|
async def __aenter__(self) -> "DiscourseClient":
|
||||||
|
"""Enter async context."""
|
||||||
|
self._client = httpx.AsyncClient(
|
||||||
|
base_url=self.base_url,
|
||||||
|
cookies=self.cookies,
|
||||||
|
headers={
|
||||||
|
"Accept": "application/json",
|
||||||
|
"User-Agent": "EVE-Forum-Moderator/0.1.0",
|
||||||
|
},
|
||||||
|
timeout=30.0,
|
||||||
|
follow_redirects=True,
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, *args: Any) -> None:
|
||||||
|
"""Exit async context."""
|
||||||
|
if self._client:
|
||||||
|
await self._client.aclose()
|
||||||
|
self._client = None
|
||||||
|
|
||||||
|
async def _rate_limit(self) -> None:
|
||||||
|
"""Enforce rate limiting between requests."""
|
||||||
|
now = asyncio.get_event_loop().time()
|
||||||
|
elapsed = now - self._last_request
|
||||||
|
if elapsed < REQUEST_DELAY:
|
||||||
|
await asyncio.sleep(REQUEST_DELAY - elapsed)
|
||||||
|
self._last_request = asyncio.get_event_loop().time()
|
||||||
|
|
||||||
|
async def _request(self, method: str, path: str, **kwargs: Any) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Make a rate-limited request with retry logic.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
method: HTTP method
|
||||||
|
path: API path
|
||||||
|
**kwargs: Additional httpx request arguments
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Parsed JSON response
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
DiscourseError: On API errors
|
||||||
|
"""
|
||||||
|
if not self._client:
|
||||||
|
raise DiscourseError("Client not initialized. Use 'async with' context.")
|
||||||
|
|
||||||
|
last_error: Exception | None = None
|
||||||
|
|
||||||
|
for attempt in range(MAX_RETRIES):
|
||||||
|
await self._rate_limit()
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = await self._client.request(method, path, **kwargs)
|
||||||
|
|
||||||
|
if response.status_code == 404:
|
||||||
|
raise UserNotFoundError(f"Resource not found: {path}")
|
||||||
|
|
||||||
|
if response.status_code == 401:
|
||||||
|
raise AuthenticationError(
|
||||||
|
"Authentication failed. Your cookies may have expired."
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 403:
|
||||||
|
raise AuthenticationError(
|
||||||
|
"Access forbidden. You may not have permission to view this resource."
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 429:
|
||||||
|
retry_after = int(response.headers.get("Retry-After", 60))
|
||||||
|
raise RateLimitError(
|
||||||
|
f"Rate limited. Retry after {retry_after} seconds."
|
||||||
|
)
|
||||||
|
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
except (httpx.TimeoutException, httpx.NetworkError) as e:
|
||||||
|
last_error = e
|
||||||
|
if attempt < MAX_RETRIES - 1:
|
||||||
|
wait = REQUEST_DELAY * (RETRY_BACKOFF**attempt)
|
||||||
|
await asyncio.sleep(wait)
|
||||||
|
continue
|
||||||
|
|
||||||
|
raise DiscourseError(
|
||||||
|
f"Request failed after {MAX_RETRIES} retries: {last_error}"
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_user(self, username: str) -> UserProfile:
|
||||||
|
"""
|
||||||
|
Get user profile information.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
username: The forum username
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
UserProfile object
|
||||||
|
"""
|
||||||
|
data = await self._request("GET", f"/u/{username}.json")
|
||||||
|
user = data.get("user", {})
|
||||||
|
|
||||||
|
return UserProfile(
|
||||||
|
username=user.get("username", username),
|
||||||
|
name=user.get("name"),
|
||||||
|
created_at=parse_date(user.get("created_at", "2000-01-01")),
|
||||||
|
trust_level=user.get("trust_level", 0),
|
||||||
|
post_count=user.get("post_count", 0),
|
||||||
|
topics_entered=user.get("topics_entered", 0),
|
||||||
|
time_read=user.get("time_read", 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_user_posts(
|
||||||
|
self,
|
||||||
|
username: str,
|
||||||
|
days: int = 30,
|
||||||
|
max_posts: int = 200,
|
||||||
|
) -> list[UserPost]:
|
||||||
|
"""
|
||||||
|
Get a user's posts from the last N days.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
username: The forum username
|
||||||
|
days: Number of days to look back
|
||||||
|
max_posts: Maximum number of posts to fetch
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of UserPost objects, newest first
|
||||||
|
"""
|
||||||
|
posts: list[UserPost] = []
|
||||||
|
cutoff = datetime.now(timezone.utc) - timedelta(days=days)
|
||||||
|
offset = 0
|
||||||
|
|
||||||
|
while len(posts) < max_posts:
|
||||||
|
# Fetch user actions (includes posts)
|
||||||
|
data = await self._request(
|
||||||
|
"GET",
|
||||||
|
f"/user_actions.json",
|
||||||
|
params={
|
||||||
|
"username": username,
|
||||||
|
"filter": "5", # 5 = posts/replies
|
||||||
|
"offset": offset,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
actions = data.get("user_actions", [])
|
||||||
|
if not actions:
|
||||||
|
break
|
||||||
|
|
||||||
|
for action in actions:
|
||||||
|
# Parse the created_at timestamp
|
||||||
|
created_str = action.get("created_at", "")
|
||||||
|
if not created_str:
|
||||||
|
continue
|
||||||
|
|
||||||
|
created_at = parse_date(created_str)
|
||||||
|
if created_at.tzinfo is None:
|
||||||
|
created_at = created_at.replace(tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
# Stop if we've gone past the cutoff
|
||||||
|
if created_at < cutoff:
|
||||||
|
return posts
|
||||||
|
|
||||||
|
# Extract post data
|
||||||
|
post = UserPost(
|
||||||
|
post_id=action.get("post_id", 0),
|
||||||
|
post_number=action.get("post_number", 1),
|
||||||
|
topic_id=action.get("topic_id", 0),
|
||||||
|
topic_title=action.get("title", "Unknown Topic"),
|
||||||
|
topic_slug=action.get("slug", "topic"),
|
||||||
|
content_raw=action.get("excerpt", ""), # May be truncated
|
||||||
|
content_cooked=action.get("excerpt", ""),
|
||||||
|
created_at=created_at,
|
||||||
|
category_name=action.get("category_name"),
|
||||||
|
)
|
||||||
|
|
||||||
|
posts.append(post)
|
||||||
|
|
||||||
|
if len(posts) >= max_posts:
|
||||||
|
break
|
||||||
|
|
||||||
|
offset += len(actions)
|
||||||
|
|
||||||
|
# Safety check - if we got fewer than expected, we're at the end
|
||||||
|
if len(actions) < 30:
|
||||||
|
break
|
||||||
|
|
||||||
|
return posts
|
||||||
|
|
||||||
|
async def get_full_post(self, post_id: int) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Get the full content of a specific post.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
post_id: The post ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Post data dictionary
|
||||||
|
"""
|
||||||
|
data = await self._request("GET", f"/posts/{post_id}.json")
|
||||||
|
return data
|
||||||
|
|
||||||
|
async def enrich_posts(self, posts: list[UserPost]) -> list[UserPost]:
|
||||||
|
"""
|
||||||
|
Enrich posts with full content (the activity feed only has excerpts).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
posts: List of posts with potentially truncated content
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Posts with full content
|
||||||
|
"""
|
||||||
|
enriched: list[UserPost] = []
|
||||||
|
|
||||||
|
for post in posts:
|
||||||
|
try:
|
||||||
|
full_data = await self.get_full_post(post.post_id)
|
||||||
|
|
||||||
|
# Create new post with full content
|
||||||
|
enriched_post = UserPost(
|
||||||
|
post_id=post.post_id,
|
||||||
|
post_number=post.post_number,
|
||||||
|
topic_id=post.topic_id,
|
||||||
|
topic_title=post.topic_title,
|
||||||
|
topic_slug=post.topic_slug,
|
||||||
|
content_raw=full_data.get("raw", post.content_raw),
|
||||||
|
content_cooked=full_data.get("cooked", post.content_cooked),
|
||||||
|
created_at=post.created_at,
|
||||||
|
category_name=post.category_name,
|
||||||
|
)
|
||||||
|
enriched.append(enriched_post)
|
||||||
|
|
||||||
|
except DiscourseError:
|
||||||
|
# If we can't fetch full content, use what we have
|
||||||
|
enriched.append(post)
|
||||||
|
|
||||||
|
return enriched
|
||||||
302
src/eve_mod/report.py
Normal file
302
src/eve_mod/report.py
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
"""Report generator for forum moderation analysis."""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.panel import Panel
|
||||||
|
from rich.table import Table
|
||||||
|
from rich.text import Text
|
||||||
|
|
||||||
|
from .analyzer import PostAnalysis, Violation
|
||||||
|
from .discourse import UserProfile
|
||||||
|
|
||||||
|
|
||||||
|
class ReportGenerator:
|
||||||
|
"""Generates moderation reports in terminal and markdown formats."""
|
||||||
|
|
||||||
|
def __init__(self, output_dir: Path | str = "reports"):
|
||||||
|
"""
|
||||||
|
Initialize the report generator.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
output_dir: Directory to save markdown reports
|
||||||
|
"""
|
||||||
|
self.output_dir = Path(output_dir)
|
||||||
|
self.output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
self.console = Console()
|
||||||
|
|
||||||
|
def _severity_color(self, severity: str) -> str:
|
||||||
|
"""Get color for severity level."""
|
||||||
|
colors = {
|
||||||
|
"low": "yellow",
|
||||||
|
"medium": "orange1",
|
||||||
|
"high": "red",
|
||||||
|
"critical": "bold red",
|
||||||
|
}
|
||||||
|
return colors.get(severity, "white")
|
||||||
|
|
||||||
|
def _severity_emoji(self, severity: str) -> str:
|
||||||
|
"""Get indicator for severity level (Discourse-friendly)."""
|
||||||
|
# Using text indicators instead of emoji for broader compatibility
|
||||||
|
indicators = {
|
||||||
|
"low": "[LOW]",
|
||||||
|
"medium": "[MED]",
|
||||||
|
"high": "[HIGH]",
|
||||||
|
"critical": "[CRITICAL]",
|
||||||
|
}
|
||||||
|
return indicators.get(severity, "[-]")
|
||||||
|
|
||||||
|
def print_summary(
|
||||||
|
self,
|
||||||
|
user: UserProfile,
|
||||||
|
analyses: list[PostAnalysis],
|
||||||
|
days: int,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Print a summary to the terminal.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user: User profile information
|
||||||
|
analyses: List of post analyses
|
||||||
|
days: Number of days analyzed
|
||||||
|
"""
|
||||||
|
violations = [a for a in analyses if a.has_violations]
|
||||||
|
total_violations = sum(len(a.violations) for a in violations)
|
||||||
|
|
||||||
|
# Header
|
||||||
|
self.console.print()
|
||||||
|
self.console.print(
|
||||||
|
Panel(
|
||||||
|
f"[bold]User Review: {user.username}[/bold]\n"
|
||||||
|
f"Period: Last {days} days\n"
|
||||||
|
f"Posts Analyzed: {len(analyses)}\n"
|
||||||
|
f"Posts with Violations: {len(violations)}\n"
|
||||||
|
f"Total Violations Found: {total_violations}",
|
||||||
|
title="EVE Forum Moderation Report",
|
||||||
|
border_style="blue",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not violations:
|
||||||
|
self.console.print(
|
||||||
|
"\n[green]No rule violations detected in the analyzed posts.[/green]\n"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Summary table
|
||||||
|
table = Table(title="Violations Summary", show_header=True, header_style="bold")
|
||||||
|
table.add_column("Date", style="dim", width=12)
|
||||||
|
table.add_column("Topic", width=40, overflow="ellipsis")
|
||||||
|
table.add_column("Rule", width=25)
|
||||||
|
table.add_column("Severity", width=10, justify="center")
|
||||||
|
table.add_column("Confidence", width=10, justify="center")
|
||||||
|
|
||||||
|
for analysis in violations:
|
||||||
|
for v in analysis.violations:
|
||||||
|
table.add_row(
|
||||||
|
analysis.post.created_at.strftime("%Y-%m-%d"),
|
||||||
|
analysis.post.topic_title[:38] + "..."
|
||||||
|
if len(analysis.post.topic_title) > 40
|
||||||
|
else analysis.post.topic_title,
|
||||||
|
f"{v.rule_id}: {v.rule_name}",
|
||||||
|
Text(v.severity.upper(), style=self._severity_color(v.severity)),
|
||||||
|
f"{v.confidence:.0%}",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.console.print(table)
|
||||||
|
self.console.print()
|
||||||
|
|
||||||
|
def print_detailed(self, analyses: list[PostAnalysis]) -> None:
|
||||||
|
"""
|
||||||
|
Print detailed violation information to the terminal.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
analyses: List of post analyses with violations
|
||||||
|
"""
|
||||||
|
violations = [a for a in analyses if a.has_violations]
|
||||||
|
|
||||||
|
if not violations:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.console.print("[bold]Detailed Analysis[/bold]\n")
|
||||||
|
|
||||||
|
for analysis in violations:
|
||||||
|
self.console.print(
|
||||||
|
Panel(
|
||||||
|
f"[bold]{analysis.post.topic_title}[/bold]\n"
|
||||||
|
f"[dim]{analysis.post.url}[/dim]\n"
|
||||||
|
f"Date: {analysis.post.created_at.strftime('%Y-%m-%d %H:%M UTC')}",
|
||||||
|
border_style="yellow",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for v in analysis.violations:
|
||||||
|
self.console.print(
|
||||||
|
f" [{self._severity_color(v.severity)}]"
|
||||||
|
f"{v.severity.upper()}[/] - "
|
||||||
|
f"[bold]Rule {v.rule_id}: {v.rule_name}[/bold] "
|
||||||
|
f"(confidence: {v.confidence:.0%})"
|
||||||
|
)
|
||||||
|
self.console.print(f" [dim]Explanation:[/dim] {v.explanation}")
|
||||||
|
if v.quote:
|
||||||
|
self.console.print(f' [dim]Quote:[/dim] "{v.quote}"')
|
||||||
|
self.console.print()
|
||||||
|
|
||||||
|
def generate_markdown(
|
||||||
|
self,
|
||||||
|
user: UserProfile,
|
||||||
|
analyses: list[PostAnalysis],
|
||||||
|
days: int,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Generate a Discourse-compatible markdown report.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user: User profile information
|
||||||
|
analyses: List of post analyses
|
||||||
|
days: Number of days analyzed
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Markdown string
|
||||||
|
"""
|
||||||
|
violations = [a for a in analyses if a.has_violations]
|
||||||
|
total_violations = sum(len(a.violations) for a in violations)
|
||||||
|
|
||||||
|
lines = [
|
||||||
|
f"## User Review: {user.username}",
|
||||||
|
"",
|
||||||
|
f"**Period:** Last {days} days (ending {datetime.now().strftime('%Y-%m-%d')})",
|
||||||
|
f"**Posts Analyzed:** {len(analyses)}",
|
||||||
|
f"**Posts with Violations:** {len(violations)}",
|
||||||
|
f"**Total Violations Found:** {total_violations}",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
|
||||||
|
if not violations:
|
||||||
|
lines.extend(
|
||||||
|
[
|
||||||
|
"---",
|
||||||
|
"",
|
||||||
|
"*No rule violations detected in the analyzed posts.*",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
# Summary table
|
||||||
|
lines.extend(
|
||||||
|
[
|
||||||
|
"---",
|
||||||
|
"",
|
||||||
|
"### Summary",
|
||||||
|
"",
|
||||||
|
"| Date | Topic | Rule | Severity | Confidence |",
|
||||||
|
"|------|-------|------|----------|------------|",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
for analysis in violations:
|
||||||
|
for v in analysis.violations:
|
||||||
|
topic_link = f"[{self._escape_md(analysis.post.topic_title[:40])}]({analysis.post.url})"
|
||||||
|
lines.append(
|
||||||
|
f"| {analysis.post.created_at.strftime('%Y-%m-%d')} "
|
||||||
|
f"| {topic_link} "
|
||||||
|
f"| {v.rule_id}: {v.rule_name} "
|
||||||
|
f"| {self._severity_emoji(v.severity)} "
|
||||||
|
f"| {v.confidence:.0%} |"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Detailed analysis
|
||||||
|
lines.extend(
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"---",
|
||||||
|
"",
|
||||||
|
"### Detailed Analysis",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
for i, analysis in enumerate(violations, 1):
|
||||||
|
lines.extend(
|
||||||
|
[
|
||||||
|
f"#### {i}. [{self._escape_md(analysis.post.topic_title)}]({analysis.post.url})",
|
||||||
|
f"**Date:** {analysis.post.created_at.strftime('%Y-%m-%d %H:%M UTC')} | "
|
||||||
|
f"**Category:** {analysis.post.category_name or 'Unknown'}",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
for v in analysis.violations:
|
||||||
|
lines.extend(
|
||||||
|
[
|
||||||
|
f"**{self._severity_emoji(v.severity)} Rule {v.rule_id} - {v.rule_name}** "
|
||||||
|
f"(Confidence: {v.confidence:.0%})",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
if v.quote:
|
||||||
|
lines.extend(
|
||||||
|
[
|
||||||
|
f"> {self._escape_md(v.quote)}",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
lines.extend(
|
||||||
|
[
|
||||||
|
f"*{v.explanation}*",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
lines.append("---")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# Footer
|
||||||
|
lines.extend(
|
||||||
|
[
|
||||||
|
"*Report generated by EVE Forum Moderator Assistant*",
|
||||||
|
f"*Generated: {datetime.now().strftime('%Y-%m-%d %H:%M UTC')}*",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
def _escape_md(self, text: str) -> str:
|
||||||
|
"""Escape markdown special characters."""
|
||||||
|
# Escape characters that could break markdown formatting
|
||||||
|
for char in ["[", "]", "|", "*", "_", "`"]:
|
||||||
|
text = text.replace(char, f"\\{char}")
|
||||||
|
return text
|
||||||
|
|
||||||
|
def save_report(
|
||||||
|
self,
|
||||||
|
user: UserProfile,
|
||||||
|
analyses: list[PostAnalysis],
|
||||||
|
days: int,
|
||||||
|
filename: str | None = None,
|
||||||
|
) -> Path:
|
||||||
|
"""
|
||||||
|
Save a markdown report to file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user: User profile information
|
||||||
|
analyses: List of post analyses
|
||||||
|
days: Number of days analyzed
|
||||||
|
filename: Optional custom filename
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path to the saved report
|
||||||
|
"""
|
||||||
|
if filename is None:
|
||||||
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
filename = f"{user.username}_{timestamp}.md"
|
||||||
|
|
||||||
|
filepath = self.output_dir / filename
|
||||||
|
content = self.generate_markdown(user, analyses, days)
|
||||||
|
filepath.write_text(content)
|
||||||
|
|
||||||
|
return filepath
|
||||||
476
src/eve_mod/rules.py
Normal file
476
src/eve_mod/rules.py
Normal file
@@ -0,0 +1,476 @@
|
|||||||
|
"""EVE Forum Moderation Policy - Structured rule definitions."""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
class RuleCategory(str, Enum):
|
||||||
|
"""Categories of forum rules."""
|
||||||
|
|
||||||
|
CONDUCT = "conduct"
|
||||||
|
CONTENT = "content"
|
||||||
|
OTHER = "other"
|
||||||
|
|
||||||
|
|
||||||
|
class Severity(str, Enum):
|
||||||
|
"""Severity levels for rule violations."""
|
||||||
|
|
||||||
|
LOW = "low"
|
||||||
|
MEDIUM = "medium"
|
||||||
|
HIGH = "high"
|
||||||
|
CRITICAL = "critical"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Rule:
|
||||||
|
"""A forum moderation rule."""
|
||||||
|
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
category: RuleCategory
|
||||||
|
description: str
|
||||||
|
default_severity: Severity
|
||||||
|
examples: tuple[str, ...] = ()
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.id}: {self.name}"
|
||||||
|
|
||||||
|
|
||||||
|
# Prohibited Conduct Rules (1.x)
|
||||||
|
CONDUCT_RULES = (
|
||||||
|
Rule(
|
||||||
|
id="1.1",
|
||||||
|
name="Trolling",
|
||||||
|
category=RuleCategory.CONDUCT,
|
||||||
|
description="Posting inflammatory, extraneous, or off-topic messages with the intent of provoking an emotional response or disrupting normal discussion.",
|
||||||
|
default_severity=Severity.MEDIUM,
|
||||||
|
examples=(
|
||||||
|
"Deliberately misrepresenting someone's position to provoke anger",
|
||||||
|
"Posting bait designed to start arguments",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
id="1.2",
|
||||||
|
name="Flaming",
|
||||||
|
category=RuleCategory.CONDUCT,
|
||||||
|
description="Hostile and insulting interaction between forum users. This includes direct insults, name-calling, and aggressive language directed at other players.",
|
||||||
|
default_severity=Severity.MEDIUM,
|
||||||
|
examples=(
|
||||||
|
"Calling another player an idiot or similar insult",
|
||||||
|
"Aggressive personal attacks in response to disagreement",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
id="1.3",
|
||||||
|
name="Ranting",
|
||||||
|
category=RuleCategory.CONDUCT,
|
||||||
|
description="Posting lengthy, emotional complaints that do not contribute to constructive discussion.",
|
||||||
|
default_severity=Severity.LOW,
|
||||||
|
examples=(
|
||||||
|
"Long posts consisting entirely of complaints without suggestions",
|
||||||
|
"Emotional outbursts without constructive content",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
id="1.4",
|
||||||
|
name="Personal Attacks",
|
||||||
|
category=RuleCategory.CONDUCT,
|
||||||
|
description="Attacking another player personally rather than addressing their arguments or ideas.",
|
||||||
|
default_severity=Severity.MEDIUM,
|
||||||
|
examples=(
|
||||||
|
"Attacking someone's character instead of their argument",
|
||||||
|
"Making personal accusations unrelated to the discussion",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
id="1.5",
|
||||||
|
name="Harassment",
|
||||||
|
category=RuleCategory.CONDUCT,
|
||||||
|
description="Repeated unwanted contact or attention directed at a specific player, including following them across threads to attack or belittle them.",
|
||||||
|
default_severity=Severity.HIGH,
|
||||||
|
examples=(
|
||||||
|
"Following a player to multiple threads to continue arguments",
|
||||||
|
"Repeatedly tagging or mentioning someone to provoke them",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
id="1.6",
|
||||||
|
name="Doxxing",
|
||||||
|
category=RuleCategory.CONDUCT,
|
||||||
|
description="Revealing or threatening to reveal real-life personal information about another player without their consent.",
|
||||||
|
default_severity=Severity.CRITICAL,
|
||||||
|
examples=(
|
||||||
|
"Posting someone's real name without consent",
|
||||||
|
"Threatening to reveal personal information",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
id="1.7",
|
||||||
|
name="Racism & Discrimination",
|
||||||
|
category=RuleCategory.CONDUCT,
|
||||||
|
description="Any form of discrimination based on race, ethnicity, national origin, or similar characteristics.",
|
||||||
|
default_severity=Severity.CRITICAL,
|
||||||
|
examples=(
|
||||||
|
"Racial slurs or epithets",
|
||||||
|
"Stereotyping based on ethnicity or national origin",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
id="1.8",
|
||||||
|
name="Hate Speech",
|
||||||
|
category=RuleCategory.CONDUCT,
|
||||||
|
description="Speech that attacks a person or group on the basis of protected attributes such as race, religion, ethnic origin, national origin, sex, disability, sexual orientation, or gender identity.",
|
||||||
|
default_severity=Severity.CRITICAL,
|
||||||
|
examples=(
|
||||||
|
"Slurs targeting protected groups",
|
||||||
|
"Advocating violence against protected groups",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
id="1.9",
|
||||||
|
name="Sexism",
|
||||||
|
category=RuleCategory.CONDUCT,
|
||||||
|
description="Discrimination or prejudice based on sex or gender.",
|
||||||
|
default_severity=Severity.HIGH,
|
||||||
|
examples=(
|
||||||
|
"Demeaning comments based on gender",
|
||||||
|
"Sexist stereotypes or assumptions",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
id="1.10",
|
||||||
|
name="Spamming",
|
||||||
|
category=RuleCategory.CONDUCT,
|
||||||
|
description="Posting the same or similar content repeatedly, or posting content with no meaningful contribution to discussion.",
|
||||||
|
default_severity=Severity.LOW,
|
||||||
|
examples=(
|
||||||
|
"Posting the same message in multiple threads",
|
||||||
|
"Repetitive one-word responses",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
id="1.11",
|
||||||
|
name="Bumping",
|
||||||
|
category=RuleCategory.CONDUCT,
|
||||||
|
description="Posting simply to move a thread to the top of the forum without adding meaningful content.",
|
||||||
|
default_severity=Severity.LOW,
|
||||||
|
examples=(
|
||||||
|
"Posts that just say 'bump'",
|
||||||
|
"Empty or meaningless posts to keep thread visible",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
id="1.12",
|
||||||
|
name="Off-Topic Posting",
|
||||||
|
category=RuleCategory.CONDUCT,
|
||||||
|
description="Posting content that is not relevant to the thread topic or forum section.",
|
||||||
|
default_severity=Severity.LOW,
|
||||||
|
examples=(
|
||||||
|
"Discussing unrelated games in EVE forums",
|
||||||
|
"Derailing discussions with unrelated content",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
id="1.13",
|
||||||
|
name="Pyramid Quoting",
|
||||||
|
category=RuleCategory.CONDUCT,
|
||||||
|
description="Excessive nested quoting that makes posts difficult to read and disrupts discussion flow.",
|
||||||
|
default_severity=Severity.LOW,
|
||||||
|
examples=("Quoting entire posts multiple levels deep",),
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
id="1.14",
|
||||||
|
name="Rumor Mongering",
|
||||||
|
category=RuleCategory.CONDUCT,
|
||||||
|
description="Spreading unverified information or speculation as fact, particularly regarding CCP decisions, policies, or future plans.",
|
||||||
|
default_severity=Severity.MEDIUM,
|
||||||
|
examples=(
|
||||||
|
"Presenting speculation about CCP decisions as fact",
|
||||||
|
"Spreading unverified claims about game changes",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
id="1.15",
|
||||||
|
name="New Player Bashing",
|
||||||
|
category=RuleCategory.CONDUCT,
|
||||||
|
description="Hostile or dismissive behavior toward new players asking questions or learning the game.",
|
||||||
|
default_severity=Severity.MEDIUM,
|
||||||
|
examples=(
|
||||||
|
"Mocking new players for asking basic questions",
|
||||||
|
"Telling new players to quit instead of helping",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
id="1.16",
|
||||||
|
name="Impersonation",
|
||||||
|
category=RuleCategory.CONDUCT,
|
||||||
|
description="Pretending to be another player, CCP employee, or ISD volunteer.",
|
||||||
|
default_severity=Severity.HIGH,
|
||||||
|
examples=(
|
||||||
|
"Claiming to be a CCP developer",
|
||||||
|
"Using names similar to known players to deceive",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
id="1.17",
|
||||||
|
name="Advertising",
|
||||||
|
category=RuleCategory.CONDUCT,
|
||||||
|
description="Promoting products, services, or websites unrelated to EVE Online.",
|
||||||
|
default_severity=Severity.MEDIUM,
|
||||||
|
examples=(
|
||||||
|
"Promoting external products or services",
|
||||||
|
"Posting referral links for non-EVE services",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Prohibited Content Rules (2.x)
|
||||||
|
CONTENT_RULES = (
|
||||||
|
Rule(
|
||||||
|
id="2.1",
|
||||||
|
name="Pornography",
|
||||||
|
category=RuleCategory.CONTENT,
|
||||||
|
description="Sexually explicit images or content.",
|
||||||
|
default_severity=Severity.CRITICAL,
|
||||||
|
examples=("Posting explicit images", "Linking to adult content"),
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
id="2.2",
|
||||||
|
name="Profanity",
|
||||||
|
category=RuleCategory.CONTENT,
|
||||||
|
description="Excessive or extreme profanity, particularly when directed at others.",
|
||||||
|
default_severity=Severity.MEDIUM,
|
||||||
|
examples=(
|
||||||
|
"Excessive swearing in posts",
|
||||||
|
"Using profanity as personal attacks",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
id="2.3",
|
||||||
|
name="Real Money Trading (RMT)",
|
||||||
|
category=RuleCategory.CONTENT,
|
||||||
|
description="Discussion of, solicitation for, or promotion of real money trading of in-game items, ISK, or accounts.",
|
||||||
|
default_severity=Severity.HIGH,
|
||||||
|
examples=(
|
||||||
|
"Offering to sell ISK for real money",
|
||||||
|
"Advertising account sales",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
id="2.4",
|
||||||
|
name="Discussion of Warnings & Bans",
|
||||||
|
category=RuleCategory.CONTENT,
|
||||||
|
description="Public discussion of warnings, bans, or other disciplinary actions taken against any player.",
|
||||||
|
default_severity=Severity.MEDIUM,
|
||||||
|
examples=(
|
||||||
|
"Complaining about receiving a warning",
|
||||||
|
"Discussing why another player was banned",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
id="2.5",
|
||||||
|
name="Discussion of Moderation",
|
||||||
|
category=RuleCategory.CONTENT,
|
||||||
|
description="Publicly discussing or disputing moderation decisions. Appeals should be handled through proper support channels.",
|
||||||
|
default_severity=Severity.MEDIUM,
|
||||||
|
examples=(
|
||||||
|
"Complaining about thread locks",
|
||||||
|
"Disputing moderator actions publicly",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
id="2.6",
|
||||||
|
name="Private Communications with CCP",
|
||||||
|
category=RuleCategory.CONTENT,
|
||||||
|
description="Sharing private communications with CCP staff without permission.",
|
||||||
|
default_severity=Severity.MEDIUM,
|
||||||
|
examples=(
|
||||||
|
"Posting GM responses",
|
||||||
|
"Sharing private CCP correspondence",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
id="2.7",
|
||||||
|
name="In-Game Bugs & Exploits",
|
||||||
|
category=RuleCategory.CONTENT,
|
||||||
|
description="Public discussion of bugs or exploits that could be abused. These should be reported through proper bug reporting channels.",
|
||||||
|
default_severity=Severity.HIGH,
|
||||||
|
examples=(
|
||||||
|
"Describing how to reproduce an exploit",
|
||||||
|
"Sharing details of game bugs publicly",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
id="2.8",
|
||||||
|
name="Real World Religion",
|
||||||
|
category=RuleCategory.CONTENT,
|
||||||
|
description="Discussion of real-world religious beliefs or practices.",
|
||||||
|
default_severity=Severity.MEDIUM,
|
||||||
|
examples=(
|
||||||
|
"Discussing real-world religious topics",
|
||||||
|
"Religious debates unrelated to EVE lore",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
id="2.9",
|
||||||
|
name="Real World Politics",
|
||||||
|
category=RuleCategory.CONTENT,
|
||||||
|
description="Discussion of real-world political topics, parties, or figures.",
|
||||||
|
default_severity=Severity.MEDIUM,
|
||||||
|
examples=(
|
||||||
|
"Discussing real-world elections",
|
||||||
|
"Political debates unrelated to EVE",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
id="2.10",
|
||||||
|
name="Layout-Distorting Content",
|
||||||
|
category=RuleCategory.CONTENT,
|
||||||
|
description="Images, text, or formatting that disrupts the normal forum layout.",
|
||||||
|
default_severity=Severity.LOW,
|
||||||
|
examples=(
|
||||||
|
"Oversized images",
|
||||||
|
"Formatting that breaks page layout",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
id="2.11",
|
||||||
|
name="Advertising Other Games/Services",
|
||||||
|
category=RuleCategory.CONTENT,
|
||||||
|
description="Promoting other games or competing services.",
|
||||||
|
default_severity=Severity.MEDIUM,
|
||||||
|
examples=(
|
||||||
|
"Promoting competing MMOs",
|
||||||
|
"Advertising other gaming services",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
id="2.12",
|
||||||
|
name="Real Financial Transfer Requests",
|
||||||
|
category=RuleCategory.CONTENT,
|
||||||
|
description="Soliciting real money from other players.",
|
||||||
|
default_severity=Severity.HIGH,
|
||||||
|
examples=(
|
||||||
|
"Asking for PayPal donations",
|
||||||
|
"GoFundMe or similar solicitations",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Other Rules (3-9)
|
||||||
|
OTHER_RULES = (
|
||||||
|
Rule(
|
||||||
|
id="3",
|
||||||
|
name="Non-Constructive Posting",
|
||||||
|
category=RuleCategory.OTHER,
|
||||||
|
description="Posts should contribute positively to discussion. Posts that exist solely to complain, mock, or derail without offering constructive feedback may be moderated.",
|
||||||
|
default_severity=Severity.LOW,
|
||||||
|
examples=(
|
||||||
|
"Posts that only mock without substance",
|
||||||
|
"Pure negativity without constructive criticism",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
id="4",
|
||||||
|
name="Abuse of Forum Tools",
|
||||||
|
category=RuleCategory.OTHER,
|
||||||
|
description="Misusing forum features such as flagging, voting, or reporting in bad faith.",
|
||||||
|
default_severity=Severity.MEDIUM,
|
||||||
|
examples=(
|
||||||
|
"Mass flagging posts you disagree with",
|
||||||
|
"Coordinated abuse of voting systems",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
id="5",
|
||||||
|
name="Re-Opening Locked Topics",
|
||||||
|
category=RuleCategory.OTHER,
|
||||||
|
description="Creating new threads to continue discussion from threads that have been locked by moderators.",
|
||||||
|
default_severity=Severity.MEDIUM,
|
||||||
|
examples=("Creating new thread to continue locked discussion",),
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
id="6",
|
||||||
|
name="Language Requirements",
|
||||||
|
category=RuleCategory.OTHER,
|
||||||
|
description="Posts in English-language forum sections must be in English. Other language sections have their own requirements.",
|
||||||
|
default_severity=Severity.LOW,
|
||||||
|
examples=("Posting in non-English in English sections",),
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
id="7",
|
||||||
|
name="Attacking CCP/ISD Staff",
|
||||||
|
category=RuleCategory.OTHER,
|
||||||
|
description="Personal attacks or harassment directed at CCP staff or ISD volunteers is strictly prohibited.",
|
||||||
|
default_severity=Severity.CRITICAL,
|
||||||
|
examples=(
|
||||||
|
"Insulting CCP developers personally",
|
||||||
|
"Harassing ISD volunteers",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
id="8",
|
||||||
|
name="Section-Specific Rules",
|
||||||
|
category=RuleCategory.OTHER,
|
||||||
|
description="Individual forum sections may have additional rules that must be followed.",
|
||||||
|
default_severity=Severity.LOW,
|
||||||
|
examples=("Violating Marketplace section rules",),
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
id="9",
|
||||||
|
name="Killmail/Chatlog Abuse",
|
||||||
|
category=RuleCategory.OTHER,
|
||||||
|
description="Using killmails or chat logs primarily to troll, flame, or harass other players rather than for legitimate discussion.",
|
||||||
|
default_severity=Severity.MEDIUM,
|
||||||
|
examples=(
|
||||||
|
"Posting killmails solely to mock someone",
|
||||||
|
"Sharing chat logs to harass",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# All rules combined
|
||||||
|
ALL_RULES: tuple[Rule, ...] = CONDUCT_RULES + CONTENT_RULES + OTHER_RULES
|
||||||
|
|
||||||
|
# Quick lookup by ID
|
||||||
|
RULES_BY_ID: dict[str, Rule] = {rule.id: rule for rule in ALL_RULES}
|
||||||
|
|
||||||
|
|
||||||
|
def get_rule(rule_id: str) -> Rule | None:
|
||||||
|
"""Get a rule by its ID."""
|
||||||
|
return RULES_BY_ID.get(rule_id)
|
||||||
|
|
||||||
|
|
||||||
|
def format_rules_for_prompt() -> str:
|
||||||
|
"""Format all rules as a string suitable for LLM system prompts."""
|
||||||
|
lines = ["# EVE Online Forum Moderation Rules\n"]
|
||||||
|
|
||||||
|
lines.append("## Prohibited Conduct (Rules 1.x)\n")
|
||||||
|
for rule in CONDUCT_RULES:
|
||||||
|
lines.append(f"### {rule.id} - {rule.name}")
|
||||||
|
lines.append(f"{rule.description}")
|
||||||
|
lines.append(f"Default severity: {rule.default_severity.value.upper()}\n")
|
||||||
|
|
||||||
|
lines.append("## Prohibited Content (Rules 2.x)\n")
|
||||||
|
for rule in CONTENT_RULES:
|
||||||
|
lines.append(f"### {rule.id} - {rule.name}")
|
||||||
|
lines.append(f"{rule.description}")
|
||||||
|
lines.append(f"Default severity: {rule.default_severity.value.upper()}\n")
|
||||||
|
|
||||||
|
lines.append("## Other Rules (Rules 3-9)\n")
|
||||||
|
for rule in OTHER_RULES:
|
||||||
|
lines.append(f"### {rule.id} - {rule.name}")
|
||||||
|
lines.append(f"{rule.description}")
|
||||||
|
lines.append(f"Default severity: {rule.default_severity.value.upper()}\n")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def load_policy_markdown() -> str:
|
||||||
|
"""Load the full policy markdown file."""
|
||||||
|
policy_path = Path(__file__).parent.parent.parent / "rules" / "forum_policy.md"
|
||||||
|
if policy_path.exists():
|
||||||
|
return policy_path.read_text()
|
||||||
|
# Fallback to generated format
|
||||||
|
return format_rules_for_prompt()
|
||||||
461
uv.lock
generated
Normal file
461
uv.lock
generated
Normal file
@@ -0,0 +1,461 @@
|
|||||||
|
version = 1
|
||||||
|
revision = 3
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "annotated-types"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anthropic"
|
||||||
|
version = "0.75.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "anyio" },
|
||||||
|
{ name = "distro" },
|
||||||
|
{ name = "docstring-parser" },
|
||||||
|
{ name = "httpx" },
|
||||||
|
{ name = "jiter" },
|
||||||
|
{ name = "pydantic" },
|
||||||
|
{ name = "sniffio" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/04/1f/08e95f4b7e2d35205ae5dcbb4ae97e7d477fc521c275c02609e2931ece2d/anthropic-0.75.0.tar.gz", hash = "sha256:e8607422f4ab616db2ea5baacc215dd5f028da99ce2f022e33c7c535b29f3dfb", size = 439565, upload-time = "2025-11-24T20:41:45.28Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/1c/1cd02b7ae64302a6e06724bf80a96401d5313708651d277b1458504a1730/anthropic-0.75.0-py3-none-any.whl", hash = "sha256:ea8317271b6c15d80225a9f3c670152746e88805a7a61e14d4a374577164965b", size = 388164, upload-time = "2025-11-24T20:41:43.587Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyio"
|
||||||
|
version = "4.12.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "idna" },
|
||||||
|
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266, upload-time = "2025-11-28T23:37:38.911Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "certifi"
|
||||||
|
version = "2026.1.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "click"
|
||||||
|
version = "8.3.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "distro"
|
||||||
|
version = "1.9.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "docstring-parser"
|
||||||
|
version = "0.17.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "eve-forum-moderator"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { editable = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "anthropic" },
|
||||||
|
{ name = "click" },
|
||||||
|
{ name = "httpx" },
|
||||||
|
{ name = "python-dateutil" },
|
||||||
|
{ name = "python-dotenv" },
|
||||||
|
{ name = "rich" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "anthropic", specifier = ">=0.30" },
|
||||||
|
{ name = "click", specifier = ">=8.1" },
|
||||||
|
{ name = "httpx", specifier = ">=0.27" },
|
||||||
|
{ name = "python-dateutil", specifier = ">=2.9" },
|
||||||
|
{ name = "python-dotenv", specifier = ">=1.0" },
|
||||||
|
{ name = "rich", specifier = ">=13.0" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "h11"
|
||||||
|
version = "0.16.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpcore"
|
||||||
|
version = "1.0.9"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "certifi" },
|
||||||
|
{ name = "h11" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpx"
|
||||||
|
version = "0.28.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "anyio" },
|
||||||
|
{ name = "certifi" },
|
||||||
|
{ name = "httpcore" },
|
||||||
|
{ name = "idna" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna"
|
||||||
|
version = "3.11"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
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" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jiter"
|
||||||
|
version = "0.12.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294, upload-time = "2025-11-09T20:49:23.302Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/32/f9/eaca4633486b527ebe7e681c431f529b63fe2709e7c5242fc0f43f77ce63/jiter-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8f8a7e317190b2c2d60eb2e8aa835270b008139562d70fe732e1c0020ec53c9", size = 316435, upload-time = "2025-11-09T20:47:02.087Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/c1/40c9f7c22f5e6ff715f28113ebaba27ab85f9af2660ad6e1dd6425d14c19/jiter-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2218228a077e784c6c8f1a8e5d6b8cb1dea62ce25811c356364848554b2056cd", size = 320548, upload-time = "2025-11-09T20:47:03.409Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6b/1b/efbb68fe87e7711b00d2cfd1f26bb4bfc25a10539aefeaa7727329ffb9cb/jiter-0.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9354ccaa2982bf2188fd5f57f79f800ef622ec67beb8329903abf6b10da7d423", size = 351915, upload-time = "2025-11-09T20:47:05.171Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/2d/c06e659888c128ad1e838123d0638f0efad90cc30860cb5f74dd3f2fc0b3/jiter-0.12.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2607185ea89b4af9a604d4c7ec40e45d3ad03ee66998b031134bc510232bb7", size = 368966, upload-time = "2025-11-09T20:47:06.508Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6b/20/058db4ae5fb07cf6a4ab2e9b9294416f606d8e467fb74c2184b2a1eeacba/jiter-0.12.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a585a5e42d25f2e71db5f10b171f5e5ea641d3aa44f7df745aa965606111cc2", size = 482047, upload-time = "2025-11-09T20:47:08.382Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/bb/dc2b1c122275e1de2eb12905015d61e8316b2f888bdaac34221c301495d6/jiter-0.12.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd9e21d34edff5a663c631f850edcb786719c960ce887a5661e9c828a53a95d9", size = 380835, upload-time = "2025-11-09T20:47:09.81Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/7d/38f9cd337575349de16da575ee57ddb2d5a64d425c9367f5ef9e4612e32e/jiter-0.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a612534770470686cd5431478dc5a1b660eceb410abade6b1b74e320ca98de6", size = 364587, upload-time = "2025-11-09T20:47:11.529Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/a3/b13e8e61e70f0bb06085099c4e2462647f53cc2ca97614f7fedcaa2bb9f3/jiter-0.12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3985aea37d40a908f887b34d05111e0aae822943796ebf8338877fee2ab67725", size = 390492, upload-time = "2025-11-09T20:47:12.993Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/71/e0d11422ed027e21422f7bc1883c61deba2d9752b720538430c1deadfbca/jiter-0.12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b1207af186495f48f72529f8d86671903c8c10127cac6381b11dddc4aaa52df6", size = 522046, upload-time = "2025-11-09T20:47:14.6Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9f/59/b968a9aa7102a8375dbbdfbd2aeebe563c7e5dddf0f47c9ef1588a97e224/jiter-0.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef2fb241de583934c9915a33120ecc06d94aa3381a134570f59eed784e87001e", size = 513392, upload-time = "2025-11-09T20:47:16.011Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ca/e4/7df62002499080dbd61b505c5cb351aa09e9959d176cac2aa8da6f93b13b/jiter-0.12.0-cp311-cp311-win32.whl", hash = "sha256:453b6035672fecce8007465896a25b28a6b59cfe8fbc974b2563a92f5a92a67c", size = 206096, upload-time = "2025-11-09T20:47:17.344Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bb/60/1032b30ae0572196b0de0e87dce3b6c26a1eff71aad5fe43dee3082d32e0/jiter-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:ca264b9603973c2ad9435c71a8ec8b49f8f715ab5ba421c85a51cde9887e421f", size = 204899, upload-time = "2025-11-09T20:47:19.365Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/d5/c145e526fccdb834063fb45c071df78b0cc426bbaf6de38b0781f45d956f/jiter-0.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:cb00ef392e7d684f2754598c02c409f376ddcef857aae796d559e6cacc2d78a5", size = 188070, upload-time = "2025-11-09T20:47:20.75Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/c9/5b9f7b4983f1b542c64e84165075335e8a236fa9e2ea03a0c79780062be8/jiter-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:305e061fa82f4680607a775b2e8e0bcb071cd2205ac38e6ef48c8dd5ebe1cf37", size = 314449, upload-time = "2025-11-09T20:47:22.999Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/6e/e8efa0e78de00db0aee82c0cf9e8b3f2027efd7f8a71f859d8f4be8e98ef/jiter-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c1860627048e302a528333c9307c818c547f214d8659b0705d2195e1a94b274", size = 319855, upload-time = "2025-11-09T20:47:24.779Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/26/894cd88e60b5d58af53bec5c6759d1292bd0b37a8b5f60f07abf7a63ae5f/jiter-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df37577a4f8408f7e0ec3205d2a8f87672af8f17008358063a4d6425b6081ce3", size = 350171, upload-time = "2025-11-09T20:47:26.469Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f5/27/a7b818b9979ac31b3763d25f3653ec3a954044d5e9f5d87f2f247d679fd1/jiter-0.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fdd787356c1c13a4f40b43c2156276ef7a71eb487d98472476476d803fb2cf", size = 365590, upload-time = "2025-11-09T20:47:27.918Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/7e/e46195801a97673a83746170b17984aa8ac4a455746354516d02ca5541b4/jiter-0.12.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1eb5db8d9c65b112aacf14fcd0faae9913d07a8afea5ed06ccdd12b724e966a1", size = 479462, upload-time = "2025-11-09T20:47:29.654Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ca/75/f833bfb009ab4bd11b1c9406d333e3b4357709ed0570bb48c7c06d78c7dd/jiter-0.12.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73c568cc27c473f82480abc15d1301adf333a7ea4f2e813d6a2c7d8b6ba8d0df", size = 378983, upload-time = "2025-11-09T20:47:31.026Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/71/b3/7a69d77943cc837d30165643db753471aff5df39692d598da880a6e51c24/jiter-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4321e8a3d868919bcb1abb1db550d41f2b5b326f72df29e53b2df8b006eb9403", size = 361328, upload-time = "2025-11-09T20:47:33.286Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b0/ac/a78f90caf48d65ba70d8c6efc6f23150bc39dc3389d65bbec2a95c7bc628/jiter-0.12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a51bad79f8cc9cac2b4b705039f814049142e0050f30d91695a2d9a6611f126", size = 386740, upload-time = "2025-11-09T20:47:34.703Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/39/b6/5d31c2cc8e1b6a6bcf3c5721e4ca0a3633d1ab4754b09bc7084f6c4f5327/jiter-0.12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a67b678f6a5f1dd6c36d642d7db83e456bc8b104788262aaefc11a22339f5a9", size = 520875, upload-time = "2025-11-09T20:47:36.058Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/b5/4df540fae4e9f68c54b8dab004bd8c943a752f0b00efd6e7d64aa3850339/jiter-0.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efe1a211fe1fd14762adea941e3cfd6c611a136e28da6c39272dbb7a1bbe6a86", size = 511457, upload-time = "2025-11-09T20:47:37.932Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/65/86b74010e450a1a77b2c1aabb91d4a91dd3cd5afce99f34d75fd1ac64b19/jiter-0.12.0-cp312-cp312-win32.whl", hash = "sha256:d779d97c834b4278276ec703dc3fc1735fca50af63eb7262f05bdb4e62203d44", size = 204546, upload-time = "2025-11-09T20:47:40.47Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1c/c7/6659f537f9562d963488e3e55573498a442503ced01f7e169e96a6110383/jiter-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e8269062060212b373316fe69236096aaf4c49022d267c6736eebd66bbbc60bb", size = 205196, upload-time = "2025-11-09T20:47:41.794Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/21/f4/935304f5169edadfec7f9c01eacbce4c90bb9a82035ac1de1f3bd2d40be6/jiter-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:06cb970936c65de926d648af0ed3d21857f026b1cf5525cb2947aa5e01e05789", size = 186100, upload-time = "2025-11-09T20:47:43.007Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/a6/97209693b177716e22576ee1161674d1d58029eb178e01866a0422b69224/jiter-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6cc49d5130a14b732e0612bc76ae8db3b49898732223ef8b7599aa8d9810683e", size = 313658, upload-time = "2025-11-09T20:47:44.424Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/06/4d/125c5c1537c7d8ee73ad3d530a442d6c619714b95027143f1b61c0b4dfe0/jiter-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37f27a32ce36364d2fa4f7fdc507279db604d27d239ea2e044c8f148410defe1", size = 318605, upload-time = "2025-11-09T20:47:45.973Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/99/bf/a840b89847885064c41a5f52de6e312e91fa84a520848ee56c97e4fa0205/jiter-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc0944aa3d4b4773e348cda635252824a78f4ba44328e042ef1ff3f6080d1cf", size = 349803, upload-time = "2025-11-09T20:47:47.535Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/88/e63441c28e0db50e305ae23e19c1d8fae012d78ed55365da392c1f34b09c/jiter-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da25c62d4ee1ffbacb97fac6dfe4dcd6759ebdc9015991e92a6eae5816287f44", size = 365120, upload-time = "2025-11-09T20:47:49.284Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0a/7c/49b02714af4343970eb8aca63396bc1c82fa01197dbb1e9b0d274b550d4e/jiter-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:048485c654b838140b007390b8182ba9774621103bd4d77c9c3f6f117474ba45", size = 479918, upload-time = "2025-11-09T20:47:50.807Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/69/ba/0a809817fdd5a1db80490b9150645f3aae16afad166960bcd562be194f3b/jiter-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:635e737fbb7315bef0037c19b88b799143d2d7d3507e61a76751025226b3ac87", size = 379008, upload-time = "2025-11-09T20:47:52.211Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5f/c3/c9fc0232e736c8877d9e6d83d6eeb0ba4e90c6c073835cc2e8f73fdeef51/jiter-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e017c417b1ebda911bd13b1e40612704b1f5420e30695112efdbed8a4b389ed", size = 361785, upload-time = "2025-11-09T20:47:53.512Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/96/61/61f69b7e442e97ca6cd53086ddc1cf59fb830549bc72c0a293713a60c525/jiter-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:89b0bfb8b2bf2351fba36bb211ef8bfceba73ef58e7f0c68fb67b5a2795ca2f9", size = 386108, upload-time = "2025-11-09T20:47:54.893Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/2e/76bb3332f28550c8f1eba3bf6e5efe211efda0ddbbaf24976bc7078d42a5/jiter-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f5aa5427a629a824a543672778c9ce0c5e556550d1569bb6ea28a85015287626", size = 519937, upload-time = "2025-11-09T20:47:56.253Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/84/d6/fa96efa87dc8bff2094fb947f51f66368fa56d8d4fc9e77b25d7fbb23375/jiter-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed53b3d6acbcb0fd0b90f20c7cb3b24c357fe82a3518934d4edfa8c6898e498c", size = 510853, upload-time = "2025-11-09T20:47:58.32Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/28/93f67fdb4d5904a708119a6ab58a8f1ec226ff10a94a282e0215402a8462/jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de", size = 204699, upload-time = "2025-11-09T20:47:59.686Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c4/1f/30b0eb087045a0abe2a5c9c0c0c8da110875a1d3be83afd4a9a4e548be3c/jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a", size = 204258, upload-time = "2025-11-09T20:48:01.01Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/f4/2b4daf99b96bce6fc47971890b14b2a36aef88d7beb9f057fafa032c6141/jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60", size = 185503, upload-time = "2025-11-09T20:48:02.35Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/39/ca/67bb15a7061d6fe20b9b2a2fd783e296a1e0f93468252c093481a2f00efa/jiter-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53839b35a38f56b8be26a7851a48b89bc47e5d88e900929df10ed93b95fea3d6", size = 317965, upload-time = "2025-11-09T20:48:03.783Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/af/1788031cd22e29c3b14bc6ca80b16a39a0b10e611367ffd480c06a259831/jiter-0.12.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f669548e55c91ab47fef8bddd9c954dab1938644e715ea49d7e117015110a4", size = 345831, upload-time = "2025-11-09T20:48:05.55Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/17/710bf8472d1dff0d3caf4ced6031060091c1320f84ee7d5dcbed1f352417/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb", size = 361272, upload-time = "2025-11-09T20:48:06.951Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fb/f1/1dcc4618b59761fef92d10bcbb0b038b5160be653b003651566a185f1a5c/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604, upload-time = "2025-11-09T20:48:08.328Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/32/63cb1d9f1c5c6632a783c0052cde9ef7ba82688f7065e2f0d5f10a7e3edb/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628, upload-time = "2025-11-09T20:48:09.572Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a8/99/45c9f0dbe4a1416b2b9a8a6d1236459540f43d7fb8883cff769a8db0612d/jiter-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c46d927acd09c67a9fb1416df45c5a04c27e83aae969267e98fba35b74e99525", size = 312478, upload-time = "2025-11-09T20:48:10.898Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4c/a7/54ae75613ba9e0f55fcb0bc5d1f807823b5167cc944e9333ff322e9f07dd/jiter-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:774ff60b27a84a85b27b88cd5583899c59940bcc126caca97eb2a9df6aa00c49", size = 318706, upload-time = "2025-11-09T20:48:12.266Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/59/31/2aa241ad2c10774baf6c37f8b8e1f39c07db358f1329f4eb40eba179c2a2/jiter-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5433fab222fb072237df3f637d01b81f040a07dcac1cb4a5c75c7aa9ed0bef1", size = 351894, upload-time = "2025-11-09T20:48:13.673Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/4f/0f2759522719133a9042781b18cc94e335b6d290f5e2d3e6899d6af933e3/jiter-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8c593c6e71c07866ec6bfb790e202a833eeec885022296aff6b9e0b92d6a70e", size = 365714, upload-time = "2025-11-09T20:48:15.083Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/6f/806b895f476582c62a2f52c453151edd8a0fde5411b0497baaa41018e878/jiter-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90d32894d4c6877a87ae00c6b915b609406819dce8bc0d4e962e4de2784e567e", size = 478989, upload-time = "2025-11-09T20:48:16.706Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/6c/012d894dc6e1033acd8db2b8346add33e413ec1c7c002598915278a37f79/jiter-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:798e46eed9eb10c3adbbacbd3bdb5ecd4cf7064e453d00dbef08802dae6937ff", size = 378615, upload-time = "2025-11-09T20:48:18.614Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/87/30/d718d599f6700163e28e2c71c0bbaf6dace692e7df2592fd793ac9276717/jiter-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3f1368f0a6719ea80013a4eb90ba72e75d7ea67cfc7846db2ca504f3df0169a", size = 364745, upload-time = "2025-11-09T20:48:20.117Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8f/85/315b45ce4b6ddc7d7fceca24068543b02bdc8782942f4ee49d652e2cc89f/jiter-0.12.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f04a9d0b4406f7e51279710b27484af411896246200e461d80d3ba0caa901a", size = 386502, upload-time = "2025-11-09T20:48:21.543Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/0b/ce0434fb40c5b24b368fe81b17074d2840748b4952256bab451b72290a49/jiter-0.12.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fd990541982a24281d12b67a335e44f117e4c6cbad3c3b75c7dea68bf4ce3a67", size = 519845, upload-time = "2025-11-09T20:48:22.964Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/a3/7a7a4488ba052767846b9c916d208b3ed114e3eb670ee984e4c565b9cf0d/jiter-0.12.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b111b0e9152fa7df870ecaebb0bd30240d9f7fff1f2003bcb4ed0f519941820b", size = 510701, upload-time = "2025-11-09T20:48:24.483Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c3/16/052ffbf9d0467b70af24e30f91e0579e13ded0c17bb4a8eb2aed3cb60131/jiter-0.12.0-cp314-cp314-win32.whl", hash = "sha256:a78befb9cc0a45b5a5a0d537b06f8544c2ebb60d19d02c41ff15da28a9e22d42", size = 205029, upload-time = "2025-11-09T20:48:25.749Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e4/18/3cf1f3f0ccc789f76b9a754bdb7a6977e5d1d671ee97a9e14f7eb728d80e/jiter-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:e1fe01c082f6aafbe5c8faf0ff074f38dfb911d53f07ec333ca03f8f6226debf", size = 204960, upload-time = "2025-11-09T20:48:27.415Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/68/736821e52ecfdeeb0f024b8ab01b5a229f6b9293bbdb444c27efade50b0f/jiter-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:d72f3b5a432a4c546ea4bedc84cce0c3404874f1d1676260b9c7f048a9855451", size = 185529, upload-time = "2025-11-09T20:48:29.125Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/61/12ed8ee7a643cce29ac97c2281f9ce3956eb76b037e88d290f4ed0d41480/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6ded41aeba3603f9728ed2b6196e4df875348ab97b28fc8afff115ed42ba7a7", size = 318974, upload-time = "2025-11-09T20:48:30.87Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2d/c6/f3041ede6d0ed5e0e79ff0de4c8f14f401bbf196f2ef3971cdbe5fd08d1d/jiter-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a947920902420a6ada6ad51892082521978e9dd44a802663b001436e4b771684", size = 345932, upload-time = "2025-11-09T20:48:32.658Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/5d/4d94835889edd01ad0e2dbfc05f7bdfaed46292e7b504a6ac7839aa00edb/jiter-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add5e227e0554d3a52cf390a7635edaffdf4f8fce4fdbcef3cc2055bb396a30c", size = 367243, upload-time = "2025-11-09T20:48:34.093Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/76/0051b0ac2816253a99d27baf3dda198663aff882fa6ea7deeb94046da24e/jiter-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9b1cda8fcb736250d7e8711d4580ebf004a46771432be0ae4796944b5dfa5d", size = 479315, upload-time = "2025-11-09T20:48:35.507Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/ae/83f793acd68e5cb24e483f44f482a1a15601848b9b6f199dacb970098f77/jiter-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb12a2223fe0135c7ff1356a143d57f95bbf1f4a66584f1fc74df21d86b993", size = 380714, upload-time = "2025-11-09T20:48:40.014Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/5e/4808a88338ad2c228b1126b93fcd8ba145e919e886fe910d578230dabe3b/jiter-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c596cc0f4cb574877550ce4ecd51f8037469146addd676d7c1a30ebe6391923f", size = 365168, upload-time = "2025-11-09T20:48:41.462Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/d4/04619a9e8095b42aef436b5aeb4c0282b4ff1b27d1db1508df9f5dc82750/jiter-0.12.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ab4c823b216a4aeab3fdbf579c5843165756bd9ad87cc6b1c65919c4715f783", size = 387893, upload-time = "2025-11-09T20:48:42.921Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/17/ea/d3c7e62e4546fdc39197fa4a4315a563a89b95b6d54c0d25373842a59cbe/jiter-0.12.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e427eee51149edf962203ff8db75a7514ab89be5cb623fb9cea1f20b54f1107b", size = 520828, upload-time = "2025-11-09T20:48:44.278Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/0b/c6d3562a03fd767e31cb119d9041ea7958c3c80cb3d753eafb19b3b18349/jiter-0.12.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:edb868841f84c111255ba5e80339d386d937ec1fdce419518ce1bd9370fac5b6", size = 511009, upload-time = "2025-11-09T20:48:45.726Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/51/2cb4468b3448a8385ebcd15059d325c9ce67df4e2758d133ab9442b19834/jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183", size = 205110, upload-time = "2025-11-09T20:48:47.033Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/c5/ae5ec83dec9c2d1af805fd5fe8f74ebded9c8670c5210ec7820ce0dbeb1e/jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873", size = 205223, upload-time = "2025-11-09T20:48:49.076Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/9a/3c5391907277f0e55195550cf3fa8e293ae9ee0c00fb402fec1e38c0c82f/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564, upload-time = "2025-11-09T20:48:50.376Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fe/54/5339ef1ecaa881c6948669956567a64d2670941925f245c434f494ffb0e5/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:4739a4657179ebf08f85914ce50332495811004cc1747852e8b2041ed2aab9b8", size = 311144, upload-time = "2025-11-09T20:49:10.503Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/27/74/3446c652bffbd5e81ab354e388b1b5fc1d20daac34ee0ed11ff096b1b01a/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:41da8def934bf7bec16cb24bd33c0ca62126d2d45d81d17b864bd5ad721393c3", size = 305877, upload-time = "2025-11-09T20:49:12.269Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a1/f4/ed76ef9043450f57aac2d4fbeb27175aa0eb9c38f833be6ef6379b3b9a86/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c44ee814f499c082e69872d426b624987dbc5943ab06e9bbaa4f81989fdb79e", size = 340419, upload-time = "2025-11-09T20:49:13.803Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/21/01/857d4608f5edb0664aa791a3d45702e1a5bcfff9934da74035e7b9803846/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd2097de91cf03eaa27b3cbdb969addf83f0179c6afc41bbc4513705e013c65d", size = 347212, upload-time = "2025-11-09T20:49:15.643Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/f5/12efb8ada5f5c9edc1d4555fe383c1fb2eac05ac5859258a72d61981d999/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:e8547883d7b96ef2e5fe22b88f8a4c8725a56e7f4abafff20fd5272d634c7ecb", size = 309974, upload-time = "2025-11-09T20:49:17.187Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/85/15/d6eb3b770f6a0d332675141ab3962fd4a7c270ede3515d9f3583e1d28276/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:89163163c0934854a668ed783a2546a0617f71706a2551a4a0666d91ab365d6b", size = 304233, upload-time = "2025-11-09T20:49:18.734Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8c/3e/e7e06743294eea2cf02ced6aa0ff2ad237367394e37a0e2b4a1108c67a36/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d96b264ab7d34bbb2312dedc47ce07cd53f06835eacbc16dde3761f47c3a9e7f", size = 338537, upload-time = "2025-11-09T20:49:20.317Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2f/9c/6753e6522b8d0ef07d3a3d239426669e984fb0eba15a315cdbc1253904e4/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c", size = 346110, upload-time = "2025-11-09T20:49:21.817Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "markdown-it-py"
|
||||||
|
version = "4.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "mdurl" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mdurl"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pydantic"
|
||||||
|
version = "2.12.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "annotated-types" },
|
||||||
|
{ name = "pydantic-core" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
{ name = "typing-inspection" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pydantic-core"
|
||||||
|
version = "2.41.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pygments"
|
||||||
|
version = "2.19.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-dateutil"
|
||||||
|
version = "2.9.0.post0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "six" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
|
||||||
|
wheels = [
|
||||||
|
{ 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" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-dotenv"
|
||||||
|
version = "1.2.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
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" }
|
||||||
|
wheels = [
|
||||||
|
{ 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" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rich"
|
||||||
|
version = "14.2.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "markdown-it-py" },
|
||||||
|
{ name = "pygments" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "six"
|
||||||
|
version = "1.17.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sniffio"
|
||||||
|
version = "1.3.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.15.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-inspection"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user