Added version 4
This commit is contained in:
BIN
jokes_bot/.DS_Store
vendored
Normal file
BIN
jokes_bot/.DS_Store
vendored
Normal file
Binary file not shown.
207
jokes_bot/README.md
Normal file
207
jokes_bot/README.md
Normal file
@@ -0,0 +1,207 @@
|
||||
# Step-by-Step Complete Setup Guide
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before starting, ensure you have Python 3.x installed on your system.
|
||||
|
||||
## Check Your Python Installation
|
||||
|
||||
Open PowerShell or Terminal:
|
||||
|
||||
Press Windows + R, type `powershell`, press Enter (on Windows)
|
||||
|
||||
Check Python Version:
|
||||
|
||||
```bash
|
||||
python --version
|
||||
# Should show: Python 3.x.x
|
||||
|
||||
# If that doesn't work, try:
|
||||
python3 --version
|
||||
# Or on some systems:
|
||||
py --version
|
||||
```
|
||||
|
||||
## Create Your Project Folder
|
||||
|
||||
Organize your files properly from the start.
|
||||
|
||||
Open PowerShell, Terminal, or Command Prompt:
|
||||
|
||||
Make sure you're not in the Python shell (should see C:\> or PS C:\>).
|
||||
|
||||
Navigate to Desktop or Documents:
|
||||
|
||||
Let's create a folder on your Desktop for easy access:
|
||||
|
||||
```bash
|
||||
# Go to Desktop (Windows)
|
||||
cd Desktop
|
||||
|
||||
# Create a new folder for your project
|
||||
mkdir project_folder
|
||||
|
||||
# Go into your new folder
|
||||
cd project_folder
|
||||
|
||||
# Verify you're in the right place
|
||||
pwd
|
||||
# Should show: C:\Users\YourName\Desktop\project_folder
|
||||
# Or use 'ls' to see files (should be empty)
|
||||
ls
|
||||
```
|
||||
|
||||
**Best Practice:** Keep all related files in one folder for easy management.
|
||||
|
||||
## Set Up Virtual Environment (venv)
|
||||
|
||||
Isolate project dependencies for clean development.
|
||||
|
||||
### Why Virtual Environment?
|
||||
|
||||
- Keeps project dependencies separate
|
||||
- Avoids version conflicts between projects
|
||||
- Makes sharing and deployment easier
|
||||
|
||||
### Create Virtual Environment:
|
||||
|
||||
Make sure you're in your [project_folder](file:///Users/home/YandexDisk/TECHNOLYCEUM/ict/Year/2025/ai/ai6/ai6-m3/ai6-m3/jokes_bot/v4.0/database..py#L1-L45), then run:
|
||||
|
||||
```bash
|
||||
# Create virtual environment named 'venv'
|
||||
python -m venv venv
|
||||
|
||||
# or
|
||||
|
||||
python3 -m venv venv
|
||||
|
||||
# Check if venv folder was created
|
||||
ls
|
||||
# You should see a 'venv' folder in the list
|
||||
```
|
||||
|
||||
### Activate Virtual Environment:
|
||||
|
||||
#### On Windows (PowerShell):
|
||||
|
||||
```bash
|
||||
venv\Scripts\Activate.ps1
|
||||
# If you get an error about execution policy, run this first:
|
||||
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||
venv\Scripts\Activate.ps1
|
||||
```
|
||||
|
||||
#### On Windows (Command Prompt):
|
||||
|
||||
```cmd
|
||||
venv\Scripts\activate
|
||||
```
|
||||
|
||||
#### On Mac/Linux:
|
||||
|
||||
```bash
|
||||
source venv/bin/activate
|
||||
```
|
||||
|
||||
**Success Indicator:** You should see `(venv)` at the start of your command line.
|
||||
|
||||
## Install Required Libraries
|
||||
|
||||
Get the Python packages needed for this project.
|
||||
|
||||
First, activate your venv:
|
||||
|
||||
Make sure you see `(venv)` before the prompt.
|
||||
|
||||
Install required packages:
|
||||
|
||||
```bash
|
||||
# Install the required library
|
||||
pip install package-name==version.number
|
||||
|
||||
# Verify installation
|
||||
pip show package-name
|
||||
# Should show the installed version
|
||||
```
|
||||
|
||||
Create Requirements File:
|
||||
|
||||
```bash
|
||||
pip freeze > requirements.txt
|
||||
|
||||
# View the requirements file
|
||||
type requirements.txt
|
||||
# (Mac/Linux: use 'cat requirements.txt' instead of 'type')
|
||||
```
|
||||
|
||||
**Why this matters:** Anyone can install exact same versions with `pip install -r requirements.txt`
|
||||
|
||||
## Using IDLE Editor
|
||||
|
||||
To open Python IDLE editor:
|
||||
|
||||
### On Windows:
|
||||
|
||||
```bash
|
||||
# Launch IDLE (blank new file)
|
||||
py -m idlelib.idle
|
||||
|
||||
# OR (depending on your Python installation)
|
||||
python -m idlelib.idle
|
||||
|
||||
# Open an existing file
|
||||
py -m idlelib.idle "C:\path\to\your\file.py"
|
||||
```
|
||||
|
||||
### On Mac:
|
||||
|
||||
```bash
|
||||
# Launch IDLE (blank new file)
|
||||
python3 -m idlelib.idle
|
||||
|
||||
# OR (if installed)
|
||||
idle3
|
||||
|
||||
# Open an existing file
|
||||
python3 -m idlelib.idle ~/path/to/your/file.py
|
||||
```
|
||||
|
||||
### On Linux:
|
||||
|
||||
```bash
|
||||
# Launch IDLE (blank new file)
|
||||
python3 -m idlelib.idle
|
||||
|
||||
# OR (if installed)
|
||||
idle3
|
||||
|
||||
# Open an existing file
|
||||
python3 -m idlelib.idle /path/to/your/file.py
|
||||
```
|
||||
|
||||
## Copy Existing Code
|
||||
|
||||
Start with our working code and modify it.
|
||||
|
||||
### Create main file:
|
||||
|
||||
In your [project_folder](file:///Users/home/YandexDisk/TECHNOLYCEUM/ict/Year/2025/ai/ai6/ai6-m3/ai6-m3/jokes_bot/v4.0/database..py#L1-L45), create a new file:
|
||||
|
||||
```bash
|
||||
# Using Python IDLE or any text editor:
|
||||
notepad app.py
|
||||
# (Or use VS Code, Sublime Text, or Python's IDLE)
|
||||
```
|
||||
|
||||
### Expected Folder Structure:
|
||||
|
||||
```
|
||||
project_folder/
|
||||
├── app.py # Main file
|
||||
├── requirements.txt # Python dependencies
|
||||
└── venv/ # Virtual environment
|
||||
```
|
||||
|
||||
## Final Steps
|
||||
|
||||
Once you have completed all the steps above, you'll have a properly configured Python development environment ready for your project. Remember to always activate your virtual environment (`source venv/bin/activate` on Mac/Linux or `venv\Scripts\Activate.ps1` on Windows) before starting work on your project.
|
||||
BIN
jokes_bot/__pycache__/database.cpython-39.pyc
Normal file
BIN
jokes_bot/__pycache__/database.cpython-39.pyc
Normal file
Binary file not shown.
35
jokes_bot/v1.0/jokes_v1.py
Normal file
35
jokes_bot/v1.0/jokes_v1.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# app.py — Teacher-Only Telegram Joke Bot
|
||||
from telegram import Update
|
||||
from telegram.ext import ApplicationBuilder, CommandHandler, ContextTypes
|
||||
import random
|
||||
|
||||
# 🔑 REPLACE WITH REAL TOKEN FROM @BotFather
|
||||
BOT_TOKEN = "Yddsdsd"
|
||||
|
||||
# --- COPY STUDENT JOKES HERE BEFORE CLASS ---
|
||||
JOKE_LIST = [
|
||||
"Why did the robot go to school? To recharge his brain! 🔋",
|
||||
"Knock knock!\\nWho's there?\\nBoo!\\nBoo who?\\nDon't cry! 😂",
|
||||
"Why don't eggs tell jokes? They'd crack each other up! 🥚",
|
||||
"What do you call a penguin in the desert? Lost! 🐧",
|
||||
# Add student jokes here
|
||||
]
|
||||
|
||||
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
await update.message.reply_text("🤖 Hi! I'm your Joke Bot!\nType /joke for a funny joke in English! 😄")
|
||||
|
||||
async def get_random_joke(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
joke = random.choice(JOKE_LIST)
|
||||
await update.message.reply_text(joke)
|
||||
|
||||
|
||||
def main():
|
||||
print("🚀 Starting Joke Bot...")
|
||||
app = ApplicationBuilder().token(BOT_TOKEN).build()
|
||||
app.add_handler(CommandHandler("start", start))
|
||||
app.add_handler(CommandHandler("joke", get_random_joke))
|
||||
print("✅ Bot is running! Press Ctrl+C to stop.")
|
||||
app.run_polling()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
43
jokes_bot/v2.0/jokes_v2.py
Normal file
43
jokes_bot/v2.0/jokes_v2.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from telegram import Update
|
||||
from telegram.ext import Application, CommandHandler, MessageHandler, filters
|
||||
import sqlite3
|
||||
|
||||
TOKEN = "7864875699:AAEWf6ff1DYNzPvW6Dbn7D2W5aavg9KPhgY"
|
||||
|
||||
waiting = {} # Track who's adding jokes
|
||||
|
||||
async def start(update, context):
|
||||
await update.message.reply_text("🤣 Joke Bot!\n/joke - get joke\n/addjoke - add joke")
|
||||
|
||||
async def joke(update, context):
|
||||
conn = sqlite3.connect('jokes.db')
|
||||
conn.execute('CREATE TABLE IF NOT EXISTS jokes (text TEXT, user TEXT, name TEXT)')
|
||||
joke = conn.execute("SELECT text FROM jokes ORDER BY RANDOM() LIMIT 1").fetchone()
|
||||
conn.close()
|
||||
await update.message.reply_text(joke[0] if joke else "No jokes! /addjoke to add one")
|
||||
|
||||
async def addjoke(update, context):
|
||||
waiting[update.effective_user.id] = True
|
||||
await update.message.reply_text("📝 Type your joke:")
|
||||
|
||||
async def handle_text(update, context):
|
||||
user_id = update.effective_user.id
|
||||
if user_id in waiting:
|
||||
conn = sqlite3.connect('jokes.db')
|
||||
conn.execute('CREATE TABLE IF NOT EXISTS jokes (text TEXT, user TEXT, name TEXT)')
|
||||
conn.execute("INSERT INTO jokes VALUES (?, ?, ?)",
|
||||
(update.message.text, user_id, update.effective_user.first_name))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
del waiting[user_id]
|
||||
await update.message.reply_text("✅ Saved!")
|
||||
else:
|
||||
await update.message.reply_text("Try /start")
|
||||
|
||||
app = Application.builder().token(TOKEN).build()
|
||||
app.add_handler(CommandHandler("start", start))
|
||||
app.add_handler(CommandHandler("joke", joke))
|
||||
app.add_handler(CommandHandler("addjoke", addjoke))
|
||||
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_text))
|
||||
print("Bot running!")
|
||||
app.run_polling()
|
||||
BIN
jokes_bot/v3.0/.DS_Store
vendored
Normal file
BIN
jokes_bot/v3.0/.DS_Store
vendored
Normal file
Binary file not shown.
684
jokes_bot/v3.0/Joke Bot_ Telegram & SQLite Integration.html
Normal file
684
jokes_bot/v3.0/Joke Bot_ Telegram & SQLite Integration.html
Normal file
@@ -0,0 +1,684 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- saved from url=(0069)file:///Users/home/Downloads/deepseek_html_20260120_a4e55e%20(1).html -->
|
||||
<html lang="en"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Joke Bot Upgrade: SQLite Database Integration</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #f8fafc;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
height: calc(100vh - 80px);
|
||||
}
|
||||
|
||||
.slide {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.08);
|
||||
padding: 30px 40px;
|
||||
margin: 10px 0;
|
||||
display: none;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.slide.active {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #2c3e50;
|
||||
font-size: 2.2rem;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #2c3e50;
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #2c3e50;
|
||||
font-size: 1.4rem;
|
||||
margin: 20px 0 10px 0;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.6;
|
||||
margin: 15px 0;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
background: #1a202c;
|
||||
color: #e2e8f0;
|
||||
padding: 20px;
|
||||
border-radius: 6px;
|
||||
margin: 15px 0;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.instruction-block {
|
||||
background: #fff8e1;
|
||||
border-left: 4px solid #ffb300;
|
||||
padding: 15px;
|
||||
margin: 15px 0;
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
display: inline-block;
|
||||
background: #ffb300;
|
||||
color: white;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
line-height: 24px;
|
||||
margin-right: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.nav-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 25px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
background: #4299e1;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 25px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.nav-btn:hover {
|
||||
background: #3182ce;
|
||||
}
|
||||
|
||||
.nav-btn:disabled {
|
||||
background: #cbd5e0;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.slide-counter {
|
||||
text-align: center;
|
||||
color: #718096;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.terminal {
|
||||
background: #2d3748;
|
||||
color: #e2e8f0;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
font-family: 'Consolas', monospace;
|
||||
margin: 10px 0;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.db-structure {
|
||||
background: #f0fff4;
|
||||
border: 1px solid #9ae6b4;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
margin: 10px 0;
|
||||
font-family: 'Consolas', monospace;
|
||||
font-size: 0.9rem;
|
||||
color: #2f855a;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 15px 0 15px 20px;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #edf2f7;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: 'Consolas', monospace;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.slide {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="slide-counter" id="slide-counter">Slide 3 of 8</div>
|
||||
|
||||
<div class="container">
|
||||
<!-- Slide 1: Title -->
|
||||
<div class="slide" id="slide1">
|
||||
<div style="flex: 1; display: flex; flex-direction: column; justify-content: center;">
|
||||
<h1>Joke Bot Upgrade: SQLite Database</h1>
|
||||
<p style="font-size: 1.3rem; color: #4a5568;">From Static List to Dynamic Database</p>
|
||||
<p style="margin-top: 30px; font-size: 1.1rem; color: #718096;">
|
||||
Transform your simple joke bot into a collaborative joke database
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="nav-container">
|
||||
<button class="nav-btn" id="prevBtn1">Previous</button>
|
||||
<button class="nav-btn" id="nextBtn1">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slide 2: Original vs Upgraded Code -->
|
||||
<div class="slide" id="slide2">
|
||||
<h2>From Static List to Database</h2>
|
||||
|
||||
<p><strong>Original Code (Static List):</strong></p>
|
||||
<div class="code-block"># Static joke list - hardcoded, unchanging
|
||||
JOKE_LIST = [
|
||||
"Why did the robot go to school? To recharge his brain!",
|
||||
"Knock knock!\nWho's there?\nBoo!\nBoo who?\nDon't cry!",
|
||||
"Why don't eggs tell jokes? They'd crack each other up!",
|
||||
]
|
||||
|
||||
async def get_random_joke(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
joke = random.choice(JOKE_LIST) # Limited to pre-defined jokes
|
||||
await update.message.reply_text(joke)</div>
|
||||
|
||||
<p><strong>Upgraded Code (SQLite Database):</strong></p>
|
||||
<div class="code-block"># Dynamic database - users can add jokes
|
||||
import sqlite3
|
||||
|
||||
async def joke(update, context):
|
||||
conn = sqlite3.connect('jokes.db')
|
||||
conn.execute('CREATE TABLE IF NOT EXISTS jokes (text TEXT, user TEXT, name TEXT)')
|
||||
joke = conn.execute("SELECT text FROM jokes ORDER BY RANDOM() LIMIT 1").fetchone()
|
||||
conn.close()
|
||||
await update.message.reply_text(joke[0] if joke else "No jokes! /addjoke to add one")
|
||||
|
||||
async def addjoke(update, context):
|
||||
waiting[update.effective_user.id] = True
|
||||
await update.message.reply_text("Type your joke:")</div>
|
||||
|
||||
<p><strong>Key Improvements:</strong></p>
|
||||
<ul>
|
||||
<li>Static list → Dynamic SQLite database</li>
|
||||
<li>Hardcoded jokes → User-contributed content</li>
|
||||
<li>Limited selection → Unlimited joke storage</li>
|
||||
<li>Teacher-only → Collaborative system</li>
|
||||
</ul>
|
||||
|
||||
<div class="nav-container">
|
||||
<button class="nav-btn" id="prevBtn2">Previous</button>
|
||||
<button class="nav-btn" id="nextBtn2">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slide 3: Step-by-Step Guide - Part 1 -->
|
||||
<div class="slide active" id="slide3">
|
||||
<h2>Step-by-Step Upgrade Guide - Part 1</h2>
|
||||
<p>Let's upgrade your Joke Bot one step at a time:</p>
|
||||
|
||||
<div class="instruction-block">
|
||||
<div class="step-number">1</div>
|
||||
<strong>Remove the old joke list</strong>
|
||||
<p>Find this line in your original code:</p>
|
||||
<div class="code-block" style="background: #4a5568; font-size: 0.9rem; padding: 10px;">
|
||||
JOKE_LIST = [
|
||||
"Why did the robot go to school? To recharge his brain!",
|
||||
# ... more jokes ...
|
||||
]</div>
|
||||
<p>Delete everything from <code>JOKE_LIST = [</code> to the closing <code>]</code> including all jokes.</p>
|
||||
</div>
|
||||
|
||||
<div class="instruction-block">
|
||||
<div class="step-number">2</div>
|
||||
<strong>Add SQLite import</strong>
|
||||
<p>At the VERY TOP of your file, add this line:</p>
|
||||
<div class="code-block" style="background: #4a5568; font-size: 0.9rem; padding: 10px;">
|
||||
import sqlite3</div>
|
||||
<p>This should go right after the other imports, like:</p>
|
||||
<div class="code-block" style="background: #4a5568; font-size: 0.9rem; padding: 10px;">
|
||||
from telegram import Update
|
||||
from telegram.ext import ApplicationBuilder, CommandHandler, ContextTypes
|
||||
import random
|
||||
import sqlite3 # ← ADD THIS LINE</div>
|
||||
</div>
|
||||
|
||||
<div class="instruction-block">
|
||||
<div class="step-number">3</div>
|
||||
<strong>Add waiting dictionary</strong>
|
||||
<p>Find your <code>BOT_TOKEN</code> line and add this right after it:</p>
|
||||
<div class="code-block" style="background: #4a5568; font-size: 0.9rem; padding: 10px;">
|
||||
BOT_TOKEN = "YOUR_BOT_TOKEN_HERE"
|
||||
waiting = {} # ← ADD THIS LINE</div>
|
||||
<p>This dictionary will track users who are typing jokes.</p>
|
||||
</div>
|
||||
|
||||
<div class="nav-container">
|
||||
<button class="nav-btn" id="prevBtn3">Previous</button>
|
||||
<button class="nav-btn" id="nextBtn3">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slide 4: Step-by-Step Guide - Part 2 -->
|
||||
<div class="slide" id="slide4">
|
||||
<h2>Step-by-Step Upgrade Guide - Part 2</h2>
|
||||
|
||||
<div class="instruction-block">
|
||||
<div class="step-number">4</div>
|
||||
<strong>Update the joke function</strong>
|
||||
<p>Find your <code>get_random_joke</code> function. DELETE this entire function:</p>
|
||||
<div class="code-block" style="background: #4a5568; font-size: 0.9rem; padding: 10px;">
|
||||
async def get_random_joke(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
joke = random.choice(JOKE_LIST)
|
||||
await update.message.reply_text(joke)</div>
|
||||
|
||||
<p>REPLACE it with this new function:</p>
|
||||
<div class="code-block" style="background: #4a5568; font-size: 0.9rem; padding: 10px;">
|
||||
async def joke(update, context):
|
||||
"""Get random joke from database"""
|
||||
conn = sqlite3.connect('jokes.db')
|
||||
conn.execute('CREATE TABLE IF NOT EXISTS jokes (text TEXT, user TEXT, name TEXT)')
|
||||
joke = conn.execute("SELECT text FROM jokes ORDER BY RANDOM() LIMIT 1").fetchone()
|
||||
conn.close()
|
||||
|
||||
if joke:
|
||||
await update.message.reply_text(joke[0])
|
||||
else:
|
||||
await update.message.reply_text("No jokes in database! Use /addjoke to add one.")</div>
|
||||
</div>
|
||||
|
||||
<div class="instruction-block">
|
||||
<div class="step-number">5</div>
|
||||
<strong>Add the addjoke function</strong>
|
||||
<p>Right after your <code>joke</code> function, add this new function:</p>
|
||||
<div class="code-block" style="background: #4a5568; font-size: 0.9rem; padding: 10px;">
|
||||
async def addjoke(update, context):
|
||||
"""Initiate joke addition process"""
|
||||
waiting[update.effective_user.id] = True
|
||||
await update.message.reply_text("Type your joke (one message):")</div>
|
||||
<p>This function starts the process when users type <code>/addjoke</code>.</p>
|
||||
</div>
|
||||
|
||||
<div class="nav-container">
|
||||
<button class="nav-btn" id="prevBtn4">Previous</button>
|
||||
<button class="nav-btn" id="nextBtn4">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slide 5: Step-by-Step Guide - Part 3 -->
|
||||
<div class="slide" id="slide5">
|
||||
<h2>Step-by-Step Upgrade Guide - Part 3</h2>
|
||||
|
||||
<div class="instruction-block">
|
||||
<div class="step-number">6</div>
|
||||
<strong>Add the text handler function</strong>
|
||||
<p>Add this function after your <code>addjoke</code> function:</p>
|
||||
<div class="code-block" style="background: #4a5568; font-size: 0.9rem; padding: 10px; margin-bottom: 10px;">
|
||||
async def handle_text(update, context):
|
||||
"""Handle text messages for joke addition"""
|
||||
user_id = update.effective_user.id
|
||||
|
||||
if user_id in waiting:
|
||||
# User is adding a joke
|
||||
conn = sqlite3.connect('jokes.db')
|
||||
conn.execute('CREATE TABLE IF NOT EXISTS jokes (text TEXT, user TEXT, name TEXT)')
|
||||
|
||||
# Insert joke with user info
|
||||
conn.execute("INSERT INTO jokes VALUES (?, ?, ?)",
|
||||
(update.message.text, user_id, update.effective_user.first_name))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
# Remove from waiting list
|
||||
del waiting[user_id]
|
||||
await update.message.reply_text("✅ Joke saved to database!")
|
||||
else:
|
||||
# Regular message, not adding joke
|
||||
await update.message.reply_text("Try /start to see available commands")</div>
|
||||
<p>This function catches regular messages and saves jokes to the database.</p>
|
||||
</div>
|
||||
|
||||
<div class="instruction-block">
|
||||
<div class="step-number">7</div>
|
||||
<strong>Update the main function</strong>
|
||||
<p>Find your <code>main()</code> function. Change it from this:</p>
|
||||
<div class="code-block" style="background: #4a5568; font-size: 0.9rem; padding: 10px; margin-bottom: 10px;">
|
||||
def main():
|
||||
print("🚀 Starting Joke Bot...")
|
||||
app = ApplicationBuilder().token(BOT_TOKEN).build()
|
||||
app.add_handler(CommandHandler("start", start))
|
||||
app.add_handler(CommandHandler("joke", get_random_joke))
|
||||
print("✅ Bot is running! Press Ctrl+C to stop.")
|
||||
app.run_polling()</div>
|
||||
|
||||
<p>To this (CHANGE the highlighted lines):</p>
|
||||
<div class="code-block" style="background: #4a5568; font-size: 0.9rem; padding: 10px;">
|
||||
def main():
|
||||
print("🚀 Starting Joke Bot...")
|
||||
app = ApplicationBuilder().token(BOT_TOKEN).build()
|
||||
app.add_handler(CommandHandler("start", start))
|
||||
app.add_handler(CommandHandler("joke", joke)) # ← CHANGE get_random_joke to joke
|
||||
app.add_handler(CommandHandler("addjoke", addjoke)) # ← ADD THIS LINE
|
||||
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_text)) # ← ADD THIS LINE
|
||||
print("✅ Bot is running! Press Ctrl+C to stop.")
|
||||
app.run_polling()</div>
|
||||
</div>
|
||||
|
||||
<div class="nav-container">
|
||||
<button class="nav-btn" id="prevBtn5">Previous</button>
|
||||
<button class="nav-btn" id="nextBtn5">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slide 6: SQLite Database Implementation -->
|
||||
<div class="slide" id="slide6">
|
||||
<h2>SQLite Database Implementation</h2>
|
||||
|
||||
<p><strong>Your Code Should Now Include:</strong></p>
|
||||
|
||||
<div class="code-block">import sqlite3 # Added at top
|
||||
|
||||
# ... other imports ...
|
||||
|
||||
BOT_TOKEN = "YOUR_BOT_TOKEN_HERE"
|
||||
waiting = {} # Added after token
|
||||
|
||||
# ... start function remains the same ...
|
||||
|
||||
async def joke(update, context):
|
||||
# New database-powered joke function
|
||||
conn = sqlite3.connect('jokes.db')
|
||||
conn.execute('CREATE TABLE IF NOT EXISTS jokes (text TEXT, user TEXT, name TEXT)')
|
||||
joke = conn.execute("SELECT text FROM jokes ORDER BY RANDOM() LIMIT 1").fetchone()
|
||||
conn.close()
|
||||
|
||||
if joke:
|
||||
await update.message.reply_text(joke[0])
|
||||
else:
|
||||
await update.message.reply_text("No jokes in database! Use /addjoke to add one.")
|
||||
|
||||
async def addjoke(update, context):
|
||||
# New function for adding jokes
|
||||
waiting[update.effective_user.id] = True
|
||||
await update.message.reply_text("Type your joke (one message):")
|
||||
|
||||
async def handle_text(update, context):
|
||||
# New function to handle text messages
|
||||
user_id = update.effective_user.id
|
||||
|
||||
if user_id in waiting:
|
||||
# Save joke to database
|
||||
conn = sqlite3.connect('jokes.db')
|
||||
conn.execute('CREATE TABLE IF NOT EXISTS jokes (text TEXT, user TEXT, name TEXT)')
|
||||
conn.execute("INSERT INTO jokes VALUES (?, ?, ?)",
|
||||
(update.message.text, user_id, update.effective_user.first_name))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
del waiting[user_id]
|
||||
await update.message.reply_text("✅ Joke saved to database!")
|
||||
else:
|
||||
await update.message.reply_text("Try /start to see available commands")</div>
|
||||
|
||||
<div class="nav-container">
|
||||
<button class="nav-btn" id="prevBtn6">Previous</button>
|
||||
<button class="nav-btn" id="nextBtn6">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slide 7: Database Structure & Commands -->
|
||||
<div class="slide" id="slide7">
|
||||
<h2>Database Structure & Bot Commands</h2>
|
||||
|
||||
<p><strong>SQLite Database Schema:</strong></p>
|
||||
<div class="db-structure">-- jokes.db SQLite database structure
|
||||
CREATE TABLE jokes (
|
||||
text TEXT, -- The joke text
|
||||
user TEXT, -- User ID for tracking
|
||||
name TEXT -- User's first name
|
||||
);
|
||||
|
||||
-- Sample data:
|
||||
INSERT INTO jokes VALUES
|
||||
('Why did the Python cross the road? To get to the other side!', '12345', 'Alice'),
|
||||
('What do you call a fake noodle? An impasta!', '67890', 'Bob');</div>
|
||||
|
||||
<p><strong>Your Bot Now Has These Commands:</strong></p>
|
||||
<div class="terminal">/start - Start the bot and see commands
|
||||
/joke - Get a random joke from database
|
||||
/addjoke - Add your own joke to database</div>
|
||||
|
||||
<p><strong>How It Works:</strong></p>
|
||||
<ul>
|
||||
<li>When user types <code>/joke</code> → Bot gets random joke from database</li>
|
||||
<li>When user types <code>/addjoke</code> → Bot waits for their joke</li>
|
||||
<li>User types their joke → Bot saves it to SQLite database</li>
|
||||
<li>Everyone can now access that joke with <code>/joke</code></li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Important Note:</strong> The database file <code>jokes.db</code> will be created automatically the first time you run your bot.</p>
|
||||
|
||||
<div class="nav-container">
|
||||
<button class="nav-btn" id="prevBtn7">Previous</button>
|
||||
<button class="nav-btn" id="nextBtn7">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slide 8: Complete Implementation -->
|
||||
<div class="slide" id="slide8">
|
||||
<h2>Complete Implementation Code</h2>
|
||||
|
||||
<div class="code-block">from telegram import Update
|
||||
from telegram.ext import Application, CommandHandler, MessageHandler, filters
|
||||
import sqlite3
|
||||
|
||||
# Replace with your actual bot token
|
||||
TOKEN = "YOUR_BOT_TOKEN_HERE"
|
||||
|
||||
# Track users who are currently adding jokes
|
||||
waiting = {}
|
||||
|
||||
async def start(update, context):
|
||||
"""Handle /start command"""
|
||||
await update.message.reply_text(
|
||||
"Welcome to Joke Bot!\n\n"
|
||||
"Available commands:\n"
|
||||
"/joke - Get a random joke\n"
|
||||
"/addjoke - Add your own joke to the database"
|
||||
)
|
||||
|
||||
async def joke(update, context):
|
||||
"""Handle /joke command - get random joke from database"""
|
||||
conn = sqlite3.connect('jokes.db')
|
||||
# Create table if it doesn't exist
|
||||
conn.execute('CREATE TABLE IF NOT EXISTS jokes (text TEXT, user TEXT, name TEXT)')
|
||||
|
||||
# Get random joke
|
||||
joke = conn.execute("SELECT text FROM jokes ORDER BY RANDOM() LIMIT 1").fetchone()
|
||||
conn.close()
|
||||
|
||||
if joke:
|
||||
await update.message.reply_text(joke[0])
|
||||
else:
|
||||
await update.message.reply_text("No jokes in the database yet! Use /addjoke to add one.")
|
||||
|
||||
async def addjoke(update, context):
|
||||
"""Handle /addjoke command - start joke addition process"""
|
||||
waiting[update.effective_user.id] = True
|
||||
await update.message.reply_text("Type your joke (send it in one message):")
|
||||
|
||||
async def handle_text(update, context):
|
||||
"""Handle all text messages"""
|
||||
user_id = update.effective_user.id
|
||||
|
||||
if user_id in waiting:
|
||||
# User is adding a joke
|
||||
conn = sqlite3.connect('jokes.db')
|
||||
conn.execute('CREATE TABLE IF NOT EXISTS jokes (text TEXT, user TEXT, name TEXT)')
|
||||
|
||||
# Save joke with user information
|
||||
conn.execute(
|
||||
"INSERT INTO jokes VALUES (?, ?, ?)",
|
||||
(update.message.text, user_id, update.effective_user.first_name)
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
# Remove user from waiting list
|
||||
del waiting[user_id]
|
||||
await update.message.reply_text("Joke saved to the database! Others can now see it with /joke")
|
||||
else:
|
||||
# Regular message (not adding joke)
|
||||
await update.message.reply_text("I don't understand that. Try /start to see available commands.")
|
||||
|
||||
# Main bot setup
|
||||
app = Application.builder().token(TOKEN).build()
|
||||
|
||||
# Add command handlers
|
||||
app.add_handler(CommandHandler("start", start))
|
||||
app.add_handler(CommandHandler("joke", joke))
|
||||
app.add_handler(CommandHandler("addjoke", addjoke))
|
||||
|
||||
# Add text message handler (for joke input)
|
||||
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_text))
|
||||
|
||||
print("Joke Bot is running with SQLite database support!")
|
||||
app.run_polling()</div>
|
||||
|
||||
<div class="nav-container">
|
||||
<button class="nav-btn" id="prevBtn8">Previous</button>
|
||||
<button class="nav-btn" id="nextBtn8">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const totalSlides = 8;
|
||||
let currentSlide = 1;
|
||||
|
||||
function updateSlideCounter() {
|
||||
document.getElementById('slide-counter').textContent = `Slide ${currentSlide} of ${totalSlides}`;
|
||||
}
|
||||
|
||||
function showSlide(slideNumber) {
|
||||
for (let i = 1; i <= totalSlides; i++) {
|
||||
const slide = document.getElementById(`slide${i}`);
|
||||
if (slide) slide.classList.remove('active');
|
||||
}
|
||||
|
||||
const slideElement = document.getElementById(`slide${slideNumber}`);
|
||||
if (slideElement) {
|
||||
slideElement.classList.add('active');
|
||||
currentSlide = slideNumber;
|
||||
updateSlideCounter();
|
||||
updateButtons();
|
||||
}
|
||||
}
|
||||
|
||||
function updateButtons() {
|
||||
for (let i = 1; i <= totalSlides; i++) {
|
||||
const prevBtn = document.getElementById(`prevBtn${i}`);
|
||||
const nextBtn = document.getElementById(`nextBtn${i}`);
|
||||
if (prevBtn) prevBtn.disabled = currentSlide === 1;
|
||||
if (nextBtn) {
|
||||
nextBtn.textContent = currentSlide === totalSlides ? 'Finish' : 'Next';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 1; i <= totalSlides; i++) {
|
||||
const nextBtn = document.getElementById(`nextBtn${i}`);
|
||||
const prevBtn = document.getElementById(`prevBtn${i}`);
|
||||
|
||||
if (nextBtn) {
|
||||
nextBtn.addEventListener('click', () => {
|
||||
if (currentSlide < totalSlides) {
|
||||
showSlide(currentSlide + 1);
|
||||
} else {
|
||||
alert('Congratulations! You have completed the Joke Bot upgrade tutorial. Your bot now has a SQLite database and can accept user-submitted jokes!');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (prevBtn) {
|
||||
prevBtn.addEventListener('click', () => {
|
||||
if (currentSlide > 1) {
|
||||
showSlide(currentSlide - 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'ArrowRight' || e.key === ' ') {
|
||||
if (currentSlide < totalSlides) showSlide(currentSlide + 1);
|
||||
} else if (e.key === 'ArrowLeft') {
|
||||
if (currentSlide > 1) showSlide(currentSlide - 1);
|
||||
}
|
||||
});
|
||||
|
||||
updateSlideCounter();
|
||||
updateButtons();
|
||||
</script>
|
||||
|
||||
</body></html>
|
||||
749
jokes_bot/v3.0/Lesson Slides Template.html
Normal file
749
jokes_bot/v3.0/Lesson Slides Template.html
Normal file
@@ -0,0 +1,749 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- saved from url=(0063)file:///Users/home/Downloads/deepseek_html_20260120_76fab4.html -->
|
||||
<html lang="en"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Lesson Slides Template</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f8f9fa;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
height: calc(100vh - 60px);
|
||||
}
|
||||
|
||||
.slide {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
padding: 30px 40px;
|
||||
margin: 10px 0;
|
||||
display: none;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
max-height: 82vh;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.slide.active {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #2c3e50;
|
||||
font-size: 2.2rem;
|
||||
margin-bottom: 15px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #2c3e50;
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #2c3e50;
|
||||
font-size: 1.4rem;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.lesson-title {
|
||||
color: #2980b9;
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.subject-topic {
|
||||
color: #27ae60;
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.subject-title {
|
||||
text-align: center;
|
||||
color: #7f8c8d;
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 30px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.6;
|
||||
margin: 15px 0;
|
||||
color: #34495e;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
margin: 15px 0 20px 30px;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.6;
|
||||
color: #34495e;
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.rules-container {
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
padding: 25px;
|
||||
margin: 20px 0;
|
||||
border-left: 4px solid #3498db;
|
||||
}
|
||||
|
||||
.outcomes-container {
|
||||
background-color: #e8f4fd;
|
||||
border-radius: 6px;
|
||||
padding: 25px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.why-container {
|
||||
background-color: #f0f7ff;
|
||||
border-radius: 6px;
|
||||
padding: 25px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.terminal {
|
||||
background-color: #2c3e50;
|
||||
color: #ecf0f1;
|
||||
padding: 20px;
|
||||
border-radius: 6px;
|
||||
font-family: 'Courier New', monospace;
|
||||
margin: 20px 0;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.terminal-command {
|
||||
color: #2ecc71;
|
||||
}
|
||||
|
||||
.terminal-comment {
|
||||
color: #95a5a6;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: auto;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #3498db;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
font-size: 1rem;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
font-weight: 500;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #2980b9;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background-color: #bdc3c7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.slide-counter {
|
||||
text-align: center;
|
||||
color: #7f8c8d;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.lead {
|
||||
font-size: 1.2rem;
|
||||
color: #7f8c8d;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: #f1f2f3;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.important-box {
|
||||
background-color: #fff3cd;
|
||||
border: 2px solid #ffeaa7;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.container {
|
||||
height: calc(100vh - 40px);
|
||||
}
|
||||
|
||||
.slide {
|
||||
padding: 20px 25px;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.lesson-title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.subject-topic {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
p, ul, ol {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 8px 16px;
|
||||
font-size: 0.95rem;
|
||||
min-width: 100px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="slide-counter" id="slide-counter">Slide 7 of 12</div>
|
||||
|
||||
<div class="container">
|
||||
<!-- Slide 1: Title Slide -->
|
||||
<div class="slide" id="slide1">
|
||||
<div style="flex: 1; display: flex; flex-direction: column; justify-content: center;">
|
||||
<div class="lesson-title">Upgrade Your Joke Bot!</div>
|
||||
<div class="subject-topic">AI6-M3: Database Integration</div>
|
||||
<div class="subject-title">Digital Technologies / Information Communication Technologies</div>
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<button id="prevBtn1">Previous</button>
|
||||
<button id="nextBtn1">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slide 2: Rules -->
|
||||
<div class="slide" id="slide2">
|
||||
<h2>Classroom Guidelines</h2>
|
||||
|
||||
<div class="rules-container">
|
||||
<h3>How Points Are Earned:</h3>
|
||||
|
||||
<p><strong>+2 Points</strong> - You attended class</p>
|
||||
<p><strong>+1 Point</strong> - You listened quietly during instruction</p>
|
||||
<p><strong>+1 Point</strong> - You attempted all assigned work</p>
|
||||
<p><strong>+1 Point</strong> - You completed all assigned work</p>
|
||||
|
||||
<p style="margin-top: 25px; font-weight: bold; color: #27ae60;">
|
||||
Maximum: 5 points per class session
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<button id="prevBtn2">Previous</button>
|
||||
<button id="nextBtn2">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slide 3: Learning Outcomes -->
|
||||
<div class="slide" id="slide3">
|
||||
<h2>Learning Outcomes</h2>
|
||||
<p class="subject-title">By the end of this lesson, you will be able to:</p>
|
||||
|
||||
<div class="outcomes-container">
|
||||
<ol>
|
||||
<li>Download a project from the internet (using Git clone)</li>
|
||||
<li>Open and understand a Python program (starter_app.py)</li>
|
||||
<li>Make simple changes to make the program better</li>
|
||||
<li>Save your work to the internet (using Git push)</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<button id="prevBtn3">Previous</button>
|
||||
<button id="nextBtn3">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slide 4: Why This Matters -->
|
||||
<div class="slide" id="slide4">
|
||||
<h2>Why This Matters</h2>
|
||||
|
||||
<div class="why-container">
|
||||
<ul>
|
||||
<li>Learn how real programmers work together</li>
|
||||
<li>Understand how apps remember things (using databases)</li>
|
||||
<li>Practice following step-by-step instructions</li>
|
||||
<li>Create something that actually works!</li>
|
||||
<li>These skills help with school projects and future jobs</li>
|
||||
</ul>
|
||||
|
||||
<p style="margin-top: 30px;"><strong>Who uses these skills?</strong></p>
|
||||
<p>Every app on your phone (Instagram, TikTok, games) needs databases to remember your information!</p>
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<button id="prevBtn4">Previous</button>
|
||||
<button id="nextBtn4">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slide 5: Our Special Web Address -->
|
||||
<div class="slide" id="slide5">
|
||||
<h2>Step 1: Find Our Project Online</h2>
|
||||
<p class="lead">Every project has a special web address (URL)</p>
|
||||
|
||||
<div class="content-container">
|
||||
<div class="important-box">
|
||||
<strong>💡 IMPORTANT:</strong> You MUST use this exact address:<br>
|
||||
<code>https://gitea.techshare.cc/technolyceum/ai6-m3.git</code>
|
||||
</div>
|
||||
|
||||
<p><strong>Think of it like:</strong></p>
|
||||
<ul>
|
||||
<li>This is our project's "home" on the internet</li>
|
||||
<li>Like downloading a game from the App Store</li>
|
||||
<li>We're getting all the files we need to start</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>What's inside this address?</strong></p>
|
||||
<div class="terminal">
|
||||
<span class="terminal-comment"># Inside this address you'll find:</span>
|
||||
📁 ai6-m3-project/
|
||||
├── 📄 starter_app.py ← The joke bot you'll upgrade!
|
||||
├── 📄 requirements.txt ← List of things needed
|
||||
├── 📄 README.md ← Instructions
|
||||
└── 📄 .gitignore ← Special settings file
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<button id="prevBtn5">Previous</button>
|
||||
<button id="nextBtn5">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slide 6: Download with Git Clone -->
|
||||
<div class="slide" id="slide6">
|
||||
<h2>Step 2: Download to Your Computer</h2>
|
||||
<p class="lead">Using "Git Clone" - like copying a folder from the internet</p>
|
||||
|
||||
<div class="content-container">
|
||||
<p><strong>Open Command Prompt (Windows):</strong></p>
|
||||
<p>Press <code>Windows Key</code>, type <code>cmd</code>, press <code>Enter</code></p>
|
||||
|
||||
<p><strong>Go to Desktop:</strong></p>
|
||||
<div class="terminal">
|
||||
<span class="terminal-comment"># Type this to go to your Desktop:</span>
|
||||
<span class="terminal-command">cd Desktop</span>
|
||||
|
||||
<span class="terminal-comment"># Check you're on Desktop:</span>
|
||||
<span class="terminal-command">dir</span>
|
||||
<span class="terminal-comment"># You'll see your Desktop files</span>
|
||||
</div>
|
||||
|
||||
<p><strong>DOWNLOAD THE PROJECT:</strong></p>
|
||||
<div class="terminal">
|
||||
<span class="terminal-comment"># Copy everything from the internet to your computer:</span>
|
||||
<span class="terminal-command">git clone https://gitea.techshare.cc/technolyceum/ai6-m3.git</span>
|
||||
|
||||
<span class="terminal-comment"># This creates a folder called "ai6-m3" on your Desktop</span>
|
||||
</div>
|
||||
|
||||
<p><strong>Check it worked:</strong></p>
|
||||
<div class="terminal">
|
||||
<span class="terminal-command">cd ai6-m3</span>
|
||||
<span class="terminal-command">dir</span>
|
||||
<span class="terminal-comment"># You should see "starter_app.py" in the list!</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<button id="prevBtn6">Previous</button>
|
||||
<button id="nextBtn6">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slide 7: Open and Look at the Joke Bot -->
|
||||
<div class="slide active" id="slide7">
|
||||
<h2>Step 3: Open the Joke Bot Program</h2>
|
||||
<p class="lead">Let's see what we're working with!</p>
|
||||
|
||||
<div class="content-container">
|
||||
<p><strong>Using VS Code (or any text editor):</strong></p>
|
||||
|
||||
<div class="terminal">
|
||||
<span class="terminal-comment"># Make sure you're in the right folder:</span>
|
||||
<span class="terminal-command">cd Desktop\ai6-m3</span>
|
||||
|
||||
<span class="terminal-comment"># Open the folder in VS Code:</span>
|
||||
<span class="terminal-command">code .</span>
|
||||
<span class="terminal-comment"># Or open VS Code and use File → Open Folder</span>
|
||||
</div>
|
||||
|
||||
<p><strong>Look for this file:</strong></p>
|
||||
<div class="terminal">
|
||||
📁 ai6-m3/
|
||||
└── 📄 <strong>starter_app.py</strong> ← DOUBLE CLICK THIS!
|
||||
</div>
|
||||
|
||||
<p><strong>What you'll see:</strong></p>
|
||||
<ul>
|
||||
<li>A Python program that tells jokes</li>
|
||||
<li>Right now, it only has 3 jokes</li>
|
||||
<li>We're going to make it remember MORE jokes</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Try running it first:</strong></p>
|
||||
<div class="terminal">
|
||||
<span class="terminal-command">python starter_app.py</span>
|
||||
<span class="terminal-comment"># It should tell you a random joke!</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<button id="prevBtn7">Previous</button>
|
||||
<button id="nextBtn8">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slide 8: Make the Joke Bot Better -->
|
||||
<div class="slide" id="slide8">
|
||||
<h2>Step 4: Upgrade Your Joke Bot!</h2>
|
||||
<p class="lead">Make it remember jokes using a database</p>
|
||||
|
||||
<div class="content-container">
|
||||
<p><strong>Your mission:</strong></p>
|
||||
<ol>
|
||||
<li>Add at least 5 new jokes to the program</li>
|
||||
<li>Make it remember jokes even after you close it</li>
|
||||
<li>Test that it works!</li>
|
||||
</ol>
|
||||
|
||||
<p><strong>Simple changes to make:</strong></p>
|
||||
<div class="terminal">
|
||||
<span class="terminal-comment"># In starter_app.py, find this part:</span>
|
||||
jokes = [
|
||||
"Why don't scientists trust atoms?...",
|
||||
"Why did the chicken cross the road?...",
|
||||
"What do you call fake spaghetti?..."
|
||||
]
|
||||
|
||||
<span class="terminal-comment"># ADD YOUR JOKES HERE!</span>
|
||||
<span class="terminal-command"> "Your first joke here",</span>
|
||||
<span class="terminal-command"> "Your second joke here",</span>
|
||||
<span class="terminal-command"> "Keep adding more!"</span>
|
||||
<span class="terminal-comment"># Don't forget the comma at the end!</span>
|
||||
</div>
|
||||
|
||||
<p><strong>Database part (teacher will help):</strong></p>
|
||||
<div class="terminal">
|
||||
<span class="terminal-comment"># We'll add code to save jokes to a file</span>
|
||||
<span class="terminal-comment"># So jokes don't disappear when you close the program</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<button id="prevBtn8">Previous</button>
|
||||
<button id="nextBtn9">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slide 9: Save Your Work with Git -->
|
||||
<div class="slide" id="slide9">
|
||||
<h2>Step 5: Save Your Work Online</h2>
|
||||
<p class="lead">Using Git - like saving to the cloud</p>
|
||||
|
||||
<div class="content-container">
|
||||
<p><strong>First, check what you changed:</strong></p>
|
||||
<div class="terminal">
|
||||
<span class="terminal-comment"># Make sure you're in ai6-m3 folder:</span>
|
||||
<span class="terminal-command">cd Desktop\ai6-m3</span>
|
||||
|
||||
<span class="terminal-comment"># See what files you changed:</span>
|
||||
<span class="terminal-command">git status</span>
|
||||
<span class="terminal-comment"># It will show "starter_app.py" in red or green</span>
|
||||
</div>
|
||||
|
||||
<p><strong>Step 1: Tell Git about your changes</strong></p>
|
||||
<div class="terminal">
|
||||
<span class="terminal-command">git add .</span>
|
||||
<span class="terminal-comment"># This means: "Save ALL my changes"</span>
|
||||
<span class="terminal-comment"># The dot (.) means "everything in this folder"</span>
|
||||
</div>
|
||||
|
||||
<p><strong>Step 2: Give your work a title</strong></p>
|
||||
<div class="terminal">
|
||||
<span class="terminal-command">git commit -m "Upgraded app for database implementation"</span>
|
||||
<span class="terminal-comment"># This is like putting your name on your work</span>
|
||||
<span class="terminal-comment"># "Upgraded app for database implementation" is your title</span>
|
||||
</div>
|
||||
|
||||
<p><strong>Step 3: Send it online</strong></p>
|
||||
<div class="terminal">
|
||||
<span class="terminal-command">git push</span>
|
||||
<span class="terminal-comment"># This sends your work back to the internet</span>
|
||||
<span class="terminal-comment"># Now everyone can see your upgraded joke bot!</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<button id="prevBtn9">Previous</button>
|
||||
<button id="nextBtn10">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slide 10: What You Learned -->
|
||||
<div class="slide" id="slide10">
|
||||
<h2>What You Learned</h2>
|
||||
|
||||
<div class="content-container">
|
||||
<p>In this lesson, you have learned how to:</p>
|
||||
|
||||
<ol>
|
||||
<li>Download a project from the internet using a special address</li>
|
||||
<li>Open and run a Python program on your computer</li>
|
||||
<li>Add new jokes to make the program better</li>
|
||||
<li>Save your work and share it online using Git</li>
|
||||
</ol>
|
||||
|
||||
<p><strong>Key Takeaways:</strong></p>
|
||||
<p>You now know the basic workflow of real programmers: <strong>Download → Modify → Save → Share</strong>. This is how all apps and games get made!</p>
|
||||
|
||||
<div class="important-box">
|
||||
<strong>🎉 CONGRATULATIONS!</strong><br>
|
||||
You just completed your first programming project upgrade!
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<button id="prevBtn10">Previous</button>
|
||||
<button id="nextBtn11">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slide 11: Questions -->
|
||||
<div class="slide" id="slide11">
|
||||
<h2>Questions & Discussion</h2>
|
||||
|
||||
<div class="content-container">
|
||||
<p><strong>Review what we covered:</strong></p>
|
||||
<ul>
|
||||
<li>What was the most fun part of upgrading the joke bot?</li>
|
||||
<li>Did your jokes make your friends laugh?</li>
|
||||
<li>What other things would you like a bot to remember?</li>
|
||||
<li>Was Git push confusing? What part?</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Common Issues:</strong></p>
|
||||
<ul>
|
||||
<li>Forgot commas between jokes? → Add them!</li>
|
||||
<li>Git says "not a git repository"? → Make sure you're in ai6-m3 folder!</li>
|
||||
<li>Python won't run? → Check for typos in your code</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Need Additional Help?</strong></p>
|
||||
<p>Raise your hand! Ask your neighbor! The teacher is here to help!</p>
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<button id="prevBtn11">Previous</button>
|
||||
<button id="nextBtn12">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slide 12: Thank You -->
|
||||
<div class="slide" id="slide12">
|
||||
<div class="content-container">
|
||||
<h1>Great Job! 🎉</h1>
|
||||
|
||||
<p>Thank you for being awesome programmers today!</p>
|
||||
|
||||
<p><strong>Your Joke Bot is now upgraded and saved online!</strong></p>
|
||||
|
||||
<div class="important-box">
|
||||
<strong>🌟 YOU DID IT! 🌟</strong><br>
|
||||
You downloaded code, made it better, and saved it for everyone to see!
|
||||
</div>
|
||||
|
||||
<p><strong>Next Steps:</strong></p>
|
||||
<ul>
|
||||
<li>Show your joke bot to family or friends</li>
|
||||
<li>Think of other cool upgrades (maybe add puns?)</li>
|
||||
<li>Be proud - you just did real programming!</li>
|
||||
</ul>
|
||||
|
||||
<p style="margin-top: 30px; color: #7f8c8d;">
|
||||
Digital Technologies / ICT<br>
|
||||
Grade 5-8 | Beginner Programmers
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<button id="prevBtn12">Previous</button>
|
||||
<button id="nextBtn12">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ==================== CONFIGURATION ====================
|
||||
// Set total number of slides for this lesson
|
||||
const totalSlides = 12; // UPDATE THIS NUMBER FOR EACH LESSON
|
||||
// =======================================================
|
||||
|
||||
let currentSlide = 1;
|
||||
|
||||
// Update slide counter
|
||||
function updateSlideCounter() {
|
||||
document.getElementById('slide-counter').textContent = `Slide ${currentSlide} of ${totalSlides}`;
|
||||
}
|
||||
|
||||
// Slide navigation
|
||||
function showSlide(slideNumber) {
|
||||
// Hide all slides
|
||||
for (let i = 1; i <= totalSlides; i++) {
|
||||
const slide = document.getElementById(`slide${i}`);
|
||||
if (slide) {
|
||||
slide.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
// Show current slide
|
||||
const currentSlideElement = document.getElementById(`slide${slideNumber}`);
|
||||
if (currentSlideElement) {
|
||||
currentSlideElement.classList.add('active');
|
||||
currentSlide = slideNumber;
|
||||
updateSlideCounter();
|
||||
|
||||
// Update button states
|
||||
updateButtons();
|
||||
}
|
||||
}
|
||||
|
||||
function updateButtons() {
|
||||
// Update previous button
|
||||
const prevButtons = document.querySelectorAll('[id^="prevBtn"]');
|
||||
prevButtons.forEach(btn => {
|
||||
btn.disabled = currentSlide === 1;
|
||||
});
|
||||
|
||||
// Update next button text on last slide
|
||||
const nextButtons = document.querySelectorAll('[id^="nextBtn"]');
|
||||
nextButtons.forEach(btn => {
|
||||
if (currentSlide === totalSlides) {
|
||||
btn.textContent = 'Complete Lesson';
|
||||
} else {
|
||||
btn.textContent = 'Next';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Next button functionality
|
||||
document.querySelectorAll('[id^="nextBtn"]').forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
if (currentSlide < totalSlides) {
|
||||
showSlide(currentSlide + 1);
|
||||
} else {
|
||||
// Last slide - completion message
|
||||
alert('🎉 Lesson completed! You are now a Joke Bot Upgrader! Thank you for participating! 🎉');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Previous button functionality
|
||||
document.querySelectorAll('[id^="prevBtn"]').forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
if (currentSlide > 1) {
|
||||
showSlide(currentSlide - 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize the presentation
|
||||
updateSlideCounter();
|
||||
updateButtons();
|
||||
|
||||
// Keyboard navigation
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'ArrowRight' || e.key === ' ' || e.key === 'PageDown') {
|
||||
if (currentSlide < totalSlides) {
|
||||
showSlide(currentSlide + 1);
|
||||
}
|
||||
} else if (e.key === 'ArrowLeft' || e.key === 'PageUp') {
|
||||
if (currentSlide > 1) {
|
||||
showSlide(currentSlide - 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize to first slide
|
||||
showSlide(1);
|
||||
</script>
|
||||
|
||||
</body></html>
|
||||
344
jokes_bot/v3.0/README.md
Normal file
344
jokes_bot/v3.0/README.md
Normal file
@@ -0,0 +1,344 @@
|
||||
Step-by-Step Complete Setup Guide
|
||||
📋 Before We Start - Check Your Python Installation
|
||||
1. Open PowerShell:
|
||||
Press Windows + R
|
||||
|
||||
Type powershell
|
||||
|
||||
Press Enter
|
||||
|
||||
2. Check Python Version:
|
||||
powershell
|
||||
python --version
|
||||
# Should show: Python 3.x.x
|
||||
|
||||
# If that doesn't work, try:
|
||||
python3 --version
|
||||
# Or on some systems:
|
||||
py --version
|
||||
📁 Create Your Project Folder
|
||||
3. Navigate to Documents and Create Folder:
|
||||
powershell
|
||||
# Go to Documents folder
|
||||
cd Documents
|
||||
|
||||
# Create a new folder using format: [firstname]_[surname initial]
|
||||
# Example if your name is Ivan Lim:
|
||||
mkdir ivan_l
|
||||
|
||||
# Go into your new folder
|
||||
cd ivan_l
|
||||
|
||||
# Verify location
|
||||
pwd
|
||||
🎯 Get the Template Repository
|
||||
4. Access the Template Repository:
|
||||
Go to: https://gitea.techshare.cc/technolyceum/ai6-m3
|
||||
|
||||
Find the template named: [firstname]_[surname initial]_ai6-m3
|
||||
|
||||
Click on the template
|
||||
|
||||
Click the "Clone" button and copy the URL
|
||||
|
||||
5. Clone the Repository:
|
||||
powershell
|
||||
# Clone using the copied URL
|
||||
git clone https://gitea.techshare.cc/technolyceum/ai6-m3/[firstname]_[surname initial]_ai6-m3.git
|
||||
|
||||
# Go into the cloned folder
|
||||
cd [firstname]_[surname initial]_ai6-m3
|
||||
🐍 Set Up Virtual Environment
|
||||
6. Create and Activate Virtual Environment:
|
||||
powershell
|
||||
# Create virtual environment
|
||||
python -m venv venv
|
||||
|
||||
# Activate it
|
||||
.\venv\Scripts\Activate.ps1
|
||||
|
||||
# If you see a security warning:
|
||||
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||
.\venv\Scripts\Activate.ps1
|
||||
✅ Success: You should see (venv) at the start of your command line!
|
||||
|
||||
📝 Type Your Bot Code
|
||||
7. Open Python IDLE:
|
||||
powershell
|
||||
# Open IDLE from PowerShell
|
||||
python -m idlelib
|
||||
8. Create app.py in IDLE:
|
||||
In IDLE: Click File → New File
|
||||
|
||||
TYPE the following code line by line:
|
||||
|
||||
python
|
||||
from telegram import Update
|
||||
from telegram.ext import Application, CommandHandler, MessageHandler, filters
|
||||
import sqlite3
|
||||
|
||||
TOKEN = "TEACHER_WILL_PROVIDE_THIS"
|
||||
|
||||
waiting = {}
|
||||
|
||||
async def start(update, context):
|
||||
await update.message.reply_text("🤣 Joke Bot!\n/joke - get joke\n/addjoke - add joke")
|
||||
|
||||
async def joke(update, context):
|
||||
conn = sqlite3.connect('jokes.db')
|
||||
conn.execute('CREATE TABLE IF NOT EXISTS jokes (text TEXT, user TEXT, name TEXT)')
|
||||
joke = conn.execute("SELECT text FROM jokes ORDER BY RANDOM() LIMIT 1").fetchone()
|
||||
conn.close()
|
||||
await update.message.reply_text(joke[0] if joke else "No jokes! /addjoke to add one")
|
||||
|
||||
async def addjoke(update, context):
|
||||
waiting[update.effective_user.id] = True
|
||||
await update.message.reply_text("📝 Type your joke:")
|
||||
|
||||
async def handle_text(update, context):
|
||||
user_id = update.effective_user.id
|
||||
if user_id in waiting:
|
||||
conn = sqlite3.connect('jokes.db')
|
||||
conn.execute('CREATE TABLE IF NOT EXISTS jokes (text TEXT, user TEXT, name TEXT)')
|
||||
conn.execute("INSERT INTO jokes VALUES (?, ?, ?)",
|
||||
(update.message.text, user_id, update.effective_user.first_name))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
del waiting[user_id]
|
||||
await update.message.reply_text("✅ Saved!")
|
||||
else:
|
||||
await update.message.reply_text("Try /start")
|
||||
|
||||
app = Application.builder().token(TOKEN).build()
|
||||
app.add_handler(CommandHandler("start", start))
|
||||
app.add_handler(CommandHandler("joke", joke))
|
||||
app.add_handler(CommandHandler("addjoke", addjoke))
|
||||
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_text))
|
||||
print("Bot running!")
|
||||
app.run_polling()
|
||||
Click File → Save As
|
||||
|
||||
Navigate to your project folder
|
||||
|
||||
Save as: app.py
|
||||
|
||||
Click Save
|
||||
|
||||
📦 Install Dependencies
|
||||
9. Install Required Package:
|
||||
powershell
|
||||
# Make sure venv is activated
|
||||
pip install python-telegram-bot
|
||||
|
||||
# Check installation
|
||||
pip show python-telegram-bot
|
||||
10. Create requirements.txt:
|
||||
powershell
|
||||
pip freeze > requirements.txt
|
||||
Get-Content requirements.txt
|
||||
🗄️ Explore SQLite Database Using GUI
|
||||
11. First, Create the Database File:
|
||||
powershell
|
||||
# Run your bot once to create the database
|
||||
python app.py
|
||||
|
||||
# You'll see an error about the token - that's OK
|
||||
# Press Ctrl+C to stop it after 2 seconds
|
||||
12. Install DB Browser for SQLite:
|
||||
Go to: https://sqlitebrowser.org/dl/
|
||||
|
||||
Download the standard installer for Windows
|
||||
|
||||
Run the installer
|
||||
|
||||
Follow installation steps (just click Next)
|
||||
|
||||
Keep all default options
|
||||
|
||||
13. Open Your Database in DB Browser:
|
||||
Open File Explorer (Windows key + E)
|
||||
|
||||
Navigate to your project folder:
|
||||
|
||||
text
|
||||
C:\Users\YourName\Documents\[firstname]_[surname initial]\[firstname]_[surname initial]_ai6-m3
|
||||
Find jokes.db file
|
||||
|
||||
Double-click jokes.db
|
||||
|
||||
It should open in DB Browser for SQLite
|
||||
|
||||
If it doesn't, open DB Browser first, then click "Open Database"
|
||||
|
||||
14. Explore Database in DB Browser:
|
||||
Tab 1: Database Structure
|
||||
See the jokes table
|
||||
|
||||
See the 3 columns: text, user, name
|
||||
|
||||
Check data types: TEXT, TEXT, TEXT
|
||||
|
||||
Tab 2: Browse Data
|
||||
Click "Browse Data" tab
|
||||
|
||||
Select table: jokes
|
||||
|
||||
See any jokes already in the table
|
||||
|
||||
Table will be empty at first
|
||||
|
||||
15. Add Jokes Using DB Browser:
|
||||
Click "Browse Data" tab
|
||||
|
||||
Click New Record button (or press Insert key)
|
||||
|
||||
Fill in the fields:
|
||||
|
||||
text: Your joke (e.g., "Why did the chicken cross the road?")
|
||||
|
||||
user: Your user ID number (e.g., 12345)
|
||||
|
||||
name: Your name (e.g., "Ivan")
|
||||
|
||||
Click Apply button to save
|
||||
|
||||
Add 2-3 jokes this way
|
||||
|
||||
16. Edit Jokes Using DB Browser:
|
||||
Click on a joke in the table
|
||||
|
||||
Click inside any field
|
||||
|
||||
Change the text
|
||||
|
||||
Click Apply to save changes
|
||||
|
||||
17. Delete Jokes Using DB Browser:
|
||||
Click on a joke row
|
||||
|
||||
Click Delete Record button (or press Delete key)
|
||||
|
||||
Click Apply to confirm
|
||||
|
||||
18. Run SQL Queries in DB Browser:
|
||||
Click "Execute SQL" tab
|
||||
|
||||
Type SQL commands:
|
||||
|
||||
sql
|
||||
-- See all jokes
|
||||
SELECT * FROM jokes;
|
||||
|
||||
-- Count jokes
|
||||
SELECT COUNT(*) FROM jokes;
|
||||
|
||||
-- See specific user's jokes
|
||||
SELECT * FROM jokes WHERE name = 'Ivan';
|
||||
|
||||
-- Add a joke via SQL
|
||||
INSERT INTO jokes VALUES ('What do you call a bear with no teeth? A gummy bear!', 999, 'SQLUser');
|
||||
Click Execute (play button) to run
|
||||
|
||||
Click Write Changes to save
|
||||
|
||||
🔍 Quick Database Exploration Tasks
|
||||
Task 1: View Empty Database
|
||||
Open DB Browser
|
||||
|
||||
Open jokes.db
|
||||
|
||||
Check "Browse Data" tab
|
||||
|
||||
Table should be empty
|
||||
|
||||
Task 2: Add Jokes via GUI
|
||||
Click "New Record"
|
||||
|
||||
Add 3 different jokes
|
||||
|
||||
Use different names for each
|
||||
|
||||
Save all with "Apply"
|
||||
|
||||
Task 3: View What You Added
|
||||
Stay in "Browse Data" tab
|
||||
|
||||
Scroll to see all jokes
|
||||
|
||||
Notice each row has a rowid (automatic number)
|
||||
|
||||
Task 4: Edit a Joke
|
||||
Click on any joke
|
||||
|
||||
Change the text
|
||||
|
||||
Click "Apply"
|
||||
|
||||
Task 5: Delete a Joke
|
||||
Click on a joke row
|
||||
|
||||
Click "Delete Record"
|
||||
|
||||
Click "Apply"
|
||||
|
||||
Task 6: Use SQL Tab
|
||||
Go to "Execute SQL" tab
|
||||
|
||||
Type: SELECT COUNT(*) FROM jokes;
|
||||
|
||||
Click "Execute"
|
||||
|
||||
See result in bottom pane
|
||||
|
||||
🎯 What You Should See in DB Browser
|
||||
Database Structure:
|
||||
Tables: 1 table (jokes)
|
||||
|
||||
Columns: text, user, name (all TEXT type)
|
||||
|
||||
Row count: Shows at bottom
|
||||
|
||||
After Adding Jokes:
|
||||
Each row shows in table
|
||||
|
||||
Can sort by clicking column headers
|
||||
|
||||
Can filter using "Filter" box
|
||||
|
||||
File Size Growth:
|
||||
Each joke adds to file size
|
||||
|
||||
File updates instantly when you click "Apply"
|
||||
|
||||
💡 Tips for DB Browser
|
||||
Always click "Apply" after changes
|
||||
|
||||
"Write Changes" saves to disk
|
||||
|
||||
"Revert Changes" undoes unsaved changes
|
||||
|
||||
Use filter to find specific jokes
|
||||
|
||||
Export data if you want to backup
|
||||
|
||||
🔧 If Something Goes Wrong
|
||||
Database won't open:
|
||||
powershell
|
||||
# Delete and recreate
|
||||
Remove-Item jokes.db -ErrorAction SilentlyContinue
|
||||
|
||||
# Run bot to create fresh
|
||||
python app.py
|
||||
# Press Ctrl+C after error
|
||||
|
||||
# Now open in DB Browser
|
||||
DB Browser not installed:
|
||||
Go to https://sqlitebrowser.org/dl/
|
||||
|
||||
Download and install
|
||||
|
||||
Or use online SQLite viewer
|
||||
|
||||
Remember: DB Browser is the easiest way to see your database. No coding needed!
|
||||
|
||||
Note: Teacher will provide the Telegram bot token later. For now, practice adding and viewing jokes in DB Browser.
|
||||
12
jokes_bot/v3.0/database.py
Normal file
12
jokes_bot/v3.0/database.py
Normal file
@@ -0,0 +1,12 @@
|
||||
import sqlite3
|
||||
conn = sqlite3.connect('jokes.db')
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''CREATE TABLE IF NOT EXISTS jokes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
joke TEXT NOT NULL,
|
||||
contributor TEXT NOT NULL,
|
||||
published TEXT NOT NULL
|
||||
)''')
|
||||
conn.commit()
|
||||
print("✅ Database and table created successfully!")
|
||||
conn.close()
|
||||
BIN
jokes_bot/v3.0/jokes.db
Normal file
BIN
jokes_bot/v3.0/jokes.db
Normal file
Binary file not shown.
33
jokes_bot/v3.0/jokes.py
Normal file
33
jokes_bot/v3.0/jokes.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# simple_joke_menu.py
|
||||
import sqlite3
|
||||
import random
|
||||
|
||||
db = sqlite3.connect('jokes.db')
|
||||
|
||||
while True:
|
||||
print("\n1. Get joke")
|
||||
print("2. Add joke")
|
||||
print("3. Quit")
|
||||
|
||||
choice = input("Your choice: ")
|
||||
|
||||
if choice == "1":
|
||||
joke = db.execute("SELECT joke FROM jokes ORDER BY RANDOM() LIMIT 1").fetchone()
|
||||
if joke:
|
||||
print(f"\n🤣 {joke[0]}")
|
||||
else:
|
||||
print("No jokes yet!")
|
||||
|
||||
elif choice == "2":
|
||||
new = input("Your joke: ")
|
||||
if new:
|
||||
name = input("Your name: ") or "Friend"
|
||||
db.execute("INSERT INTO jokes (joke, contributor) VALUES (?, ?)", (new, name))
|
||||
db.commit()
|
||||
print("Saved!")
|
||||
|
||||
elif choice == "3":
|
||||
break
|
||||
|
||||
db.close()
|
||||
print("Bye!")
|
||||
1
jokes_bot/v3.0/requirements.txt
Normal file
1
jokes_bot/v3.0/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
python-telegram-bot
|
||||
42
jokes_bot/v3.0/sample_jokes.sql
Normal file
42
jokes_bot/v3.0/sample_jokes.sql
Normal file
@@ -0,0 +1,42 @@
|
||||
-- jokes_database_fixed.sql
|
||||
-- Create and populate jokes table with column name 'joke'
|
||||
|
||||
-- Step 1: Create the jokes table
|
||||
CREATE TABLE IF NOT EXISTS jokes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
joke TEXT NOT NULL, -- CHANGED FROM 'joke_text' TO 'joke'
|
||||
contributor TEXT,
|
||||
published DATE DEFAULT CURRENT_DATE
|
||||
);
|
||||
|
||||
-- Step 2: Insert 25 sample jokes
|
||||
INSERT INTO jokes (joke, contributor, published) VALUES -- CHANGED COLUMN NAME HERE
|
||||
('Why don''t scientists trust atoms? Because they make up everything!', 'Science Fan', '2024-01-15'),
|
||||
('Why did the scarecrow win an award? He was outstanding in his field!', 'Farm Humor', '2024-01-16'),
|
||||
('What do you call a fake noodle? An impasta!', 'Italian Chef', '2024-01-17'),
|
||||
('Why did the bicycle fall over? Because it was two-tired!', 'Bike Enthusiast', '2024-01-18'),
|
||||
('What do you call a bear with no teeth? A gummy bear!', 'Animal Lover', '2024-01-19'),
|
||||
('Why don''t eggs tell jokes? They''d crack each other up!', 'Breakfast Club', '2024-01-20'),
|
||||
('What do you call cheese that isn''t yours? Nacho cheese!', 'Cheese Master', '2024-01-21'),
|
||||
('Why did the math book look so sad? Because it had too many problems!', 'Math Teacher', '2024-01-22'),
|
||||
('What do you get when you cross a snowman with a vampire? Frostbite!', 'Winter Joker', '2024-01-23'),
|
||||
('Why don''t skeletons fight each other? They don''t have the guts!', 'Bone Collector', '2024-01-24'),
|
||||
('Why did the Python programmer quit? He didn''t get arrays!', 'Python Dev', '2024-01-25'),
|
||||
('What do computers eat for a snack? Microchips!', 'Tech Guru', '2024-01-26'),
|
||||
('Why do programmers prefer dark mode? Because light attracts bugs!', 'Code Master', '2024-01-27'),
|
||||
('How many programmers does it take to change a light bulb? None, that''s a hardware issue!', 'IT Department', '2024-01-28'),
|
||||
('Why did the database administrator leave his wife? She had one-to-many relationships!', 'SQL Expert', '2024-01-29'),
|
||||
('What''s a programmer''s favorite hangout place? Foo Bar!', 'Coder Club', '2024-01-30'),
|
||||
('Why don''t web developers go outside? They prefer the indoors network!', 'Web Dev', '2024-01-31'),
|
||||
('What do you call a programmer from Finland? Nerdic!', 'International Joker', '2024-02-01'),
|
||||
('Why did the developer go broke? Because he used up all his cache!', 'Finance Guy', '2024-02-02'),
|
||||
('What''s the object-oriented way to become wealthy? Inheritance!', 'OOP Fan', '2024-02-03'),
|
||||
('Why do Java developers wear glasses? Because they don''t C#!', 'Language Wars', '2024-02-04'),
|
||||
('What''s a programmer''s favorite animal? The Python!', 'Snake Charmer', '2024-02-05'),
|
||||
('Why did the computer go to the doctor? Because it had a virus!', 'IT Support', '2024-02-06'),
|
||||
('What do you call a computer that sings? A Dell!', 'Musical Tech', '2024-02-07'),
|
||||
('Why did the smartphone go to school? To improve its connection!', 'Mobile Guru', '2024-02-08');
|
||||
|
||||
-- Step 3: Verify the data was inserted
|
||||
SELECT '✅ Database created successfully!' as message;
|
||||
SELECT '📊 Total jokes inserted: ' || COUNT(*) as joke_count FROM jokes;
|
||||
BIN
jokes_bot/v4.0/.DS_Store
vendored
Normal file
BIN
jokes_bot/v4.0/.DS_Store
vendored
Normal file
Binary file not shown.
969
jokes_bot/v4.0/AI Joke Bot v2 - Community Ratings Added!.html
Normal file
969
jokes_bot/v4.0/AI Joke Bot v2 - Community Ratings Added!.html
Normal file
@@ -0,0 +1,969 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- saved from url=(0063)file:///Users/home/Downloads/deepseek_html_20260130_cd57ee.html -->
|
||||
<html lang="en"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AI Joke Bot v2 - Community Ratings Added!</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f8f9fa;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
height: calc(100vh - 60px);
|
||||
}
|
||||
|
||||
.slide {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
padding: 30px 40px;
|
||||
margin: 10px 0;
|
||||
display: none;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
max-height: 82vh;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.slide.active {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #2c3e50;
|
||||
font-size: 2.2rem;
|
||||
margin-bottom: 15px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #2c3e50;
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #2c3e50;
|
||||
font-size: 1.4rem;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.lesson-title {
|
||||
color: #2980b9;
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.subject-topic {
|
||||
color: #27ae60;
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.subject-title {
|
||||
text-align: center;
|
||||
color: #7f8c8d;
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 30px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.6;
|
||||
margin: 15px 0;
|
||||
color: #34495e;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
margin: 15px 0 20px 30px;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.6;
|
||||
color: #34495e;
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.rules-container {
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
padding: 25px;
|
||||
margin: 20px 0;
|
||||
border-left: 4px solid #3498db;
|
||||
}
|
||||
|
||||
.outcomes-container {
|
||||
background-color: #e8f4fd;
|
||||
border-radius: 6px;
|
||||
padding: 25px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.why-container {
|
||||
background-color: #f0f7ff;
|
||||
border-radius: 6px;
|
||||
padding: 25px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.terminal {
|
||||
background-color: #2c3e50;
|
||||
color: #ecf0f1;
|
||||
padding: 20px;
|
||||
border-radius: 6px;
|
||||
font-family: 'Courier New', monospace;
|
||||
margin: 20px 0;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.terminal-command {
|
||||
color: #2ecc71;
|
||||
}
|
||||
|
||||
.terminal-comment {
|
||||
color: #95a5a6;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: auto;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #3498db;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
font-size: 1rem;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
font-weight: 500;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #2980b9;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background-color: #bdc3c7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.slide-counter {
|
||||
text-align: center;
|
||||
color: #7f8c8d;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.lead {
|
||||
font-size: 1.2rem;
|
||||
color: #7f8c8d;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: #f1f2f3;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.feature-box {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.ai-icon {
|
||||
font-size: 2rem;
|
||||
text-align: center;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.sentiment-demo {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.sentiment-item {
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
flex: 1;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.positive {
|
||||
background-color: #d4edda;
|
||||
border: 2px solid #c3e6cb;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.neutral {
|
||||
background-color: #fff3cd;
|
||||
border: 2px solid #ffeaa7;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.negative {
|
||||
background-color: #f8d7da;
|
||||
border: 2px solid #f5c6cb;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.simple-explanation {
|
||||
background-color: #e8f4fd;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
margin: 20px 0;
|
||||
border-left: 5px solid #3498db;
|
||||
}
|
||||
|
||||
.simple-explanation h3 {
|
||||
color: #2980b9;
|
||||
}
|
||||
|
||||
.analogy-box {
|
||||
background-color: #fff3cd;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
margin: 20px 0;
|
||||
border: 2px dashed #f39c12;
|
||||
}
|
||||
|
||||
.cool-fact {
|
||||
background-color: #d5f4e6;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin: 15px 0;
|
||||
border-left: 4px solid #27ae60;
|
||||
}
|
||||
|
||||
.comparison-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.comparison-table th, .comparison-table td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.comparison-table th {
|
||||
background-color: #3498db;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.comparison-table tr:nth-child(even) {
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
|
||||
.database-diagram {
|
||||
background-color: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
margin: 20px 0;
|
||||
border: 2px solid #3498db;
|
||||
font-family: monospace;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.rating-demo {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.rating-button {
|
||||
font-size: 2rem;
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.rating-button:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.upvote {
|
||||
background-color: #d4edda;
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.neutral-vote {
|
||||
background-color: #fff3cd;
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.downvote {
|
||||
background-color: #f8d7da;
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.community-box {
|
||||
background-color: #e3f2fd;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
margin: 20px 0;
|
||||
border: 2px solid #2196f3;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.container {
|
||||
height: calc(100vh - 40px);
|
||||
}
|
||||
|
||||
.slide {
|
||||
padding: 20px 25px;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.lesson-title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.subject-topic {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
p, ul, ol {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 8px 16px;
|
||||
font-size: 0.95rem;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.sentiment-demo, .rating-demo {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.sentiment-item {
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="slide-counter" id="slide-counter">Slide 1 of 12</div>
|
||||
|
||||
<div class="container">
|
||||
<!-- Slide 1: Title Slide -->
|
||||
<div class="slide active" id="slide1">
|
||||
<div style="flex: 1; display: flex; flex-direction: column; justify-content: center;">
|
||||
<div class="ai-icon">🤖✨</div>
|
||||
<div class="lesson-title">Joke Bot v2.0 Launch!</div>
|
||||
<div class="subject-topic">AI + Community = Super Smart Bot!</div>
|
||||
<div class="subject-title">Now with community ratings and smarter AI!</div>
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<button id="prevBtn1" disabled="">Previous</button>
|
||||
<button id="nextBtn1">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slide 2: Rules -->
|
||||
<div class="slide" id="slide2">
|
||||
<h2>Class Rules</h2>
|
||||
|
||||
<div class="rules-container">
|
||||
<h3>How to Earn Points Today:</h3>
|
||||
|
||||
<p><strong>+2 Points</strong> - You're here and ready to learn!</p>
|
||||
<p><strong>+1 Point</strong> - You listen during instruction</p>
|
||||
<p><strong>+1 Point</strong> - You try all the activities</p>
|
||||
<p><strong>+1 Point</strong> - You complete all the work</p>
|
||||
|
||||
<div class="cool-fact">
|
||||
<p><strong>✨ Cool Fact:</strong> You helped build this bot! Your ratings make it smarter!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<button id="prevBtn2" disabled="">Previous</button>
|
||||
<button id="nextBtn2">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slide 3: Learning Outcomes -->
|
||||
<div class="slide" id="slide3">
|
||||
<h2>What's New in v2.0?</h2>
|
||||
<p class="subject-title">Our joke bot got a MAJOR upgrade!</p>
|
||||
|
||||
<div class="outcomes-container">
|
||||
<ol>
|
||||
<li><strong>🎭 Community Voting:</strong> Rate jokes with 👍, 👎, or 😐</li>
|
||||
<li><strong>🧠 AI vs Humans:</strong> Compare computer guesses with real votes</li>
|
||||
<li><strong>🗳️ Many-to-One:</strong> Many people can rate each joke</li>
|
||||
<li><strong>📊 Smart Statistics:</strong> See what jokes are REALLY popular</li>
|
||||
<li><strong>💾 Database Upgrade:</strong> New table for storing all votes</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="analogy-box">
|
||||
<p><strong>🎮 Think of it like this:</strong> Our joke bot was good, but now it's like adding multiplayer mode to a game!</p>
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<button id="prevBtn3" disabled="">Previous</button>
|
||||
<button id="nextBtn3">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slide 4: The Big Upgrade -->
|
||||
<div class="slide" id="slide4">
|
||||
<h2>The Big Upgrade: Community Ratings!</h2>
|
||||
|
||||
<div class="simple-explanation">
|
||||
<h3>v1.0: Just AI Guesses</h3>
|
||||
<p>Old system: Only the computer guessed if jokes were funny.</p>
|
||||
<div class="sentiment-demo">
|
||||
<div class="sentiment-item positive">
|
||||
<h4>AI Says: 😊 Positive</h4>
|
||||
<p>"Why don't eggs tell jokes?"</p>
|
||||
<small>Score: +0.90</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="feature-box">
|
||||
<h3>v2.0: AI + REAL People!</h3>
|
||||
<p>New system: Both computer AND humans vote on jokes!</p>
|
||||
|
||||
<div class="rating-demo">
|
||||
<div class="rating-button upvote">👍 85%</div>
|
||||
<div class="rating-button neutral-vote">😐 10%</div>
|
||||
<div class="rating-button downvote">👎 5%</div>
|
||||
</div>
|
||||
|
||||
<p><strong>Example Joke:</strong> "Why don't scientists trust atoms?"</p>
|
||||
<p><strong>AI Thinks:</strong> 😊 Positive (Score: +0.75)</p>
|
||||
<p><strong>Community Says:</strong> 👍 92% funny! (46 votes)</p>
|
||||
</div>
|
||||
|
||||
<div class="cool-fact">
|
||||
<p><strong>🤯 Mind Blown Fact:</strong> This is how YouTube, TikTok, and Netflix work - they combine AI guesses with real user ratings!</p>
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<button id="prevBtn4" disabled="">Previous</button>
|
||||
<button id="nextBtn4">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slide 5: How It Works Now -->
|
||||
<div class="slide" id="slide5">
|
||||
<h2>Try It Yourself! 🎯</h2>
|
||||
|
||||
<div class="simple-explanation">
|
||||
<h3>Step-by-Step Demo:</h3>
|
||||
<ol>
|
||||
<li><strong>Step 1:</strong> Bot shows you a joke</li>
|
||||
<li><strong>Step 2:</strong> You see AI's guess (😊/😐/😒)</li>
|
||||
<li><strong>Step 3:</strong> YOU get to vote! 👍, 👎, or 😐</li>
|
||||
<li><strong>Step 4:</strong> Your vote gets saved with everyone else's</li>
|
||||
<li><strong>Step 5:</strong> Next person sees the community score</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="community-box">
|
||||
<h3>📱 What You See Now:</h3>
|
||||
<div style="background-color: #2c3e50; color: white; padding: 20px; border-radius: 8px; margin: 15px 0;">
|
||||
<p>🤣 "Why don't eggs tell jokes? They'd crack each other up!"</p>
|
||||
<p>🤖 AI Mood: 😊 Positive (Score: +0.90)</p>
|
||||
<p>👥 Community Rating: 👍 85% | 😐 10% | 👎 5%</p>
|
||||
<p>🗳️ 124 people voted</p>
|
||||
<hr style="margin: 15px 0; border-color: #444;">
|
||||
<p><strong>Was this joke funny to YOU?</strong></p>
|
||||
<p>1. 👍 Upvote 2. 😐 Neutral 3. 👎 Downvote</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="analogy-box">
|
||||
<p><strong>💡 This is like:</strong> YouTube comments + TikTok likes + Netflix ratings all in one joke bot!</p>
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<button id="prevBtn5" disabled="">Previous</button>
|
||||
<button id="nextBtn5">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slide 6: Database Upgrade -->
|
||||
<div class="slide" id="slide6">
|
||||
<h2>Behind the Scenes: Database Magic 🗄️</h2>
|
||||
|
||||
<div class="simple-explanation">
|
||||
<h3>OLD Database (v1.0):</h3>
|
||||
<div class="database-diagram">
|
||||
jokes table:
|
||||
├── id
|
||||
├── joke_text
|
||||
├── contributor
|
||||
├── published_date
|
||||
├── ai_score ← Only AI's guess
|
||||
└── ai_mood_label
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="feature-box">
|
||||
<h3>NEW Database (v2.0):</h3>
|
||||
<div class="database-diagram" style="background-color: #e8f4fd; border-color: #2196f3;">
|
||||
jokes table: user_sentiments table:
|
||||
├── id ├── id
|
||||
├── joke_text ├── joke_id → Links to jokes
|
||||
├── contributor ├── user_name
|
||||
├── published_date ├── sentiment → 'up','down','neutral'
|
||||
├── ai_score ← AI's guess └── timestamp
|
||||
└── ai_mood_label └── ← MANY votes per joke!
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="analogy-box">
|
||||
<h3>🧩 Many-to-One Relationship:</h3>
|
||||
<ul>
|
||||
<li><strong>One Joke</strong> can have <strong>MANY votes</strong></li>
|
||||
<li>Like one YouTube video → many likes/comments</li>
|
||||
<li>Like one TikTok → many hearts</li>
|
||||
<li>This is called a "foreign key" relationship</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<button id="prevBtn6" disabled="">Previous</button>
|
||||
<button id="nextBtn6">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slide 7: The New Code -->
|
||||
<div class="slide" id="slide7">
|
||||
<h2>The Smart New Code ✨</h2>
|
||||
|
||||
<div class="content-container">
|
||||
<h3>1. New Database Table:</h3>
|
||||
<div class="terminal">
|
||||
<span class="terminal-command">CREATE TABLE user_sentiments (</span>
|
||||
id INTEGER PRIMARY KEY,
|
||||
joke_id INTEGER, <span class="terminal-comment"># Which joke</span>
|
||||
user_name TEXT, <span class="terminal-comment"># Who voted</span>
|
||||
sentiment TEXT, <span class="terminal-comment"># 'up','down','neutral'</span>
|
||||
timestamp TEXT, <span class="terminal-comment"># When they voted</span>
|
||||
FOREIGN KEY (joke_id) REFERENCES jokes(id) <span class="terminal-comment"># The magic link!</span>
|
||||
<span class="terminal-command">);</span>
|
||||
</div>
|
||||
|
||||
<h3>2. Asking for Your Vote:</h3>
|
||||
<div class="terminal">
|
||||
<span class="terminal-comment"># After showing a joke...</span>
|
||||
print("🤣 Why don't eggs tell jokes? They'd crack each other up!")
|
||||
print("🤖 AI thinks: 😊 Positive (Score: +0.90)")
|
||||
|
||||
<span class="terminal-comment"># Get community rating</span>
|
||||
<span class="terminal-command">community_score = get_community_rating(joke_id)</span>
|
||||
print(f"👥 Community: 👍 {community_score['up']}%")
|
||||
|
||||
<span class="terminal-comment"># Ask for YOUR vote!</span>
|
||||
print("🗳️ Was this funny to YOU?")
|
||||
print("1. 👍 Upvote 2. 😐 Neutral 3. 👎 Downvote")
|
||||
<span class="terminal-command">user_choice = input("Your choice: ")</span>
|
||||
|
||||
<span class="terminal-comment"># Save your vote</span>
|
||||
<span class="terminal-command">save_user_vote(joke_id, your_name, user_choice)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cool-fact">
|
||||
<p><strong>⚡ Pro Tip:</strong> The "FOREIGN KEY" is what links votes to specific jokes. It's like adding @mentions in social media!</p>
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<button id="prevBtn7" disabled="">Previous</button>
|
||||
<button id="nextBtn7">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slide 8: AI vs Humans - The Battle! -->
|
||||
<div class="slide" id="slide8">
|
||||
<h2>AI vs Humans - The Showdown! 🥊</h2>
|
||||
|
||||
<div class="simple-explanation">
|
||||
<h3>Now We Can Compare:</h3>
|
||||
<table class="comparison-table">
|
||||
<tbody><tr>
|
||||
<th>Joke</th>
|
||||
<th>🤖 AI Guesses</th>
|
||||
<th>👥 Humans Vote</th>
|
||||
<th>Who's Right?</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>"Why don't scientists trust atoms?"</td>
|
||||
<td>😊 Positive (+0.75)</td>
|
||||
<td>👍 92% funny</td>
|
||||
<td>✅ Both agree!</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>"Parallel lines have so much in common..."</td>
|
||||
<td>😒 Negative (-0.35)</td>
|
||||
<td>👍 78% funny</td>
|
||||
<td>❌ AI wrong!</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>"I told my computer I needed a break..."</td>
|
||||
<td>😐 Neutral (0.05)</td>
|
||||
<td>👎 65% not funny</td>
|
||||
<td>❌ AI wrong!</td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
</div>
|
||||
|
||||
<div class="analogy-box">
|
||||
<h3>🎯 What This Teaches Us:</h3>
|
||||
<ul>
|
||||
<li><strong>AI gets it right sometimes</strong> - Good at obvious jokes</li>
|
||||
<li><strong>AI gets it wrong sometimes</strong> - Bad at sarcasm/complex jokes</li>
|
||||
<li><strong>Humans are better judges</strong> - We understand context</li>
|
||||
<li><strong>Community voting is powerful</strong> - Wisdom of the crowd!</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="cool-fact">
|
||||
<p><strong>🧠 Real AI Learning:</strong> Some AI systems use human votes like this to LEARN and get better! Your votes could train future AI!</p>
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<button id="prevBtn8" disabled="">Previous</button>
|
||||
<button id="nextBtn8">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slide 9: Real-World Examples -->
|
||||
<div class="slide" id="slide9">
|
||||
<h2>Where You've Seen This Before 🎬</h2>
|
||||
|
||||
<div class="simple-explanation">
|
||||
<h3>Everywhere! This is how real apps work:</h3>
|
||||
|
||||
<div class="sentiment-demo">
|
||||
<div class="sentiment-item positive" style="text-align: left;">
|
||||
<h4>📱 YouTube</h4>
|
||||
<p>👍👎 buttons</p>
|
||||
<p>Comments + AI recommendations</p>
|
||||
<small>Exactly like our joke bot!</small>
|
||||
</div>
|
||||
<div class="sentiment-item neutral" style="text-align: left;">
|
||||
<h4>📸 TikTok/Instagram</h4>
|
||||
<p>❤️ hearts = upvotes</p>
|
||||
<p>AI suggests videos you'll like</p>
|
||||
<small>Community + AI working together</small>
|
||||
</div>
|
||||
<div class="sentiment-item negative" style="text-align: left;">
|
||||
<h4>🎮 Online Games</h4>
|
||||
<p>Report systems</p>
|
||||
<p>Player ratings</p>
|
||||
<small>Community moderation</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="community-box">
|
||||
<h3>🎯 The Big Idea:</h3>
|
||||
<p><strong>Modern apps = AI + Community Feedback</strong></p>
|
||||
<ul>
|
||||
<li><strong>AI starts the guess</strong> (like our TextBlob sentiment)</li>
|
||||
<li><strong>Humans give feedback</strong> (your 👍/👎 votes)</li>
|
||||
<li><strong>System gets smarter</strong> (learns from both)</li>
|
||||
<li><strong>Better experience for everyone!</strong></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="cool-fact">
|
||||
<p><strong>🚀 Career Connection:</strong> Knowing how to build AI + community systems is a SUPER valuable skill for app developers!</p>
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<button id="prevBtn9" disabled="">Previous</button>
|
||||
<button id="nextBtn9">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slide 10: What You Built -->
|
||||
<div class="slide" id="slide10">
|
||||
<h2>What YOU Built Today 🏗️</h2>
|
||||
|
||||
<div class="content-container">
|
||||
<p>Think about it - you helped build a system that:</p>
|
||||
|
||||
<ol>
|
||||
<li><strong>🤖 Uses AI</strong> to analyze text feelings</li>
|
||||
<li><strong>🗳️ Collects community votes</strong> from real people</li>
|
||||
<li><strong>💾 Stores everything</strong> in a smart database</li>
|
||||
<li><strong>🔗 Links votes to jokes</strong> with foreign keys</li>
|
||||
<li><strong>📊 Shows statistics</strong> comparing AI vs humans</li>
|
||||
</ol>
|
||||
|
||||
<div class="feature-box">
|
||||
<p><strong>🌟 That's Professional-Level Work!</strong></p>
|
||||
<p>Companies build apps the exact same way:</p>
|
||||
<ul>
|
||||
<li>YouTube = videos + likes/dislikes</li>
|
||||
<li>Amazon = products + star ratings</li>
|
||||
<li>Netflix = shows + thumbs up/down</li>
|
||||
<li><strong>Our Joke Bot = jokes + 👍/👎 ratings!</strong></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="analogy-box">
|
||||
<p><strong>💪 You're Not Just Learning - You're Building Real Stuff!</strong></p>
|
||||
<p>The skills you learned today (AI + databases + user feedback) are exactly what tech companies look for!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<button id="prevBtn10" disabled="">Previous</button>
|
||||
<button id="nextBtn10">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slide 11: Next Steps & Ideas -->
|
||||
<div class="slide" id="slide11">
|
||||
<h2>What Could We Build Next? 🚀</h2>
|
||||
|
||||
<div class="content-container">
|
||||
<h3>Discussion Time:</h3>
|
||||
<ul>
|
||||
<li>What jokes did the AI get totally wrong? Why?</li>
|
||||
<li>Should some people's votes count more? (Like joke experts?)</li>
|
||||
<li>What if we could "train" the AI with our votes?</li>
|
||||
<li>Could we predict which jokes will be popular?</li>
|
||||
</ul>
|
||||
|
||||
<div class="analogy-box">
|
||||
<h3>🎯 Cool Extension Ideas:</h3>
|
||||
<ul>
|
||||
<li><strong>Personalized Jokes:</strong> "Show me jokes that people LIKE ME enjoyed!"</li>
|
||||
<li><strong>Joke Leaderboard:</strong> Top 10 most-liked jokes</li>
|
||||
<li><strong>AI Trainer:</strong> Use votes to make TextBlob smarter</li>
|
||||
<li><strong>Meme Version:</strong> Same system but for memes!</li>
|
||||
<li><strong>Class Poll System:</strong> Use this code for classroom votes</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="community-box">
|
||||
<h3>🧠 The Real Magic:</h3>
|
||||
<p>You now have a template for ANY voting system:</p>
|
||||
<p>Song ratings, game reviews, teacher feedback, club elections...</p>
|
||||
<p><strong>Change the jokes to something else, keep the voting system!</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<button id="prevBtn11" disabled="">Previous</button>
|
||||
<button id="nextBtn11">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slide 12: Thank You -->
|
||||
<div class="slide" id="slide12">
|
||||
<div class="content-container">
|
||||
<div class="ai-icon">🤖👥✨</div>
|
||||
<h1>You Built Version 2.0!</h1>
|
||||
|
||||
<div class="analogy-box">
|
||||
<p><strong>🎯 Remember What You Accomplished:</strong></p>
|
||||
<ul>
|
||||
<li>Started with basic joke storage (v1.0)</li>
|
||||
<li>Added AI mood detection (v1.5)</li>
|
||||
<li>Now added COMMUNITY voting (v2.0!)</li>
|
||||
<li>You built a system used by billion-dollar companies</li>
|
||||
<li>You understand AI + Human collaboration</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="cool-fact">
|
||||
<p><strong>📈 Career Superpower:</strong> You now know how to build interactive, community-driven, AI-enhanced apps. That's literally what modern software development is all about!</p>
|
||||
</div>
|
||||
|
||||
<p style="margin-top: 30px;"><strong>Next Challenge:</strong> Can we make the AI LEARN from our votes? 🤔</p>
|
||||
|
||||
<p style="margin-top: 30px; color: #7f8c8d;">
|
||||
ICT & Digital Technologies<br>
|
||||
Year 9 - Building Real Apps
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<button id="prevBtn12" disabled="">Previous</button>
|
||||
<button id="nextBtn12">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ==================== CONFIGURATION ====================
|
||||
const totalSlides = 12;
|
||||
// =======================================================
|
||||
|
||||
let currentSlide = 1;
|
||||
|
||||
// Update slide counter
|
||||
function updateSlideCounter() {
|
||||
document.getElementById('slide-counter').textContent = `Slide ${currentSlide} of ${totalSlides}`;
|
||||
}
|
||||
|
||||
// Slide navigation
|
||||
function showSlide(slideNumber) {
|
||||
// Hide all slides
|
||||
for (let i = 1; i <= totalSlides; i++) {
|
||||
const slide = document.getElementById(`slide${i}`);
|
||||
if (slide) {
|
||||
slide.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
// Show current slide
|
||||
const currentSlideElement = document.getElementById(`slide${slideNumber}`);
|
||||
if (currentSlideElement) {
|
||||
currentSlideElement.classList.add('active');
|
||||
currentSlide = slideNumber;
|
||||
updateSlideCounter();
|
||||
|
||||
// Update button states
|
||||
updateButtons();
|
||||
}
|
||||
}
|
||||
|
||||
function updateButtons() {
|
||||
// Update previous button
|
||||
const prevButtons = document.querySelectorAll('[id^="prevBtn"]');
|
||||
prevButtons.forEach(btn => {
|
||||
btn.disabled = currentSlide === 1;
|
||||
});
|
||||
|
||||
// Update next button text on last slide
|
||||
const nextButtons = document.querySelectorAll('[id^="nextBtn"]');
|
||||
nextButtons.forEach(btn => {
|
||||
if (currentSlide === totalSlides) {
|
||||
btn.textContent = 'Mission Complete! 🎉';
|
||||
} else {
|
||||
btn.textContent = 'Next';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Next button functionality
|
||||
document.querySelectorAll('[id^="nextBtn"]').forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
if (currentSlide < totalSlides) {
|
||||
showSlide(currentSlide + 1);
|
||||
} else {
|
||||
// Last slide - completion message
|
||||
alert('🎉 INCREDIBLE WORK! You understand AI + Community systems - exactly how real apps work! Keep building amazing things! 🚀');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Previous button functionality
|
||||
document.querySelectorAll('[id^="prevBtn"]').forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
if (currentSlide > 1) {
|
||||
showSlide(currentSlide - 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize the presentation
|
||||
updateSlideCounter();
|
||||
updateButtons();
|
||||
|
||||
// Keyboard navigation
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'ArrowRight' || e.key === ' ' || e.key === 'PageDown') {
|
||||
if (currentSlide < totalSlides) {
|
||||
showSlide(currentSlide + 1);
|
||||
}
|
||||
} else if (e.key === 'ArrowLeft' || e.key === 'PageUp') {
|
||||
if (currentSlide > 1) {
|
||||
showSlide(currentSlide - 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize to first slide
|
||||
showSlide(1);
|
||||
</script>
|
||||
|
||||
</body></html>
|
||||
124
jokes_bot/v4.0/README.md
Normal file
124
jokes_bot/v4.0/README.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# AI-Enhanced Joke Bot v4.0
|
||||
|
||||
Welcome to the AI-Enhanced Joke Bot! This application combines humor with artificial intelligence to deliver jokes and analyze their sentiment.
|
||||
|
||||
## 📋 Project Structure
|
||||
|
||||
```
|
||||
v4.0/
|
||||
├── database.py # Creates the SQLite database and jokes table with user sentiment tracking
|
||||
├── jokes.py # Main application with AI sentiment analysis and user rating
|
||||
├── jokes.db # SQLite database containing jokes and user sentiments
|
||||
├── sample_data.sql # Sample jokes to populate the database
|
||||
├── populate_db.py # Script to populate the database with sample data
|
||||
├── check_db.py # Utility to check database content
|
||||
├── upgrade_db.py # Utility to upgrade existing databases with new schema
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### 1. Install Dependencies
|
||||
|
||||
First, make sure you have Python installed and then install the required packages:
|
||||
|
||||
```bash
|
||||
# Create and activate virtual environment (recommended)
|
||||
python -m venv venv
|
||||
source venv/bin/activate # On Windows: venv\Scripts\activate
|
||||
|
||||
# Install required packages
|
||||
pip install textblob
|
||||
|
||||
# Download required corpora for textblob
|
||||
python -m textblob.download_corpora
|
||||
```
|
||||
|
||||
### 2. Set Up the Database
|
||||
|
||||
Run the database creation script:
|
||||
|
||||
```bash
|
||||
python database.py
|
||||
```
|
||||
|
||||
This creates the `jokes.db` file with the proper table structure, including a table for user sentiments.
|
||||
|
||||
### 3. (Optional) Upgrade Existing Database
|
||||
|
||||
If you have an existing database from a previous version, run the upgrade script:
|
||||
|
||||
```bash
|
||||
python upgrade_db.py
|
||||
```
|
||||
|
||||
This adds the `user_sentiments` table to your existing database.
|
||||
|
||||
### 4. Populate the Database with Sample Jokes
|
||||
|
||||
To add sample jokes to your database:
|
||||
|
||||
```bash
|
||||
python populate_db.py
|
||||
```
|
||||
|
||||
Alternatively, you can use the SQL file directly:
|
||||
|
||||
```bash
|
||||
sqlite3 jokes.db < sample_data.sql
|
||||
```
|
||||
|
||||
### 5. Run the Application
|
||||
|
||||
Start the Joke Bot:
|
||||
|
||||
```bash
|
||||
python jokes.py
|
||||
```
|
||||
|
||||
## 🔧 Troubleshooting
|
||||
|
||||
### No Jokes in Database
|
||||
|
||||
If you're getting "No jokes in the database" message:
|
||||
|
||||
1. Make sure you've run `python database.py` to create the database
|
||||
2. Run `python populate_db.py` to add sample jokes to the database
|
||||
3. Verify the database content with `python check_db.py`
|
||||
|
||||
### Missing Dependencies
|
||||
|
||||
If you get an ImportError for textblob:
|
||||
|
||||
```bash
|
||||
pip install textblob
|
||||
python -m textblob.download_corpora
|
||||
```
|
||||
|
||||
## ⚙️ Features
|
||||
|
||||
- **Random Joke Generator**: Get a random joke from the database
|
||||
- **AI Sentiment Analysis**: Analyzes the sentiment of jokes using TextBlob
|
||||
- **User Sentiment Rating**: After each joke, rate it with thumbs up/down/neutral
|
||||
- **Community Ratings**: See how other users rated each joke
|
||||
- **Mood-Based Joke Selection**: Filter jokes by sentiment (positive, negative, neutral)
|
||||
- **Add New Jokes**: Contribute your own jokes to the database
|
||||
- **Joke Management**: View all jokes in the database with community ratings
|
||||
|
||||
## 💡 Usage Tips
|
||||
|
||||
- Choose option 1 to get a random joke, then rate it with U(p)/D(own)/N(eutral)
|
||||
- Choose option 2 to add a new joke with automatic sentiment analysis and rating
|
||||
- Choose option 3 to analyze the sentiment of any text
|
||||
- Choose option 4 to get jokes based on mood
|
||||
- Choose option 5 to view all jokes in the database with community ratings
|
||||
- Choose option 6 to exit the application
|
||||
|
||||
## 🗂️ Database Schema
|
||||
|
||||
The application uses two tables:
|
||||
|
||||
- `jokes`: Contains the joke text, contributor, publication date, and AI-analyzed sentiment
|
||||
- `user_sentiments`: Tracks user ratings for each joke with timestamps
|
||||
|
||||
Enjoy the AI-enhanced humor experience with community feedback!
|
||||
67
jokes_bot/v4.0/check_db.py
Normal file
67
jokes_bot/v4.0/check_db.py
Normal file
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple script to check the content of the jokes database
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import os
|
||||
|
||||
def check_database():
|
||||
db_path = 'jokes.db'
|
||||
|
||||
if not os.path.exists(db_path):
|
||||
print(f"❌ Database {db_path} does not exist!")
|
||||
return
|
||||
|
||||
print(f"🔍 Checking database: {db_path}")
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Count the number of jokes in the database
|
||||
cursor.execute('SELECT COUNT(*) FROM jokes')
|
||||
count = cursor.fetchone()[0]
|
||||
print(f"📊 Total jokes in database: {count}")
|
||||
|
||||
# Count the number of user sentiments
|
||||
cursor.execute('SELECT COUNT(*) FROM user_sentiments')
|
||||
sentiment_count = cursor.fetchone()[0]
|
||||
print(f"📊 Total user sentiments recorded: {sentiment_count}")
|
||||
|
||||
# If there are jokes, show a few of them
|
||||
if count > 0:
|
||||
cursor.execute('''
|
||||
SELECT j.id, j.joke, j.contributor, j.sentiment_label,
|
||||
(SELECT COUNT(*) FROM user_sentiments us WHERE us.joke_id = j.id) as sentiment_count
|
||||
FROM jokes j
|
||||
LIMIT 5
|
||||
''')
|
||||
jokes = cursor.fetchall()
|
||||
print('\n📋 Sample of jokes in the database:')
|
||||
for i, (joke_id, joke, contributor, sentiment, sentiment_count) in enumerate(jokes, 1):
|
||||
print(f'{i:2d}. [{sentiment}] {joke[:60]}...')
|
||||
print(f' 👤 {contributor} | {sentiment_count} user ratings')
|
||||
|
||||
# Show AI sentiment distribution
|
||||
cursor.execute('SELECT sentiment_label, COUNT(*) FROM jokes GROUP BY sentiment_label')
|
||||
distribution = cursor.fetchall()
|
||||
print(f'\n📈 AI Sentiment distribution:')
|
||||
for label, cnt in distribution:
|
||||
print(f' {label}: {cnt} jokes')
|
||||
|
||||
# Show user sentiment distribution if any exist
|
||||
if sentiment_count > 0:
|
||||
cursor.execute('SELECT user_sentiment, COUNT(*) FROM user_sentiments GROUP BY user_sentiment')
|
||||
user_distribution = cursor.fetchall()
|
||||
print(f'\n👥 User Sentiment distribution:')
|
||||
for sentiment, cnt in user_distribution:
|
||||
emoji = {'up': '👍', 'down': '👎', 'neutral': '😐'}[sentiment]
|
||||
print(f' {emoji} {sentiment.capitalize()}: {cnt} ratings')
|
||||
else:
|
||||
print("\n📭 No jokes found in the database!")
|
||||
print("💡 Run populate_db.py to add sample jokes to the database.")
|
||||
|
||||
conn.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
check_database()
|
||||
22
jokes_bot/v4.0/clean_sample_data.sql
Normal file
22
jokes_bot/v4.0/clean_sample_data.sql
Normal file
@@ -0,0 +1,22 @@
|
||||
-- Insert 20 dummy jokes with various sentiments
|
||||
INSERT INTO jokes (joke, contributor, published, sentiment_score, sentiment_label) VALUES
|
||||
('Why don''t scientists trust atoms? Because they make up everything!', 'ScienceFan', '2024-01-15 10:30:00', 0.75, '😊 Positive'),
|
||||
('I told my wife she was drawing her eyebrows too high. She looked surprised.', 'Joker123', '2024-01-16 14:20:00', 0.35, '😊 Positive'),
|
||||
('Why did the scarecrow win an award? He was outstanding in his field!', 'FarmLife', '2024-01-17 09:15:00', 0.65, '😊 Positive'),
|
||||
('What do you call a fish with no eyes? Fsh!', 'MarineBio', '2024-01-18 16:45:00', 0.25, '😊 Positive'),
|
||||
('I''m reading a book on anti-gravity. It''s impossible to put down!', 'PhysicsNerd', '2024-01-19 11:30:00', 0.45, '😊 Positive'),
|
||||
('Why did the computer go to the doctor? Because it had a virus.', 'TechSupport', '2024-01-20 13:10:00', 0.05, '😐 Neutral'),
|
||||
('What do you call a bear with no teeth? A gummy bear.', 'WildlifeFan', '2024-01-21 15:25:00', 0.08, '😐 Neutral'),
|
||||
('Why did the bicycle fall over? Because it was two-tired.', 'Cyclist', '2024-01-22 10:00:00', -0.02, '😐 Neutral'),
|
||||
('What do you call a sleeping bull? A bulldozer.', 'Cowboy', '2024-01-23 14:35:00', 0.03, '😐 Neutral'),
|
||||
('Why did the math book look so sad? Because it had too many problems.', 'Student', '2024-01-24 09:50:00', -0.05, '😐 Neutral'),
|
||||
('I used to play piano by ear, but now I use my hands.', 'Musician', '2024-01-25 12:15:00', -0.15, '😒 Negative'),
|
||||
('I told my computer I needed a break, and now it won''t stop sending me Kit-Kat ads.', 'OfficeWorker', '2024-01-26 16:30:00', -0.25, '😒 Negative'),
|
||||
('Parallel lines have so much in common. It''s a shame they''ll never meet.', 'MathTeacher', '2024-01-27 11:40:00', -0.35, '😒 Negative'),
|
||||
('My wife told me to stop impersonating a flamingo. I had to put my foot down.', 'Husband', '2024-01-28 14:55:00', -0.20, '😒 Negative'),
|
||||
('I told my girlfriend she drew her eyebrows too high. She seemed surprised.', 'Boyfriend', '2024-01-29 10:10:00', -0.30, '😒 Negative'),
|
||||
('What''s orange and sounds like a parrot? A carrot!', 'Vegetarian', '2024-01-30 13:20:00', 0.85, '😊 Positive'),
|
||||
('Why don''t eggs tell jokes? They''d crack each other up!', 'Chef', '2024-01-31 15:45:00', 0.90, '😊 Positive'),
|
||||
('I invented a new word: Plagiarism!', 'Writer', '2024-02-01 09:30:00', 0.78, '😊 Positive'),
|
||||
('Why did the golfer bring two pairs of pants? In case he got a hole in one!', 'Golfer', '2024-02-02 12:15:00', 0.82, '😊 Positive'),
|
||||
('What do you call a fake noodle? An impasta!', 'ItalianFood', '2024-02-03 14:40:00', 0.88, '😊 Positive');
|
||||
27
jokes_bot/v4.0/database.py
Normal file
27
jokes_bot/v4.0/database.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# database.py
|
||||
import sqlite3
|
||||
|
||||
conn = sqlite3.connect('jokes.db')
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''CREATE TABLE IF NOT EXISTS jokes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
joke TEXT NOT NULL,
|
||||
contributor TEXT NOT NULL,
|
||||
published TEXT NOT NULL,
|
||||
sentiment_score REAL DEFAULT 0.0,
|
||||
sentiment_label TEXT DEFAULT '😐 Neutral'
|
||||
)''')
|
||||
|
||||
# Create a new table to store user sentiments for each joke
|
||||
cursor.execute('''CREATE TABLE IF NOT EXISTS user_sentiments (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
joke_id INTEGER NOT NULL,
|
||||
user_sentiment TEXT CHECK(user_sentiment IN ('up', 'down', 'neutral')) DEFAULT 'neutral',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (joke_id) REFERENCES jokes(id) ON DELETE CASCADE
|
||||
)''')
|
||||
|
||||
conn.commit()
|
||||
print("✅ Database and tables created successfully with AI sentiment columns and user sentiment tracking!")
|
||||
conn.close()
|
||||
74
jokes_bot/v4.0/debug_db.py
Normal file
74
jokes_bot/v4.0/debug_db.py
Normal file
@@ -0,0 +1,74 @@
|
||||
# debug_db.py
|
||||
import sqlite3
|
||||
import os
|
||||
|
||||
def check_database():
|
||||
print("🔍 DATABASE DEBUG CHECK")
|
||||
print("=" * 40)
|
||||
|
||||
# Check current directory
|
||||
print(f"📁 Current directory: {os.getcwd()}")
|
||||
print(f"📁 Database file exists: {os.path.exists('jokes.db')}")
|
||||
|
||||
if not os.path.exists('jokes.db'):
|
||||
print("❌ ERROR: jokes.db file not found in current directory!")
|
||||
print("💡 Try running: python3 database.py first")
|
||||
return
|
||||
|
||||
try:
|
||||
# Connect to database
|
||||
conn = sqlite3.connect('jokes.db')
|
||||
cursor = conn.cursor()
|
||||
|
||||
print("✅ Connected to database successfully")
|
||||
|
||||
# Check what tables exist
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
||||
tables = cursor.fetchall()
|
||||
print(f"\n📋 Tables found: {[table[0] for table in tables]}")
|
||||
|
||||
if 'jokes' not in [table[0] for table in tables]:
|
||||
print("❌ ERROR: 'jokes' table not found!")
|
||||
print("💡 Try running: python3 database.py to create the table")
|
||||
conn.close()
|
||||
return
|
||||
|
||||
# Check number of jokes
|
||||
cursor.execute("SELECT COUNT(*) FROM jokes")
|
||||
count = cursor.fetchone()[0]
|
||||
print(f"📊 Total jokes in database: {count}")
|
||||
|
||||
if count == 0:
|
||||
print("⚠️ WARNING: Database is empty!")
|
||||
print("💡 Load sample data with: sqlite3 jokes.db < sample_data.sql")
|
||||
else:
|
||||
# Show some sample data
|
||||
print("\n🔍 Sample jokes (first 3):")
|
||||
print("-" * 50)
|
||||
cursor.execute("SELECT id, joke, contributor, sentiment_label FROM jokes LIMIT 3")
|
||||
jokes = cursor.fetchall()
|
||||
|
||||
for joke in jokes:
|
||||
print(f"\nID: {joke[0]}")
|
||||
print(f"Joke: {joke[1]}")
|
||||
print(f"Contributor: {joke[2]}")
|
||||
print(f"Mood: {joke[3]}")
|
||||
print("-" * 30)
|
||||
|
||||
# Check column names
|
||||
print("\n📝 Table structure:")
|
||||
cursor.execute("PRAGMA table_info(jokes)")
|
||||
columns = cursor.fetchall()
|
||||
for col in columns:
|
||||
print(f" {col[1]} ({col[2]})")
|
||||
|
||||
conn.close()
|
||||
print("\n✅ Debug check completed!")
|
||||
|
||||
except sqlite3.Error as e:
|
||||
print(f"❌ SQLite error: {e}")
|
||||
except Exception as e:
|
||||
print(f"❌ General error: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
check_database()
|
||||
BIN
jokes_bot/v4.0/jokes.db
Normal file
BIN
jokes_bot/v4.0/jokes.db
Normal file
Binary file not shown.
288
jokes_bot/v4.0/jokes.py
Normal file
288
jokes_bot/v4.0/jokes.py
Normal file
@@ -0,0 +1,288 @@
|
||||
# jokes_v4.py
|
||||
import sqlite3
|
||||
import random
|
||||
from datetime import datetime
|
||||
from textblob import TextBlob # Simple NLP library
|
||||
|
||||
def analyze_joke_sentiment(joke_text):
|
||||
"""Use AI to analyze the sentiment of a joke"""
|
||||
analysis = TextBlob(joke_text)
|
||||
polarity = analysis.sentiment.polarity
|
||||
|
||||
if polarity > 0.1:
|
||||
label = "😊 Positive"
|
||||
elif polarity < -0.1:
|
||||
label = "😒 Negative"
|
||||
else:
|
||||
label = "😐 Neutral"
|
||||
|
||||
return polarity, label
|
||||
|
||||
def get_user_sentiment_for_joke(db, joke_id):
|
||||
"""Get the average user sentiment for a specific joke"""
|
||||
cursor = db.execute('''
|
||||
SELECT
|
||||
CASE
|
||||
WHEN AVG(CASE WHEN user_sentiment = 'up' THEN 1
|
||||
WHEN user_sentiment = 'down' THEN -1
|
||||
ELSE 0 END) > 0.1 THEN '👍 Up'
|
||||
WHEN AVG(CASE WHEN user_sentiment = 'up' THEN 1
|
||||
WHEN user_sentiment = 'down' THEN -1
|
||||
ELSE 0 END) < -0.1 THEN '👎 Down'
|
||||
ELSE '😐 Neutral'
|
||||
END as avg_sentiment,
|
||||
COUNT(*) as total_votes
|
||||
FROM user_sentiments
|
||||
WHERE joke_id = ?
|
||||
''', (joke_id,))
|
||||
|
||||
result = cursor.fetchone()
|
||||
avg_sentiment, total_votes = result
|
||||
return avg_sentiment, total_votes
|
||||
|
||||
def add_user_sentiment(db, joke_id, user_choice):
|
||||
"""Add user sentiment for a specific joke"""
|
||||
try:
|
||||
db.execute('''
|
||||
INSERT INTO user_sentiments (joke_id, user_sentiment)
|
||||
VALUES (?, ?)
|
||||
''', (joke_id, user_choice))
|
||||
db.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ Error saving sentiment: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
db = sqlite3.connect('jokes.db')
|
||||
|
||||
while True:
|
||||
print("\n" + "="*30)
|
||||
print("🤖 AI-ENHANCED JOKE BOT 🤖")
|
||||
print("="*30)
|
||||
print("1. Get random joke")
|
||||
print("2. Add new joke")
|
||||
print("3. Analyze joke sentiment")
|
||||
print("4. Get joke by mood")
|
||||
print("5. View all jokes with sentiment")
|
||||
print("6. Quit")
|
||||
|
||||
choice = input("\nYour choice: ").strip()
|
||||
|
||||
if choice == "1":
|
||||
# Get random joke with sentiment info
|
||||
cursor = db.execute('''
|
||||
SELECT id, joke, contributor, published, sentiment_label
|
||||
FROM jokes
|
||||
ORDER BY RANDOM()
|
||||
LIMIT 1
|
||||
''')
|
||||
joke_data = cursor.fetchone()
|
||||
|
||||
if joke_data:
|
||||
joke_id, joke, contributor, published, sentiment = joke_data
|
||||
print(f"\n🤣 {joke}")
|
||||
print(f" 👤 Contributor: {contributor}")
|
||||
print(f" 📅 Published: {published}")
|
||||
print(f" 🧠 AI Mood Analysis: {sentiment}")
|
||||
|
||||
# Get and display user sentiment stats
|
||||
avg_sentiment, total_votes = get_user_sentiment_for_joke(db, joke_id)
|
||||
if total_votes > 0:
|
||||
print(f" 👥 Community Rating: {avg_sentiment} ({total_votes} votes)")
|
||||
|
||||
# Ask user for their sentiment
|
||||
print(f"\n🎯 Rate this joke: 👍 (U)p, 👎 (D)own, or (N)eutral?")
|
||||
user_input = input("Your choice (u/d/n): ").strip().lower()
|
||||
|
||||
if user_input in ['u', 'up']:
|
||||
user_choice = 'up'
|
||||
elif user_input in ['d', 'down']:
|
||||
user_choice = 'down'
|
||||
else:
|
||||
user_choice = 'neutral'
|
||||
|
||||
if add_user_sentiment(db, joke_id, user_choice):
|
||||
print(f"✅ Your rating ({'👍 Up' if user_choice == 'up' else '👎 Down' if user_choice == 'down' else '😐 Neutral'}) recorded!")
|
||||
else:
|
||||
print("❌ Could not save your rating.")
|
||||
else:
|
||||
print("📭 No jokes in the database yet!")
|
||||
|
||||
elif choice == "2":
|
||||
new_joke = input("Enter your joke: ").strip()
|
||||
if not new_joke:
|
||||
print("❌ Joke cannot be empty!")
|
||||
continue
|
||||
|
||||
name = input("Your name (or press Enter for 'Anonymous'): ").strip() or "Anonymous"
|
||||
|
||||
# AI Analysis
|
||||
score, label = analyze_joke_sentiment(new_joke)
|
||||
print(f"\n🤖 AI Analysis Results:")
|
||||
print(f" Sentiment Score: {score:.2f}")
|
||||
print(f" Mood Label: {label}")
|
||||
|
||||
# Get current date/time
|
||||
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
try:
|
||||
cursor = db.cursor()
|
||||
cursor.execute('''
|
||||
INSERT INTO jokes (joke, contributor, published, sentiment_score, sentiment_label)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
''', (new_joke, name, current_time, score, label))
|
||||
|
||||
# Get the ID of the newly inserted joke
|
||||
new_joke_id = cursor.lastrowid
|
||||
|
||||
db.commit()
|
||||
print("✅ Joke saved with AI analysis!")
|
||||
|
||||
# Ask user for their sentiment on the new joke
|
||||
print(f"\n🎯 Rate your own joke: 👍 (U)p, 👎 (D)own, or (N)eutral?")
|
||||
user_input = input("Your choice (u/d/n): ").strip().lower()
|
||||
|
||||
if user_input in ['u', 'up']:
|
||||
user_choice = 'up'
|
||||
elif user_input in ['d', 'down']:
|
||||
user_choice = 'down'
|
||||
else:
|
||||
user_choice = 'neutral'
|
||||
|
||||
if add_user_sentiment(db, new_joke_id, user_choice):
|
||||
print(f"✅ Your rating ({'👍 Up' if user_choice == 'up' else '👎 Down' if user_choice == 'down' else '😐 Neutral'}) recorded!")
|
||||
except Exception as e:
|
||||
print(f"❌ Error saving joke: {e}")
|
||||
|
||||
elif choice == "3":
|
||||
joke_text = input("Enter a joke to analyze: ").strip()
|
||||
if joke_text:
|
||||
score, label = analyze_joke_sentiment(joke_text)
|
||||
print(f"\n📊 AI Analysis Results:")
|
||||
print(f" Joke: '{joke_text}'")
|
||||
print(f" Sentiment Score: {score:.2f}")
|
||||
print(f" Mood Label: {label}")
|
||||
|
||||
# Interpretation
|
||||
print(f"\n📈 Interpretation:")
|
||||
if score > 0.5:
|
||||
print(" Very positive joke! 😄")
|
||||
elif score > 0.1:
|
||||
print(" Positive joke! 😊")
|
||||
elif score < -0.5:
|
||||
print(" Very negative/sarcastic joke! 😠")
|
||||
elif score < -0.1:
|
||||
print(" Negative joke! 😒")
|
||||
else:
|
||||
print(" Neutral joke! 😐")
|
||||
else:
|
||||
print("❌ Please enter a joke to analyze.")
|
||||
|
||||
elif choice == "4":
|
||||
print("\n🎭 Choose mood:")
|
||||
print("1. 😊 Positive jokes")
|
||||
print("2. 😒 Negative jokes")
|
||||
print("3. 😐 Neutral jokes")
|
||||
print("4. 😄 Very positive jokes (score > 0.5)")
|
||||
print("5. 😠 Very negative jokes (score < -0.5)")
|
||||
|
||||
mood_choice = input("Your choice: ").strip()
|
||||
|
||||
mood_queries = {
|
||||
"1": ("😊 Positive", "sentiment_label = '😊 Positive'"),
|
||||
"2": ("😒 Negative", "sentiment_label = '😒 Negative'"),
|
||||
"3": ("😐 Neutral", "sentiment_label = '😐 Neutral'"),
|
||||
"4": ("😄 Very Positive", "sentiment_score > 0.5"),
|
||||
"5": ("😠 Very Negative", "sentiment_score < -0.5")
|
||||
}
|
||||
|
||||
if mood_choice in mood_queries:
|
||||
mood_name, query = mood_queries[mood_choice]
|
||||
|
||||
cursor = db.execute(f'''
|
||||
SELECT id, joke, contributor, sentiment_score
|
||||
FROM jokes
|
||||
WHERE {query}
|
||||
ORDER BY RANDOM()
|
||||
LIMIT 1
|
||||
''')
|
||||
|
||||
joke_data = cursor.fetchone()
|
||||
|
||||
if joke_data:
|
||||
joke_id, joke, contributor, score = joke_data
|
||||
print(f"\n{mood_name} joke:")
|
||||
print(f"🤣 {joke}")
|
||||
print(f" 👤 Contributor: {contributor}")
|
||||
print(f" 📊 Sentiment Score: {score:.2f}")
|
||||
|
||||
# Get and display user sentiment stats
|
||||
avg_sentiment, total_votes = get_user_sentiment_for_joke(db, joke_id)
|
||||
if total_votes > 0:
|
||||
print(f" 👥 Community Rating: {avg_sentiment} ({total_votes} votes)")
|
||||
|
||||
# Ask user for their sentiment
|
||||
print(f"\n🎯 Rate this joke: 👍 (U)p, 👎 (D)own, or (N)eutral?")
|
||||
user_input = input("Your choice (u/d/n): ").strip().lower()
|
||||
|
||||
if user_input in ['u', 'up']:
|
||||
user_choice = 'up'
|
||||
elif user_input in ['d', 'down']:
|
||||
user_choice = 'down'
|
||||
else:
|
||||
user_choice = 'neutral'
|
||||
|
||||
if add_user_sentiment(db, joke_id, user_choice):
|
||||
print(f"✅ Your rating ({'👍 Up' if user_choice == 'up' else '👎 Down' if user_choice == 'down' else '😐 Neutral'}) recorded!")
|
||||
else:
|
||||
print(f"📭 No {mood_name.lower()} jokes yet!")
|
||||
else:
|
||||
print("❌ Invalid choice!")
|
||||
|
||||
elif choice == "5":
|
||||
print("\n📋 ALL JOKES IN DATABASE:")
|
||||
print("-" * 70)
|
||||
|
||||
cursor = db.execute('''
|
||||
SELECT j.id, j.joke, j.contributor, j.sentiment_label, j.sentiment_score
|
||||
FROM jokes j
|
||||
ORDER BY j.id DESC
|
||||
''')
|
||||
|
||||
jokes = cursor.fetchall()
|
||||
|
||||
if jokes:
|
||||
for i, (joke_id, joke, contributor, label, score) in enumerate(jokes, 1):
|
||||
print(f"\n{i}. {joke}")
|
||||
print(f" 👤 {contributor} | AI: {label} | Score: {score:.2f}")
|
||||
|
||||
# Get and display user sentiment stats
|
||||
avg_sentiment, total_votes = get_user_sentiment_for_joke(db, joke_id)
|
||||
if total_votes > 0:
|
||||
print(f" 👥 Community Rating: {avg_sentiment} ({total_votes} votes)")
|
||||
|
||||
print(f"\n📊 Total jokes: {len(jokes)}")
|
||||
else:
|
||||
print("📭 No jokes in the database yet!")
|
||||
|
||||
elif choice == "6":
|
||||
print("\n👋 Goodbye! Thanks for using the AI Joke Bot!")
|
||||
break
|
||||
|
||||
else:
|
||||
print("❌ Invalid choice. Please select 1-6.")
|
||||
|
||||
db.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Check if textblob is installed
|
||||
try:
|
||||
import textblob
|
||||
main()
|
||||
except ImportError:
|
||||
print("❌ ERROR: textblob library is not installed!")
|
||||
print("\n📦 Please install it using:")
|
||||
print(" pip install textblob")
|
||||
print(" python -m textblob.download_corpora")
|
||||
print("\nThen run this script again.")
|
||||
120
jokes_bot/v4.0/populate_db.py
Normal file
120
jokes_bot/v4.0/populate_db.py
Normal file
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script to populate the jokes database with sample data.
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import os
|
||||
|
||||
def populate_database():
|
||||
# Connect to the database
|
||||
db_path = 'jokes.db'
|
||||
if not os.path.exists(db_path):
|
||||
print(f"❌ Database {db_path} does not exist!")
|
||||
print("Please run database.py first to create the database.")
|
||||
return False
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check if jokes table is empty before adding data
|
||||
try:
|
||||
cursor.execute('SELECT COUNT(*) FROM jokes')
|
||||
count = cursor.fetchone()[0]
|
||||
except sqlite3.OperationalError:
|
||||
print("❌ The jokes table does not exist. Please run database.py first.")
|
||||
conn.close()
|
||||
return False
|
||||
|
||||
if count > 0:
|
||||
overwrite = input(f"⚠️ Database already contains {count} jokes. Overwrite? (y/N): ")
|
||||
if overwrite.lower() != 'y':
|
||||
print("❌ Operation cancelled.")
|
||||
conn.close()
|
||||
return False
|
||||
|
||||
# Clear existing data first
|
||||
# Only try to delete from user_sentiments if the table exists
|
||||
try:
|
||||
cursor.execute('DELETE FROM user_sentiments')
|
||||
except sqlite3.OperationalError:
|
||||
# user_sentiments table doesn't exist, which is OK
|
||||
print("ℹ️ user_sentiments table does not exist yet.")
|
||||
pass
|
||||
cursor.execute('DELETE FROM jokes')
|
||||
|
||||
# Use the clean SQL file without SELECT statements
|
||||
sql_file = 'clean_sample_data.sql'
|
||||
if not os.path.exists(sql_file):
|
||||
print(f"❌ SQL file {sql_file} does not exist!")
|
||||
return False
|
||||
|
||||
try:
|
||||
# Execute the clean SQL file directly
|
||||
with open(sql_file, 'r', encoding='utf-8') as f:
|
||||
sql_commands = f.read()
|
||||
|
||||
cursor.executescript(sql_commands)
|
||||
|
||||
conn.commit()
|
||||
print(f"✅ Successfully populated the jokes table with sample jokes!")
|
||||
|
||||
# Count the total number of jokes
|
||||
cursor.execute('SELECT COUNT(*) FROM jokes')
|
||||
count = cursor.fetchone()[0]
|
||||
print(f"📊 Total jokes in database: {count}")
|
||||
|
||||
# Add some sample user sentiments for the first few jokes
|
||||
print("🎯 Adding sample user sentiments...")
|
||||
jokes_with_sentiment = [(1, 'up'), (1, 'up'), (1, 'neutral'),
|
||||
(2, 'down'), (2, 'up'),
|
||||
(3, 'up'), (3, 'up'), (3, 'up')]
|
||||
|
||||
for joke_id, sentiment in jokes_with_sentiment:
|
||||
try:
|
||||
cursor.execute('''
|
||||
INSERT INTO user_sentiments (joke_id, user_sentiment)
|
||||
VALUES (?, ?)
|
||||
''', (joke_id, sentiment))
|
||||
except sqlite3.OperationalError:
|
||||
# If user_sentiments table doesn't exist, skip adding sentiments
|
||||
print("ℹ️ Skipping user sentiments as the table doesn't exist yet.")
|
||||
break
|
||||
|
||||
conn.commit()
|
||||
print(f"✅ Added sample user sentiments for {len(jokes_with_sentiment)} joke entries")
|
||||
|
||||
# Show sentiment distribution
|
||||
cursor.execute('SELECT sentiment_label, COUNT(*) FROM jokes GROUP BY sentiment_label')
|
||||
distribution = cursor.fetchall()
|
||||
print(f'\n📈 AI Sentiment distribution:')
|
||||
for label, cnt in distribution:
|
||||
print(f' {label}: {cnt} jokes')
|
||||
|
||||
# Show user sentiment distribution if the table exists
|
||||
try:
|
||||
cursor.execute('SELECT user_sentiment, COUNT(*) FROM user_sentiments GROUP BY user_sentiment')
|
||||
user_distribution = cursor.fetchall()
|
||||
if user_distribution:
|
||||
print(f'\n👥 User Sentiment distribution:')
|
||||
for sentiment, cnt in user_distribution:
|
||||
emoji = {'up': '👍', 'down': '👎', 'neutral': '😐'}[sentiment]
|
||||
print(f' {emoji} {sentiment.capitalize()}: {cnt} ratings')
|
||||
except sqlite3.OperationalError:
|
||||
print("\nℹ️ User sentiment table not available yet.")
|
||||
|
||||
conn.close()
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ Error executing SQL commands: {e}")
|
||||
conn.rollback()
|
||||
conn.close()
|
||||
return False
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("🔄 Populating jokes database with sample data...")
|
||||
success = populate_database()
|
||||
if success:
|
||||
print("\n🎉 Database successfully populated! You can now run jokes.py to enjoy the jokes.")
|
||||
else:
|
||||
print("\n💥 Failed to populate the database.")
|
||||
37
jokes_bot/v4.0/sample_data.sql
Normal file
37
jokes_bot/v4.0/sample_data.sql
Normal file
@@ -0,0 +1,37 @@
|
||||
-- Clear existing data first (optional - uncomment if needed)
|
||||
-- DELETE FROM jokes;
|
||||
|
||||
-- Insert 20 dummy jokes with various sentiments
|
||||
INSERT INTO jokes (joke, contributor, published, sentiment_score, sentiment_label) VALUES
|
||||
('Why don''t scientists trust atoms? Because they make up everything!', 'ScienceFan', '2024-01-15 10:30:00', 0.75, '😊 Positive'),
|
||||
('I told my wife she was drawing her eyebrows too high. She looked surprised.', 'Joker123', '2024-01-16 14:20:00', 0.35, '😊 Positive'),
|
||||
('Why did the scarecrow win an award? He was outstanding in his field!', 'FarmLife', '2024-01-17 09:15:00', 0.65, '😊 Positive'),
|
||||
('What do you call a fish with no eyes? Fsh!', 'MarineBio', '2024-01-18 16:45:00', 0.25, '😊 Positive'),
|
||||
('I''m reading a book on anti-gravity. It''s impossible to put down!', 'PhysicsNerd', '2024-01-19 11:30:00', 0.45, '😊 Positive'),
|
||||
('Why did the computer go to the doctor? Because it had a virus.', 'TechSupport', '2024-01-20 13:10:00', 0.05, '😐 Neutral'),
|
||||
('What do you call a bear with no teeth? A gummy bear.', 'WildlifeFan', '2024-01-21 15:25:00', 0.08, '😐 Neutral'),
|
||||
('Why did the bicycle fall over? Because it was two-tired.', 'Cyclist', '2024-01-22 10:00:00', -0.02, '😐 Neutral'),
|
||||
('What do you call a sleeping bull? A bulldozer.', 'Cowboy', '2024-01-23 14:35:00', 0.03, '😐 Neutral'),
|
||||
('Why did the math book look so sad? Because it had too many problems.', 'Student', '2024-01-24 09:50:00', -0.05, '😐 Neutral'),
|
||||
('I used to play piano by ear, but now I use my hands.', 'Musician', '2024-01-25 12:15:00', -0.15, '😒 Negative'),
|
||||
('I told my computer I needed a break, and now it won''t stop sending me Kit-Kat ads.', 'OfficeWorker', '2024-01-26 16:30:00', -0.25, '😒 Negative'),
|
||||
('Parallel lines have so much in common. It''s a shame they''ll never meet.', 'MathTeacher', '2024-01-27 11:40:00', -0.35, '😒 Negative'),
|
||||
('My wife told me to stop impersonating a flamingo. I had to put my foot down.', 'Husband', '2024-01-28 14:55:00', -0.20, '😒 Negative'),
|
||||
('I told my girlfriend she drew her eyebrows too high. She seemed surprised.', 'Boyfriend', '2024-01-29 10:10:00', -0.30, '😒 Negative'),
|
||||
('What''s orange and sounds like a parrot? A carrot!', 'Vegetarian', '2024-01-30 13:20:00', 0.85, '😊 Positive'),
|
||||
('Why don''t eggs tell jokes? They''d crack each other up!', 'Chef', '2024-01-31 15:45:00', 0.90, '😊 Positive'),
|
||||
('I invented a new word: Plagiarism!', 'Writer', '2024-02-01 09:30:00', 0.78, '😊 Positive'),
|
||||
('Why did the golfer bring two pairs of pants? In case he got a hole in one!', 'Golfer', '2024-02-02 12:15:00', 0.82, '😊 Positive'),
|
||||
('What do you call a fake noodle? An impasta!', 'ItalianFood', '2024-02-03 14:40:00', 0.88, '😊 Positive');
|
||||
|
||||
-- Show total count
|
||||
SELECT '✅ Inserted ' || COUNT(*) || ' jokes!' as message FROM jokes;
|
||||
|
||||
-- Show distribution by sentiment
|
||||
SELECT
|
||||
sentiment_label,
|
||||
COUNT(*) as count,
|
||||
'📊' as chart
|
||||
FROM jokes
|
||||
GROUP BY sentiment_label
|
||||
ORDER BY count DESC;
|
||||
62
jokes_bot/v4.0/setup_db.py
Normal file
62
jokes_bot/v4.0/setup_db.py
Normal file
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Setup script to properly initialize the database following the project specification.
|
||||
This ensures the database has the correct schema and sample data.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
def setup_database():
|
||||
db_path = 'jokes.db'
|
||||
|
||||
print("🚀 Starting database setup process...")
|
||||
|
||||
# Step 1: Remove existing database file if it exists
|
||||
if os.path.exists(db_path):
|
||||
print(f"🗑️ Removing existing database: {db_path}")
|
||||
os.remove(db_path)
|
||||
print("✅ Old database removed")
|
||||
else:
|
||||
print("📋 No existing database to remove")
|
||||
|
||||
# Step 2: Create database with correct schema
|
||||
print("\n🔧 Creating database with correct schema...")
|
||||
result = subprocess.run([sys.executable, 'database.py'], capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
print(f"❌ Error creating database: {result.stderr}")
|
||||
return False
|
||||
else:
|
||||
print("✅ Database schema created successfully")
|
||||
|
||||
# Step 3: Populate database with sample data
|
||||
print("\n📚 Populating database with sample data...")
|
||||
result = subprocess.run([sys.executable, 'populate_db.py'], capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
print(f"❌ Error populating database: {result.stderr}")
|
||||
return False
|
||||
else:
|
||||
print("✅ Database populated with sample data")
|
||||
|
||||
# Step 4: Verify the database
|
||||
print("\n🔍 Verifying database setup...")
|
||||
result = subprocess.run([sys.executable, 'check_db.py'], capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
print(f"❌ Error verifying database: {result.stderr}")
|
||||
return False
|
||||
else:
|
||||
print("✅ Database verified successfully")
|
||||
print(result.stdout)
|
||||
|
||||
print("\n🎉 Database setup completed successfully!")
|
||||
print("You can now run 'python jokes.py' to start the Joke Bot.")
|
||||
|
||||
return True
|
||||
|
||||
if __name__ == '__main__':
|
||||
success = setup_database()
|
||||
if not success:
|
||||
print("\n💥 Database setup failed. Please check the errors above.")
|
||||
sys.exit(1)
|
||||
51
jokes_bot/v4.0/upgrade_db.py
Normal file
51
jokes_bot/v4.0/upgrade_db.py
Normal file
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script to upgrade an existing database with the new user_sentiments table.
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import os
|
||||
|
||||
def upgrade_database():
|
||||
db_path = 'jokes.db'
|
||||
|
||||
if not os.path.exists(db_path):
|
||||
print(f"❌ Database {db_path} does not exist!")
|
||||
print("Please run database.py first to create the database.")
|
||||
return False
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check if user_sentiments table already exists
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='user_sentiments';")
|
||||
table_exists = cursor.fetchone()
|
||||
|
||||
if table_exists:
|
||||
print("✅ Database is already up to date!")
|
||||
conn.close()
|
||||
return True
|
||||
|
||||
print("🔄 Upgrading database schema...")
|
||||
|
||||
# Create the user_sentiments table
|
||||
cursor.execute('''CREATE TABLE user_sentiments (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
joke_id INTEGER NOT NULL,
|
||||
user_sentiment TEXT CHECK(user_sentiment IN ('up', 'down', 'neutral')) DEFAULT 'neutral',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (joke_id) REFERENCES jokes(id) ON DELETE CASCADE
|
||||
)''')
|
||||
|
||||
conn.commit()
|
||||
print("✅ Database upgraded successfully! Added user_sentiments table.")
|
||||
conn.close()
|
||||
return True
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("🔄 Checking database schema...")
|
||||
success = upgrade_database()
|
||||
if success:
|
||||
print("\n🎉 Database is ready for the enhanced application!")
|
||||
else:
|
||||
print("\n💥 Failed to upgrade the database.")
|
||||
241
jokes_bot/venv/bin/Activate.ps1
Normal file
241
jokes_bot/venv/bin/Activate.ps1
Normal file
@@ -0,0 +1,241 @@
|
||||
<#
|
||||
.Synopsis
|
||||
Activate a Python virtual environment for the current PowerShell session.
|
||||
|
||||
.Description
|
||||
Pushes the python executable for a virtual environment to the front of the
|
||||
$Env:PATH environment variable and sets the prompt to signify that you are
|
||||
in a Python virtual environment. Makes use of the command line switches as
|
||||
well as the `pyvenv.cfg` file values present in the virtual environment.
|
||||
|
||||
.Parameter VenvDir
|
||||
Path to the directory that contains the virtual environment to activate. The
|
||||
default value for this is the parent of the directory that the Activate.ps1
|
||||
script is located within.
|
||||
|
||||
.Parameter Prompt
|
||||
The prompt prefix to display when this virtual environment is activated. By
|
||||
default, this prompt is the name of the virtual environment folder (VenvDir)
|
||||
surrounded by parentheses and followed by a single space (ie. '(.venv) ').
|
||||
|
||||
.Example
|
||||
Activate.ps1
|
||||
Activates the Python virtual environment that contains the Activate.ps1 script.
|
||||
|
||||
.Example
|
||||
Activate.ps1 -Verbose
|
||||
Activates the Python virtual environment that contains the Activate.ps1 script,
|
||||
and shows extra information about the activation as it executes.
|
||||
|
||||
.Example
|
||||
Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
|
||||
Activates the Python virtual environment located in the specified location.
|
||||
|
||||
.Example
|
||||
Activate.ps1 -Prompt "MyPython"
|
||||
Activates the Python virtual environment that contains the Activate.ps1 script,
|
||||
and prefixes the current prompt with the specified string (surrounded in
|
||||
parentheses) while the virtual environment is active.
|
||||
|
||||
.Notes
|
||||
On Windows, it may be required to enable this Activate.ps1 script by setting the
|
||||
execution policy for the user. You can do this by issuing the following PowerShell
|
||||
command:
|
||||
|
||||
PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||
|
||||
For more information on Execution Policies:
|
||||
https://go.microsoft.com/fwlink/?LinkID=135170
|
||||
|
||||
#>
|
||||
Param(
|
||||
[Parameter(Mandatory = $false)]
|
||||
[String]
|
||||
$VenvDir,
|
||||
[Parameter(Mandatory = $false)]
|
||||
[String]
|
||||
$Prompt
|
||||
)
|
||||
|
||||
<# Function declarations --------------------------------------------------- #>
|
||||
|
||||
<#
|
||||
.Synopsis
|
||||
Remove all shell session elements added by the Activate script, including the
|
||||
addition of the virtual environment's Python executable from the beginning of
|
||||
the PATH variable.
|
||||
|
||||
.Parameter NonDestructive
|
||||
If present, do not remove this function from the global namespace for the
|
||||
session.
|
||||
|
||||
#>
|
||||
function global:deactivate ([switch]$NonDestructive) {
|
||||
# Revert to original values
|
||||
|
||||
# The prior prompt:
|
||||
if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
|
||||
Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
|
||||
Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
|
||||
}
|
||||
|
||||
# The prior PYTHONHOME:
|
||||
if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
|
||||
Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
|
||||
Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
|
||||
}
|
||||
|
||||
# The prior PATH:
|
||||
if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
|
||||
Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
|
||||
Remove-Item -Path Env:_OLD_VIRTUAL_PATH
|
||||
}
|
||||
|
||||
# Just remove the VIRTUAL_ENV altogether:
|
||||
if (Test-Path -Path Env:VIRTUAL_ENV) {
|
||||
Remove-Item -Path env:VIRTUAL_ENV
|
||||
}
|
||||
|
||||
# Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
|
||||
if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
|
||||
Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
|
||||
}
|
||||
|
||||
# Leave deactivate function in the global namespace if requested:
|
||||
if (-not $NonDestructive) {
|
||||
Remove-Item -Path function:deactivate
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.Description
|
||||
Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
|
||||
given folder, and returns them in a map.
|
||||
|
||||
For each line in the pyvenv.cfg file, if that line can be parsed into exactly
|
||||
two strings separated by `=` (with any amount of whitespace surrounding the =)
|
||||
then it is considered a `key = value` line. The left hand string is the key,
|
||||
the right hand is the value.
|
||||
|
||||
If the value starts with a `'` or a `"` then the first and last character is
|
||||
stripped from the value before being captured.
|
||||
|
||||
.Parameter ConfigDir
|
||||
Path to the directory that contains the `pyvenv.cfg` file.
|
||||
#>
|
||||
function Get-PyVenvConfig(
|
||||
[String]
|
||||
$ConfigDir
|
||||
) {
|
||||
Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
|
||||
|
||||
# Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
|
||||
$pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
|
||||
|
||||
# An empty map will be returned if no config file is found.
|
||||
$pyvenvConfig = @{ }
|
||||
|
||||
if ($pyvenvConfigPath) {
|
||||
|
||||
Write-Verbose "File exists, parse `key = value` lines"
|
||||
$pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
|
||||
|
||||
$pyvenvConfigContent | ForEach-Object {
|
||||
$keyval = $PSItem -split "\s*=\s*", 2
|
||||
if ($keyval[0] -and $keyval[1]) {
|
||||
$val = $keyval[1]
|
||||
|
||||
# Remove extraneous quotations around a string value.
|
||||
if ("'""".Contains($val.Substring(0, 1))) {
|
||||
$val = $val.Substring(1, $val.Length - 2)
|
||||
}
|
||||
|
||||
$pyvenvConfig[$keyval[0]] = $val
|
||||
Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
|
||||
}
|
||||
}
|
||||
}
|
||||
return $pyvenvConfig
|
||||
}
|
||||
|
||||
|
||||
<# Begin Activate script --------------------------------------------------- #>
|
||||
|
||||
# Determine the containing directory of this script
|
||||
$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
$VenvExecDir = Get-Item -Path $VenvExecPath
|
||||
|
||||
Write-Verbose "Activation script is located in path: '$VenvExecPath'"
|
||||
Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
|
||||
Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
|
||||
|
||||
# Set values required in priority: CmdLine, ConfigFile, Default
|
||||
# First, get the location of the virtual environment, it might not be
|
||||
# VenvExecDir if specified on the command line.
|
||||
if ($VenvDir) {
|
||||
Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
|
||||
}
|
||||
else {
|
||||
Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
|
||||
$VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
|
||||
Write-Verbose "VenvDir=$VenvDir"
|
||||
}
|
||||
|
||||
# Next, read the `pyvenv.cfg` file to determine any required value such
|
||||
# as `prompt`.
|
||||
$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
|
||||
|
||||
# Next, set the prompt from the command line, or the config file, or
|
||||
# just use the name of the virtual environment folder.
|
||||
if ($Prompt) {
|
||||
Write-Verbose "Prompt specified as argument, using '$Prompt'"
|
||||
}
|
||||
else {
|
||||
Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
|
||||
if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
|
||||
Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
|
||||
$Prompt = $pyvenvCfg['prompt'];
|
||||
}
|
||||
else {
|
||||
Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)"
|
||||
Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
|
||||
$Prompt = Split-Path -Path $venvDir -Leaf
|
||||
}
|
||||
}
|
||||
|
||||
Write-Verbose "Prompt = '$Prompt'"
|
||||
Write-Verbose "VenvDir='$VenvDir'"
|
||||
|
||||
# Deactivate any currently active virtual environment, but leave the
|
||||
# deactivate function in place.
|
||||
deactivate -nondestructive
|
||||
|
||||
# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
|
||||
# that there is an activated venv.
|
||||
$env:VIRTUAL_ENV = $VenvDir
|
||||
|
||||
if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
|
||||
|
||||
Write-Verbose "Setting prompt to '$Prompt'"
|
||||
|
||||
# Set the prompt to include the env name
|
||||
# Make sure _OLD_VIRTUAL_PROMPT is global
|
||||
function global:_OLD_VIRTUAL_PROMPT { "" }
|
||||
Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
|
||||
New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
|
||||
|
||||
function global:prompt {
|
||||
Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
|
||||
_OLD_VIRTUAL_PROMPT
|
||||
}
|
||||
}
|
||||
|
||||
# Clear PYTHONHOME
|
||||
if (Test-Path -Path Env:PYTHONHOME) {
|
||||
Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
|
||||
Remove-Item -Path Env:PYTHONHOME
|
||||
}
|
||||
|
||||
# Add the venv to the PATH
|
||||
Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
|
||||
$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"
|
||||
66
jokes_bot/venv/bin/activate
Normal file
66
jokes_bot/venv/bin/activate
Normal file
@@ -0,0 +1,66 @@
|
||||
# This file must be used with "source bin/activate" *from bash*
|
||||
# you cannot run it directly
|
||||
|
||||
deactivate () {
|
||||
# reset old environment variables
|
||||
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
|
||||
PATH="${_OLD_VIRTUAL_PATH:-}"
|
||||
export PATH
|
||||
unset _OLD_VIRTUAL_PATH
|
||||
fi
|
||||
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
|
||||
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
|
||||
export PYTHONHOME
|
||||
unset _OLD_VIRTUAL_PYTHONHOME
|
||||
fi
|
||||
|
||||
# This should detect bash and zsh, which have a hash command that must
|
||||
# be called to get it to forget past commands. Without forgetting
|
||||
# past commands the $PATH changes we made may not be respected
|
||||
if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
|
||||
hash -r 2> /dev/null
|
||||
fi
|
||||
|
||||
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
|
||||
PS1="${_OLD_VIRTUAL_PS1:-}"
|
||||
export PS1
|
||||
unset _OLD_VIRTUAL_PS1
|
||||
fi
|
||||
|
||||
unset VIRTUAL_ENV
|
||||
if [ ! "${1:-}" = "nondestructive" ] ; then
|
||||
# Self destruct!
|
||||
unset -f deactivate
|
||||
fi
|
||||
}
|
||||
|
||||
# unset irrelevant variables
|
||||
deactivate nondestructive
|
||||
|
||||
VIRTUAL_ENV="/Users/home/YandexDisk/TECHNOLYCEUM/ict/Year/2025/ai/ai6/ai6-m3/ai6-m3/jokes-bot-v3.0/venv"
|
||||
export VIRTUAL_ENV
|
||||
|
||||
_OLD_VIRTUAL_PATH="$PATH"
|
||||
PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
export PATH
|
||||
|
||||
# unset PYTHONHOME if set
|
||||
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
|
||||
# could use `if (set -u; : $PYTHONHOME) ;` in bash
|
||||
if [ -n "${PYTHONHOME:-}" ] ; then
|
||||
_OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
|
||||
unset PYTHONHOME
|
||||
fi
|
||||
|
||||
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
|
||||
_OLD_VIRTUAL_PS1="${PS1:-}"
|
||||
PS1="(venv) ${PS1:-}"
|
||||
export PS1
|
||||
fi
|
||||
|
||||
# This should detect bash and zsh, which have a hash command that must
|
||||
# be called to get it to forget past commands. Without forgetting
|
||||
# past commands the $PATH changes we made may not be respected
|
||||
if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
|
||||
hash -r 2> /dev/null
|
||||
fi
|
||||
25
jokes_bot/venv/bin/activate.csh
Normal file
25
jokes_bot/venv/bin/activate.csh
Normal file
@@ -0,0 +1,25 @@
|
||||
# This file must be used with "source bin/activate.csh" *from csh*.
|
||||
# You cannot run it directly.
|
||||
# Created by Davide Di Blasi <davidedb@gmail.com>.
|
||||
# Ported to Python 3.3 venv by Andrew Svetlov <andrew.svetlov@gmail.com>
|
||||
|
||||
alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; test "\!:*" != "nondestructive" && unalias deactivate'
|
||||
|
||||
# Unset irrelevant variables.
|
||||
deactivate nondestructive
|
||||
|
||||
setenv VIRTUAL_ENV "/Users/home/YandexDisk/TECHNOLYCEUM/ict/Year/2025/ai/ai6/ai6-m3/ai6-m3/jokes-bot-v3.0/venv"
|
||||
|
||||
set _OLD_VIRTUAL_PATH="$PATH"
|
||||
setenv PATH "$VIRTUAL_ENV/bin:$PATH"
|
||||
|
||||
|
||||
set _OLD_VIRTUAL_PROMPT="$prompt"
|
||||
|
||||
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
|
||||
set prompt = "(venv) $prompt"
|
||||
endif
|
||||
|
||||
alias pydoc python -m pydoc
|
||||
|
||||
rehash
|
||||
64
jokes_bot/venv/bin/activate.fish
Normal file
64
jokes_bot/venv/bin/activate.fish
Normal file
@@ -0,0 +1,64 @@
|
||||
# This file must be used with "source <venv>/bin/activate.fish" *from fish*
|
||||
# (https://fishshell.com/); you cannot run it directly.
|
||||
|
||||
function deactivate -d "Exit virtual environment and return to normal shell environment"
|
||||
# reset old environment variables
|
||||
if test -n "$_OLD_VIRTUAL_PATH"
|
||||
set -gx PATH $_OLD_VIRTUAL_PATH
|
||||
set -e _OLD_VIRTUAL_PATH
|
||||
end
|
||||
if test -n "$_OLD_VIRTUAL_PYTHONHOME"
|
||||
set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
|
||||
set -e _OLD_VIRTUAL_PYTHONHOME
|
||||
end
|
||||
|
||||
if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
|
||||
functions -e fish_prompt
|
||||
set -e _OLD_FISH_PROMPT_OVERRIDE
|
||||
functions -c _old_fish_prompt fish_prompt
|
||||
functions -e _old_fish_prompt
|
||||
end
|
||||
|
||||
set -e VIRTUAL_ENV
|
||||
if test "$argv[1]" != "nondestructive"
|
||||
# Self-destruct!
|
||||
functions -e deactivate
|
||||
end
|
||||
end
|
||||
|
||||
# Unset irrelevant variables.
|
||||
deactivate nondestructive
|
||||
|
||||
set -gx VIRTUAL_ENV "/Users/home/YandexDisk/TECHNOLYCEUM/ict/Year/2025/ai/ai6/ai6-m3/ai6-m3/jokes-bot-v3.0/venv"
|
||||
|
||||
set -gx _OLD_VIRTUAL_PATH $PATH
|
||||
set -gx PATH "$VIRTUAL_ENV/bin" $PATH
|
||||
|
||||
# Unset PYTHONHOME if set.
|
||||
if set -q PYTHONHOME
|
||||
set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
|
||||
set -e PYTHONHOME
|
||||
end
|
||||
|
||||
if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
|
||||
# fish uses a function instead of an env var to generate the prompt.
|
||||
|
||||
# Save the current fish_prompt function as the function _old_fish_prompt.
|
||||
functions -c fish_prompt _old_fish_prompt
|
||||
|
||||
# With the original prompt function renamed, we can override with our own.
|
||||
function fish_prompt
|
||||
# Save the return status of the last command.
|
||||
set -l old_status $status
|
||||
|
||||
# Output the venv prompt; color taken from the blue of the Python logo.
|
||||
printf "%s%s%s" (set_color 4B8BBE) "(venv) " (set_color normal)
|
||||
|
||||
# Restore the return status of the previous command.
|
||||
echo "exit $old_status" | .
|
||||
# Output the original/"old" prompt.
|
||||
_old_fish_prompt
|
||||
end
|
||||
|
||||
set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
|
||||
end
|
||||
8
jokes_bot/venv/bin/httpx
Executable file
8
jokes_bot/venv/bin/httpx
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/Users/home/YandexDisk/TECHNOLYCEUM/ict/Year/2025/ai/ai6/ai6-m3/ai6-m3/jokes-bot-v3.0/venv/bin/python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from httpx import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
||||
8
jokes_bot/venv/bin/pip
Executable file
8
jokes_bot/venv/bin/pip
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/Users/home/YandexDisk/TECHNOLYCEUM/ict/Year/2025/ai/ai6/ai6-m3/ai6-m3/jokes-bot-v3.0/venv/bin/python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from pip._internal.cli.main import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
||||
8
jokes_bot/venv/bin/pip3
Executable file
8
jokes_bot/venv/bin/pip3
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/Users/home/YandexDisk/TECHNOLYCEUM/ict/Year/2025/ai/ai6/ai6-m3/ai6-m3/jokes-bot-v3.0/venv/bin/python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from pip._internal.cli.main import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
||||
8
jokes_bot/venv/bin/pip3.9
Executable file
8
jokes_bot/venv/bin/pip3.9
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/Users/home/YandexDisk/TECHNOLYCEUM/ict/Year/2025/ai/ai6/ai6-m3/ai6-m3/jokes-bot-v3.0/venv/bin/python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from pip._internal.cli.main import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
||||
1
jokes_bot/venv/bin/python
Symbolic link
1
jokes_bot/venv/bin/python
Symbolic link
@@ -0,0 +1 @@
|
||||
python3.9
|
||||
1
jokes_bot/venv/bin/python3
Symbolic link
1
jokes_bot/venv/bin/python3
Symbolic link
@@ -0,0 +1 @@
|
||||
python3.9
|
||||
1
jokes_bot/venv/bin/python3.9
Symbolic link
1
jokes_bot/venv/bin/python3.9
Symbolic link
@@ -0,0 +1 @@
|
||||
/usr/local/opt/python@3.9/bin/python3.9
|
||||
Binary file not shown.
@@ -0,0 +1,227 @@
|
||||
# don't import any costly modules
|
||||
import sys
|
||||
import os
|
||||
|
||||
|
||||
is_pypy = '__pypy__' in sys.builtin_module_names
|
||||
|
||||
|
||||
def warn_distutils_present():
|
||||
if 'distutils' not in sys.modules:
|
||||
return
|
||||
if is_pypy and sys.version_info < (3, 7):
|
||||
# PyPy for 3.6 unconditionally imports distutils, so bypass the warning
|
||||
# https://foss.heptapod.net/pypy/pypy/-/blob/be829135bc0d758997b3566062999ee8b23872b4/lib-python/3/site.py#L250
|
||||
return
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
"Distutils was imported before Setuptools, but importing Setuptools "
|
||||
"also replaces the `distutils` module in `sys.modules`. This may lead "
|
||||
"to undesirable behaviors or errors. To avoid these issues, avoid "
|
||||
"using distutils directly, ensure that setuptools is installed in the "
|
||||
"traditional way (e.g. not an editable install), and/or make sure "
|
||||
"that setuptools is always imported before distutils."
|
||||
)
|
||||
|
||||
|
||||
def clear_distutils():
|
||||
if 'distutils' not in sys.modules:
|
||||
return
|
||||
import warnings
|
||||
|
||||
warnings.warn("Setuptools is replacing distutils.")
|
||||
mods = [
|
||||
name
|
||||
for name in sys.modules
|
||||
if name == "distutils" or name.startswith("distutils.")
|
||||
]
|
||||
for name in mods:
|
||||
del sys.modules[name]
|
||||
|
||||
|
||||
def enabled():
|
||||
"""
|
||||
Allow selection of distutils by environment variable.
|
||||
"""
|
||||
which = os.environ.get('SETUPTOOLS_USE_DISTUTILS', 'local')
|
||||
return which == 'local'
|
||||
|
||||
|
||||
def ensure_local_distutils():
|
||||
import importlib
|
||||
|
||||
clear_distutils()
|
||||
|
||||
# With the DistutilsMetaFinder in place,
|
||||
# perform an import to cause distutils to be
|
||||
# loaded from setuptools._distutils. Ref #2906.
|
||||
with shim():
|
||||
importlib.import_module('distutils')
|
||||
|
||||
# check that submodules load as expected
|
||||
core = importlib.import_module('distutils.core')
|
||||
assert '_distutils' in core.__file__, core.__file__
|
||||
assert 'setuptools._distutils.log' not in sys.modules
|
||||
|
||||
|
||||
def do_override():
|
||||
"""
|
||||
Ensure that the local copy of distutils is preferred over stdlib.
|
||||
|
||||
See https://github.com/pypa/setuptools/issues/417#issuecomment-392298401
|
||||
for more motivation.
|
||||
"""
|
||||
if enabled():
|
||||
warn_distutils_present()
|
||||
ensure_local_distutils()
|
||||
|
||||
|
||||
class _TrivialRe:
|
||||
def __init__(self, *patterns):
|
||||
self._patterns = patterns
|
||||
|
||||
def match(self, string):
|
||||
return all(pat in string for pat in self._patterns)
|
||||
|
||||
|
||||
class DistutilsMetaFinder:
|
||||
def find_spec(self, fullname, path, target=None):
|
||||
# optimization: only consider top level modules and those
|
||||
# found in the CPython test suite.
|
||||
if path is not None and not fullname.startswith('test.'):
|
||||
return
|
||||
|
||||
method_name = 'spec_for_{fullname}'.format(**locals())
|
||||
method = getattr(self, method_name, lambda: None)
|
||||
return method()
|
||||
|
||||
def spec_for_distutils(self):
|
||||
if self.is_cpython():
|
||||
return
|
||||
|
||||
import importlib
|
||||
import importlib.abc
|
||||
import importlib.util
|
||||
|
||||
try:
|
||||
mod = importlib.import_module('setuptools._distutils')
|
||||
except Exception:
|
||||
# There are a couple of cases where setuptools._distutils
|
||||
# may not be present:
|
||||
# - An older Setuptools without a local distutils is
|
||||
# taking precedence. Ref #2957.
|
||||
# - Path manipulation during sitecustomize removes
|
||||
# setuptools from the path but only after the hook
|
||||
# has been loaded. Ref #2980.
|
||||
# In either case, fall back to stdlib behavior.
|
||||
return
|
||||
|
||||
class DistutilsLoader(importlib.abc.Loader):
|
||||
def create_module(self, spec):
|
||||
mod.__name__ = 'distutils'
|
||||
return mod
|
||||
|
||||
def exec_module(self, module):
|
||||
pass
|
||||
|
||||
return importlib.util.spec_from_loader(
|
||||
'distutils', DistutilsLoader(), origin=mod.__file__
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def is_cpython():
|
||||
"""
|
||||
Suppress supplying distutils for CPython (build and tests).
|
||||
Ref #2965 and #3007.
|
||||
"""
|
||||
return os.path.isfile('pybuilddir.txt')
|
||||
|
||||
def spec_for_pip(self):
|
||||
"""
|
||||
Ensure stdlib distutils when running under pip.
|
||||
See pypa/pip#8761 for rationale.
|
||||
"""
|
||||
if sys.version_info >= (3, 12) or self.pip_imported_during_build():
|
||||
return
|
||||
clear_distutils()
|
||||
self.spec_for_distutils = lambda: None
|
||||
|
||||
@classmethod
|
||||
def pip_imported_during_build(cls):
|
||||
"""
|
||||
Detect if pip is being imported in a build script. Ref #2355.
|
||||
"""
|
||||
import traceback
|
||||
|
||||
return any(
|
||||
cls.frame_file_is_setup(frame) for frame, line in traceback.walk_stack(None)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def frame_file_is_setup(frame):
|
||||
"""
|
||||
Return True if the indicated frame suggests a setup.py file.
|
||||
"""
|
||||
# some frames may not have __file__ (#2940)
|
||||
return frame.f_globals.get('__file__', '').endswith('setup.py')
|
||||
|
||||
def spec_for_sensitive_tests(self):
|
||||
"""
|
||||
Ensure stdlib distutils when running select tests under CPython.
|
||||
|
||||
python/cpython#91169
|
||||
"""
|
||||
clear_distutils()
|
||||
self.spec_for_distutils = lambda: None
|
||||
|
||||
sensitive_tests = (
|
||||
[
|
||||
'test.test_distutils',
|
||||
'test.test_peg_generator',
|
||||
'test.test_importlib',
|
||||
]
|
||||
if sys.version_info < (3, 10)
|
||||
else [
|
||||
'test.test_distutils',
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
for name in DistutilsMetaFinder.sensitive_tests:
|
||||
setattr(
|
||||
DistutilsMetaFinder,
|
||||
f'spec_for_{name}',
|
||||
DistutilsMetaFinder.spec_for_sensitive_tests,
|
||||
)
|
||||
|
||||
|
||||
DISTUTILS_FINDER = DistutilsMetaFinder()
|
||||
|
||||
|
||||
def add_shim():
|
||||
DISTUTILS_FINDER in sys.meta_path or insert_shim()
|
||||
|
||||
|
||||
class shim:
|
||||
def __enter__(self):
|
||||
insert_shim()
|
||||
|
||||
def __exit__(self, exc, value, tb):
|
||||
_remove_shim()
|
||||
|
||||
|
||||
def insert_shim():
|
||||
sys.meta_path.insert(0, DISTUTILS_FINDER)
|
||||
|
||||
|
||||
def _remove_shim():
|
||||
try:
|
||||
sys.meta_path.remove(DISTUTILS_FINDER)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
if sys.version_info < (3, 12):
|
||||
# DistutilsMetaFinder can only be disabled in Python < 3.12 (PEP 632)
|
||||
remove_shim = _remove_shim
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
__import__('_distutils_hack').do_override()
|
||||
@@ -0,0 +1 @@
|
||||
pip
|
||||
@@ -0,0 +1,20 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018 Alex Grönholm
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,105 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: anyio
|
||||
Version: 3.7.1
|
||||
Summary: High level compatibility layer for multiple asynchronous event loop implementations
|
||||
Author-email: Alex Grönholm <alex.gronholm@nextday.fi>
|
||||
License: MIT
|
||||
Project-URL: Documentation, https://anyio.readthedocs.io/en/latest/
|
||||
Project-URL: Changelog, https://anyio.readthedocs.io/en/stable/versionhistory.html
|
||||
Project-URL: Source code, https://github.com/agronholm/anyio
|
||||
Project-URL: Issue tracker, https://github.com/agronholm/anyio/issues
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: Framework :: AnyIO
|
||||
Classifier: Typing :: Typed
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3.7
|
||||
Classifier: Programming Language :: Python :: 3.8
|
||||
Classifier: Programming Language :: Python :: 3.9
|
||||
Classifier: Programming Language :: Python :: 3.10
|
||||
Classifier: Programming Language :: Python :: 3.11
|
||||
Requires-Python: >=3.7
|
||||
Description-Content-Type: text/x-rst
|
||||
License-File: LICENSE
|
||||
Requires-Dist: idna (>=2.8)
|
||||
Requires-Dist: sniffio (>=1.1)
|
||||
Requires-Dist: exceptiongroup ; python_version < "3.11"
|
||||
Requires-Dist: typing-extensions ; python_version < "3.8"
|
||||
Provides-Extra: doc
|
||||
Requires-Dist: packaging ; extra == 'doc'
|
||||
Requires-Dist: Sphinx ; extra == 'doc'
|
||||
Requires-Dist: sphinx-rtd-theme (>=1.2.2) ; extra == 'doc'
|
||||
Requires-Dist: sphinxcontrib-jquery ; extra == 'doc'
|
||||
Requires-Dist: sphinx-autodoc-typehints (>=1.2.0) ; extra == 'doc'
|
||||
Provides-Extra: test
|
||||
Requires-Dist: anyio[trio] ; extra == 'test'
|
||||
Requires-Dist: coverage[toml] (>=4.5) ; extra == 'test'
|
||||
Requires-Dist: hypothesis (>=4.0) ; extra == 'test'
|
||||
Requires-Dist: psutil (>=5.9) ; extra == 'test'
|
||||
Requires-Dist: pytest (>=7.0) ; extra == 'test'
|
||||
Requires-Dist: pytest-mock (>=3.6.1) ; extra == 'test'
|
||||
Requires-Dist: trustme ; extra == 'test'
|
||||
Requires-Dist: uvloop (>=0.17) ; (python_version < "3.12" and platform_python_implementation == "CPython" and platform_system != "Windows") and extra == 'test'
|
||||
Requires-Dist: mock (>=4) ; (python_version < "3.8") and extra == 'test'
|
||||
Provides-Extra: trio
|
||||
Requires-Dist: trio (<0.22) ; extra == 'trio'
|
||||
|
||||
.. image:: https://github.com/agronholm/anyio/actions/workflows/test.yml/badge.svg
|
||||
:target: https://github.com/agronholm/anyio/actions/workflows/test.yml
|
||||
:alt: Build Status
|
||||
.. image:: https://coveralls.io/repos/github/agronholm/anyio/badge.svg?branch=master
|
||||
:target: https://coveralls.io/github/agronholm/anyio?branch=master
|
||||
:alt: Code Coverage
|
||||
.. image:: https://readthedocs.org/projects/anyio/badge/?version=latest
|
||||
:target: https://anyio.readthedocs.io/en/latest/?badge=latest
|
||||
:alt: Documentation
|
||||
.. image:: https://badges.gitter.im/gitterHQ/gitter.svg
|
||||
:target: https://gitter.im/python-trio/AnyIO
|
||||
:alt: Gitter chat
|
||||
|
||||
AnyIO is an asynchronous networking and concurrency library that works on top of either asyncio_ or
|
||||
trio_. It implements trio-like `structured concurrency`_ (SC) on top of asyncio and works in harmony
|
||||
with the native SC of trio itself.
|
||||
|
||||
Applications and libraries written against AnyIO's API will run unmodified on either asyncio_ or
|
||||
trio_. AnyIO can also be adopted into a library or application incrementally – bit by bit, no full
|
||||
refactoring necessary. It will blend in with the native libraries of your chosen backend.
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
View full documentation at: https://anyio.readthedocs.io/
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
AnyIO offers the following functionality:
|
||||
|
||||
* Task groups (nurseries_ in trio terminology)
|
||||
* High-level networking (TCP, UDP and UNIX sockets)
|
||||
|
||||
* `Happy eyeballs`_ algorithm for TCP connections (more robust than that of asyncio on Python
|
||||
3.8)
|
||||
* async/await style UDP sockets (unlike asyncio where you still have to use Transports and
|
||||
Protocols)
|
||||
|
||||
* A versatile API for byte streams and object streams
|
||||
* Inter-task synchronization and communication (locks, conditions, events, semaphores, object
|
||||
streams)
|
||||
* Worker threads
|
||||
* Subprocesses
|
||||
* Asynchronous file I/O (using worker threads)
|
||||
* Signal handling
|
||||
|
||||
AnyIO also comes with its own pytest_ plugin which also supports asynchronous fixtures.
|
||||
It even works with the popular Hypothesis_ library.
|
||||
|
||||
.. _asyncio: https://docs.python.org/3/library/asyncio.html
|
||||
.. _trio: https://github.com/python-trio/trio
|
||||
.. _structured concurrency: https://en.wikipedia.org/wiki/Structured_concurrency
|
||||
.. _nurseries: https://trio.readthedocs.io/en/stable/reference-core.html#nurseries-and-spawning
|
||||
.. _Happy eyeballs: https://en.wikipedia.org/wiki/Happy_Eyeballs
|
||||
.. _pytest: https://docs.pytest.org/en/latest/
|
||||
.. _Hypothesis: https://hypothesis.works/
|
||||
@@ -0,0 +1,83 @@
|
||||
anyio-3.7.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
anyio-3.7.1.dist-info/LICENSE,sha256=U2GsncWPLvX9LpsJxoKXwX8ElQkJu8gCO9uC6s8iwrA,1081
|
||||
anyio-3.7.1.dist-info/METADATA,sha256=mOhfXPB7qKVQh3dUtp2NgLysa10jHWeDBNnRg-93A_c,4708
|
||||
anyio-3.7.1.dist-info/RECORD,,
|
||||
anyio-3.7.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
anyio-3.7.1.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
|
||||
anyio-3.7.1.dist-info/entry_points.txt,sha256=_d6Yu6uiaZmNe0CydowirE9Cmg7zUL2g08tQpoS3Qvc,39
|
||||
anyio-3.7.1.dist-info/top_level.txt,sha256=QglSMiWX8_5dpoVAEIHdEYzvqFMdSYWmCj6tYw2ITkQ,6
|
||||
anyio/__init__.py,sha256=Pq9lO03Zm5ynIPlhkquaOuIc1dTTeLGNUQ5HT5qwYMI,4073
|
||||
anyio/__pycache__/__init__.cpython-39.pyc,,
|
||||
anyio/__pycache__/from_thread.cpython-39.pyc,,
|
||||
anyio/__pycache__/lowlevel.cpython-39.pyc,,
|
||||
anyio/__pycache__/pytest_plugin.cpython-39.pyc,,
|
||||
anyio/__pycache__/to_process.cpython-39.pyc,,
|
||||
anyio/__pycache__/to_thread.cpython-39.pyc,,
|
||||
anyio/_backends/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
anyio/_backends/__pycache__/__init__.cpython-39.pyc,,
|
||||
anyio/_backends/__pycache__/_asyncio.cpython-39.pyc,,
|
||||
anyio/_backends/__pycache__/_trio.cpython-39.pyc,,
|
||||
anyio/_backends/_asyncio.py,sha256=fgwZmYnGOxT_pX0OZTPPgRdFqKLjnKvQUk7tsfuNmfM,67056
|
||||
anyio/_backends/_trio.py,sha256=EJAj0tNi0JRM2y3QWP7oS4ct7wnjMSYDG8IZUWMta-E,30035
|
||||
anyio/_core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
anyio/_core/__pycache__/__init__.cpython-39.pyc,,
|
||||
anyio/_core/__pycache__/_compat.cpython-39.pyc,,
|
||||
anyio/_core/__pycache__/_eventloop.cpython-39.pyc,,
|
||||
anyio/_core/__pycache__/_exceptions.cpython-39.pyc,,
|
||||
anyio/_core/__pycache__/_fileio.cpython-39.pyc,,
|
||||
anyio/_core/__pycache__/_resources.cpython-39.pyc,,
|
||||
anyio/_core/__pycache__/_signals.cpython-39.pyc,,
|
||||
anyio/_core/__pycache__/_sockets.cpython-39.pyc,,
|
||||
anyio/_core/__pycache__/_streams.cpython-39.pyc,,
|
||||
anyio/_core/__pycache__/_subprocesses.cpython-39.pyc,,
|
||||
anyio/_core/__pycache__/_synchronization.cpython-39.pyc,,
|
||||
anyio/_core/__pycache__/_tasks.cpython-39.pyc,,
|
||||
anyio/_core/__pycache__/_testing.cpython-39.pyc,,
|
||||
anyio/_core/__pycache__/_typedattr.cpython-39.pyc,,
|
||||
anyio/_core/_compat.py,sha256=XZfBUInEt7jaiTBI2Qbul7EpJdngbwTtG4Qj26un1YE,5726
|
||||
anyio/_core/_eventloop.py,sha256=xJ8KflV1bJ9GAuQRr4o1ojv8wWya4nt_XARta8uLPwc,4083
|
||||
anyio/_core/_exceptions.py,sha256=uOrN5l98o6UrOU6O3kPf0VCDl_zPP-kgZs4IyaLVgwU,2916
|
||||
anyio/_core/_fileio.py,sha256=DWuIul5izCocmJpgqDDNKc_GhMUwayHKdM5R-sbT_A8,18026
|
||||
anyio/_core/_resources.py,sha256=NbmU5O5UX3xEyACnkmYX28Fmwdl-f-ny0tHym26e0w0,435
|
||||
anyio/_core/_signals.py,sha256=KKkZAYL08auydjZnK9S4FQsxx555jT4gXAMcTXdNaok,863
|
||||
anyio/_core/_sockets.py,sha256=szcPd7kKBmlHnx8g_KJWZo2k6syouRNF2614ZrtqiV0,20667
|
||||
anyio/_core/_streams.py,sha256=5gryxQiUisED8uFUAHje5O44RL9wyndNMANzzQWUn1U,1518
|
||||
anyio/_core/_subprocesses.py,sha256=OSAcLAsjfCplXlRyTjWonfS1xU8d5MaZblXYqqY-BM4,4977
|
||||
anyio/_core/_synchronization.py,sha256=Uquo_52vZ7iZzDDoaN_j-N7jeyAlefzOZ8Pxt9mU6gY,16747
|
||||
anyio/_core/_tasks.py,sha256=1wZZWlpDkr6w3kMD629vzJDkPselDvx4XVElgTCVwyM,5316
|
||||
anyio/_core/_testing.py,sha256=7Yll-DOI0uIlIF5VHLUpGGyDPWtDEjFZ85-6ZniwIJU,2217
|
||||
anyio/_core/_typedattr.py,sha256=8o0gwQYSl04zlO9uHqcHu1T6hOw7peY9NW1mOX5DKnY,2551
|
||||
anyio/abc/__init__.py,sha256=UkC-KDbyIoKeDUDhJciwANSoyzz_qaFh4Fb7_AvwjZc,2159
|
||||
anyio/abc/__pycache__/__init__.cpython-39.pyc,,
|
||||
anyio/abc/__pycache__/_resources.cpython-39.pyc,,
|
||||
anyio/abc/__pycache__/_sockets.cpython-39.pyc,,
|
||||
anyio/abc/__pycache__/_streams.cpython-39.pyc,,
|
||||
anyio/abc/__pycache__/_subprocesses.cpython-39.pyc,,
|
||||
anyio/abc/__pycache__/_tasks.cpython-39.pyc,,
|
||||
anyio/abc/__pycache__/_testing.cpython-39.pyc,,
|
||||
anyio/abc/_resources.py,sha256=h1rkzr3E0MFqdXLh9aLLXe-A5W7k_Jc-5XzNr6SJ4w4,763
|
||||
anyio/abc/_sockets.py,sha256=WWYJ6HndKCEuvobAPDkmX0tjwN2FOxf3eTGb1DB7wHE,5243
|
||||
anyio/abc/_streams.py,sha256=yGhOmlVI3W9whmzPuewwYQ2BrKhrUFuWZ4zpVLWOK84,6584
|
||||
anyio/abc/_subprocesses.py,sha256=r-totaRbFX6kKV-4WTeuswz8n01aap8cvkYVQCRKN0M,2067
|
||||
anyio/abc/_tasks.py,sha256=a_5DLyiCbp0K57LJPOyF-PZyXmUcv_p9VRXPFj_K03M,3413
|
||||
anyio/abc/_testing.py,sha256=Eub7gXJ0tVPo_WN5iJAw10FrvC7C1uaL3b2neGr_pfs,1924
|
||||
anyio/from_thread.py,sha256=aUVKXctPgZ5wK3p5VTyrtjDj9tSQSrH6xCjBuo-hv3A,16563
|
||||
anyio/lowlevel.py,sha256=cOTncxRW5KeswqYQQdp0pfAw6OFWXius1SPhCYwHZL4,4647
|
||||
anyio/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
anyio/pytest_plugin.py,sha256=_Txgl0-I3kO1rk_KATXmIUV57C34hajcJCGcgV26CU0,5022
|
||||
anyio/streams/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
anyio/streams/__pycache__/__init__.cpython-39.pyc,,
|
||||
anyio/streams/__pycache__/buffered.cpython-39.pyc,,
|
||||
anyio/streams/__pycache__/file.cpython-39.pyc,,
|
||||
anyio/streams/__pycache__/memory.cpython-39.pyc,,
|
||||
anyio/streams/__pycache__/stapled.cpython-39.pyc,,
|
||||
anyio/streams/__pycache__/text.cpython-39.pyc,,
|
||||
anyio/streams/__pycache__/tls.cpython-39.pyc,,
|
||||
anyio/streams/buffered.py,sha256=2ifplNLwT73d1UKBxrkFdlC9wTAze9LhPL7pt_7cYgY,4473
|
||||
anyio/streams/file.py,sha256=-NP6jMcUd2f1VJwgcxgiRHdEsNnhE0lANl0ov_i7FrE,4356
|
||||
anyio/streams/memory.py,sha256=QZhc5qdomBpGCgrUVWAaqEBxI0oklVxK_62atW6tnNk,9274
|
||||
anyio/streams/stapled.py,sha256=9u2GxpiOPsGtgO1qsj2tVoW4b8bgiwp5rSDs1BFKkLM,4275
|
||||
anyio/streams/text.py,sha256=1K4ZCLKl2b7yywrW6wKEeMu3xyQHE_T0aU5_oC9GPTE,5043
|
||||
anyio/streams/tls.py,sha256=TbdCz1KtfEnp3mxHvkROXRefhE6S1LHiwgWiJX8zYaU,12099
|
||||
anyio/to_process.py,sha256=_RSsG8UME2nGxeFEdg3OEfv9XshSQwrMU7DAbwWGx9U,9242
|
||||
anyio/to_thread.py,sha256=HVpTvBei2sSXgJJeNKdwhJwQaW76LDbb1htQ-Mc6zDs,2146
|
||||
@@ -0,0 +1,5 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: bdist_wheel (0.40.0)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
[pytest11]
|
||||
anyio = anyio.pytest_plugin
|
||||
@@ -0,0 +1 @@
|
||||
anyio
|
||||
169
jokes_bot/venv/lib/python3.9/site-packages/anyio/__init__.py
Normal file
169
jokes_bot/venv/lib/python3.9/site-packages/anyio/__init__.py
Normal file
@@ -0,0 +1,169 @@
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = (
|
||||
"maybe_async",
|
||||
"maybe_async_cm",
|
||||
"run",
|
||||
"sleep",
|
||||
"sleep_forever",
|
||||
"sleep_until",
|
||||
"current_time",
|
||||
"get_all_backends",
|
||||
"get_cancelled_exc_class",
|
||||
"BrokenResourceError",
|
||||
"BrokenWorkerProcess",
|
||||
"BusyResourceError",
|
||||
"ClosedResourceError",
|
||||
"DelimiterNotFound",
|
||||
"EndOfStream",
|
||||
"ExceptionGroup",
|
||||
"IncompleteRead",
|
||||
"TypedAttributeLookupError",
|
||||
"WouldBlock",
|
||||
"AsyncFile",
|
||||
"Path",
|
||||
"open_file",
|
||||
"wrap_file",
|
||||
"aclose_forcefully",
|
||||
"open_signal_receiver",
|
||||
"connect_tcp",
|
||||
"connect_unix",
|
||||
"create_tcp_listener",
|
||||
"create_unix_listener",
|
||||
"create_udp_socket",
|
||||
"create_connected_udp_socket",
|
||||
"getaddrinfo",
|
||||
"getnameinfo",
|
||||
"wait_socket_readable",
|
||||
"wait_socket_writable",
|
||||
"create_memory_object_stream",
|
||||
"run_process",
|
||||
"open_process",
|
||||
"create_lock",
|
||||
"CapacityLimiter",
|
||||
"CapacityLimiterStatistics",
|
||||
"Condition",
|
||||
"ConditionStatistics",
|
||||
"Event",
|
||||
"EventStatistics",
|
||||
"Lock",
|
||||
"LockStatistics",
|
||||
"Semaphore",
|
||||
"SemaphoreStatistics",
|
||||
"create_condition",
|
||||
"create_event",
|
||||
"create_semaphore",
|
||||
"create_capacity_limiter",
|
||||
"open_cancel_scope",
|
||||
"fail_after",
|
||||
"move_on_after",
|
||||
"current_effective_deadline",
|
||||
"TASK_STATUS_IGNORED",
|
||||
"CancelScope",
|
||||
"create_task_group",
|
||||
"TaskInfo",
|
||||
"get_current_task",
|
||||
"get_running_tasks",
|
||||
"wait_all_tasks_blocked",
|
||||
"run_sync_in_worker_thread",
|
||||
"run_async_from_thread",
|
||||
"run_sync_from_thread",
|
||||
"current_default_worker_thread_limiter",
|
||||
"create_blocking_portal",
|
||||
"start_blocking_portal",
|
||||
"typed_attribute",
|
||||
"TypedAttributeSet",
|
||||
"TypedAttributeProvider",
|
||||
)
|
||||
|
||||
from typing import Any
|
||||
|
||||
from ._core._compat import maybe_async, maybe_async_cm
|
||||
from ._core._eventloop import (
|
||||
current_time,
|
||||
get_all_backends,
|
||||
get_cancelled_exc_class,
|
||||
run,
|
||||
sleep,
|
||||
sleep_forever,
|
||||
sleep_until,
|
||||
)
|
||||
from ._core._exceptions import (
|
||||
BrokenResourceError,
|
||||
BrokenWorkerProcess,
|
||||
BusyResourceError,
|
||||
ClosedResourceError,
|
||||
DelimiterNotFound,
|
||||
EndOfStream,
|
||||
ExceptionGroup,
|
||||
IncompleteRead,
|
||||
TypedAttributeLookupError,
|
||||
WouldBlock,
|
||||
)
|
||||
from ._core._fileio import AsyncFile, Path, open_file, wrap_file
|
||||
from ._core._resources import aclose_forcefully
|
||||
from ._core._signals import open_signal_receiver
|
||||
from ._core._sockets import (
|
||||
connect_tcp,
|
||||
connect_unix,
|
||||
create_connected_udp_socket,
|
||||
create_tcp_listener,
|
||||
create_udp_socket,
|
||||
create_unix_listener,
|
||||
getaddrinfo,
|
||||
getnameinfo,
|
||||
wait_socket_readable,
|
||||
wait_socket_writable,
|
||||
)
|
||||
from ._core._streams import create_memory_object_stream
|
||||
from ._core._subprocesses import open_process, run_process
|
||||
from ._core._synchronization import (
|
||||
CapacityLimiter,
|
||||
CapacityLimiterStatistics,
|
||||
Condition,
|
||||
ConditionStatistics,
|
||||
Event,
|
||||
EventStatistics,
|
||||
Lock,
|
||||
LockStatistics,
|
||||
Semaphore,
|
||||
SemaphoreStatistics,
|
||||
create_capacity_limiter,
|
||||
create_condition,
|
||||
create_event,
|
||||
create_lock,
|
||||
create_semaphore,
|
||||
)
|
||||
from ._core._tasks import (
|
||||
TASK_STATUS_IGNORED,
|
||||
CancelScope,
|
||||
create_task_group,
|
||||
current_effective_deadline,
|
||||
fail_after,
|
||||
move_on_after,
|
||||
open_cancel_scope,
|
||||
)
|
||||
from ._core._testing import (
|
||||
TaskInfo,
|
||||
get_current_task,
|
||||
get_running_tasks,
|
||||
wait_all_tasks_blocked,
|
||||
)
|
||||
from ._core._typedattr import TypedAttributeProvider, TypedAttributeSet, typed_attribute
|
||||
|
||||
# Re-exported here, for backwards compatibility
|
||||
# isort: off
|
||||
from .to_thread import current_default_worker_thread_limiter, run_sync_in_worker_thread
|
||||
from .from_thread import (
|
||||
create_blocking_portal,
|
||||
run_async_from_thread,
|
||||
run_sync_from_thread,
|
||||
start_blocking_portal,
|
||||
)
|
||||
|
||||
# Re-export imports so they look like they live directly in this package
|
||||
key: str
|
||||
value: Any
|
||||
for key, value in list(locals().items()):
|
||||
if getattr(value, "__module__", "").startswith("anyio."):
|
||||
value.__module__ = __name__
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,996 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import array
|
||||
import math
|
||||
import socket
|
||||
from concurrent.futures import Future
|
||||
from contextvars import copy_context
|
||||
from dataclasses import dataclass
|
||||
from functools import partial
|
||||
from io import IOBase
|
||||
from os import PathLike
|
||||
from signal import Signals
|
||||
from types import TracebackType
|
||||
from typing import (
|
||||
IO,
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
AsyncGenerator,
|
||||
AsyncIterator,
|
||||
Awaitable,
|
||||
Callable,
|
||||
Collection,
|
||||
Coroutine,
|
||||
Generic,
|
||||
Iterable,
|
||||
Mapping,
|
||||
NoReturn,
|
||||
Sequence,
|
||||
TypeVar,
|
||||
cast,
|
||||
)
|
||||
|
||||
import sniffio
|
||||
import trio.from_thread
|
||||
from outcome import Error, Outcome, Value
|
||||
from trio.socket import SocketType as TrioSocketType
|
||||
from trio.to_thread import run_sync
|
||||
|
||||
from .. import CapacityLimiterStatistics, EventStatistics, TaskInfo, abc
|
||||
from .._core._compat import DeprecatedAsyncContextManager, DeprecatedAwaitable
|
||||
from .._core._eventloop import claim_worker_thread
|
||||
from .._core._exceptions import (
|
||||
BrokenResourceError,
|
||||
BusyResourceError,
|
||||
ClosedResourceError,
|
||||
EndOfStream,
|
||||
)
|
||||
from .._core._exceptions import ExceptionGroup as BaseExceptionGroup
|
||||
from .._core._sockets import convert_ipv6_sockaddr
|
||||
from .._core._synchronization import CapacityLimiter as BaseCapacityLimiter
|
||||
from .._core._synchronization import Event as BaseEvent
|
||||
from .._core._synchronization import ResourceGuard
|
||||
from .._core._tasks import CancelScope as BaseCancelScope
|
||||
from ..abc import IPSockAddrType, UDPPacketType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from trio_typing import TaskStatus
|
||||
|
||||
try:
|
||||
from trio import lowlevel as trio_lowlevel
|
||||
except ImportError:
|
||||
from trio import hazmat as trio_lowlevel # type: ignore[no-redef]
|
||||
from trio.hazmat import wait_readable, wait_writable
|
||||
else:
|
||||
from trio.lowlevel import wait_readable, wait_writable
|
||||
|
||||
try:
|
||||
trio_open_process = trio_lowlevel.open_process
|
||||
except AttributeError:
|
||||
# isort: off
|
||||
from trio import ( # type: ignore[attr-defined, no-redef]
|
||||
open_process as trio_open_process,
|
||||
)
|
||||
|
||||
T_Retval = TypeVar("T_Retval")
|
||||
T_SockAddr = TypeVar("T_SockAddr", str, IPSockAddrType)
|
||||
|
||||
|
||||
#
|
||||
# Event loop
|
||||
#
|
||||
|
||||
run = trio.run
|
||||
current_token = trio.lowlevel.current_trio_token
|
||||
RunVar = trio.lowlevel.RunVar
|
||||
|
||||
|
||||
#
|
||||
# Miscellaneous
|
||||
#
|
||||
|
||||
sleep = trio.sleep
|
||||
|
||||
|
||||
#
|
||||
# Timeouts and cancellation
|
||||
#
|
||||
|
||||
|
||||
class CancelScope(BaseCancelScope):
|
||||
def __new__(
|
||||
cls, original: trio.CancelScope | None = None, **kwargs: object
|
||||
) -> CancelScope:
|
||||
return object.__new__(cls)
|
||||
|
||||
def __init__(self, original: trio.CancelScope | None = None, **kwargs: Any) -> None:
|
||||
self.__original = original or trio.CancelScope(**kwargs)
|
||||
|
||||
def __enter__(self) -> CancelScope:
|
||||
self.__original.__enter__()
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> bool | None:
|
||||
# https://github.com/python-trio/trio-typing/pull/79
|
||||
return self.__original.__exit__( # type: ignore[func-returns-value]
|
||||
exc_type, exc_val, exc_tb
|
||||
)
|
||||
|
||||
def cancel(self) -> DeprecatedAwaitable:
|
||||
self.__original.cancel()
|
||||
return DeprecatedAwaitable(self.cancel)
|
||||
|
||||
@property
|
||||
def deadline(self) -> float:
|
||||
return self.__original.deadline
|
||||
|
||||
@deadline.setter
|
||||
def deadline(self, value: float) -> None:
|
||||
self.__original.deadline = value
|
||||
|
||||
@property
|
||||
def cancel_called(self) -> bool:
|
||||
return self.__original.cancel_called
|
||||
|
||||
@property
|
||||
def shield(self) -> bool:
|
||||
return self.__original.shield
|
||||
|
||||
@shield.setter
|
||||
def shield(self, value: bool) -> None:
|
||||
self.__original.shield = value
|
||||
|
||||
|
||||
CancelledError = trio.Cancelled
|
||||
checkpoint = trio.lowlevel.checkpoint
|
||||
checkpoint_if_cancelled = trio.lowlevel.checkpoint_if_cancelled
|
||||
cancel_shielded_checkpoint = trio.lowlevel.cancel_shielded_checkpoint
|
||||
current_effective_deadline = trio.current_effective_deadline
|
||||
current_time = trio.current_time
|
||||
|
||||
|
||||
#
|
||||
# Task groups
|
||||
#
|
||||
|
||||
|
||||
class ExceptionGroup(BaseExceptionGroup, trio.MultiError):
|
||||
pass
|
||||
|
||||
|
||||
class TaskGroup(abc.TaskGroup):
|
||||
def __init__(self) -> None:
|
||||
self._active = False
|
||||
self._nursery_manager = trio.open_nursery()
|
||||
self.cancel_scope = None # type: ignore[assignment]
|
||||
|
||||
async def __aenter__(self) -> TaskGroup:
|
||||
self._active = True
|
||||
self._nursery = await self._nursery_manager.__aenter__()
|
||||
self.cancel_scope = CancelScope(self._nursery.cancel_scope)
|
||||
return self
|
||||
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> bool | None:
|
||||
try:
|
||||
return await self._nursery_manager.__aexit__(exc_type, exc_val, exc_tb)
|
||||
except trio.MultiError as exc:
|
||||
raise ExceptionGroup(exc.exceptions) from None
|
||||
finally:
|
||||
self._active = False
|
||||
|
||||
def start_soon(
|
||||
self, func: Callable[..., Awaitable[Any]], *args: object, name: object = None
|
||||
) -> None:
|
||||
if not self._active:
|
||||
raise RuntimeError(
|
||||
"This task group is not active; no new tasks can be started."
|
||||
)
|
||||
|
||||
self._nursery.start_soon(func, *args, name=name)
|
||||
|
||||
async def start(
|
||||
self, func: Callable[..., Awaitable[Any]], *args: object, name: object = None
|
||||
) -> object:
|
||||
if not self._active:
|
||||
raise RuntimeError(
|
||||
"This task group is not active; no new tasks can be started."
|
||||
)
|
||||
|
||||
return await self._nursery.start(func, *args, name=name)
|
||||
|
||||
|
||||
#
|
||||
# Threads
|
||||
#
|
||||
|
||||
|
||||
async def run_sync_in_worker_thread(
|
||||
func: Callable[..., T_Retval],
|
||||
*args: object,
|
||||
cancellable: bool = False,
|
||||
limiter: trio.CapacityLimiter | None = None,
|
||||
) -> T_Retval:
|
||||
def wrapper() -> T_Retval:
|
||||
with claim_worker_thread("trio"):
|
||||
return func(*args)
|
||||
|
||||
# TODO: remove explicit context copying when trio 0.20 is the minimum requirement
|
||||
context = copy_context()
|
||||
context.run(sniffio.current_async_library_cvar.set, None)
|
||||
return await run_sync(
|
||||
context.run, wrapper, cancellable=cancellable, limiter=limiter
|
||||
)
|
||||
|
||||
|
||||
# TODO: remove this workaround when trio 0.20 is the minimum requirement
|
||||
def run_async_from_thread(
|
||||
fn: Callable[..., Awaitable[T_Retval]], *args: Any
|
||||
) -> T_Retval:
|
||||
async def wrapper() -> T_Retval:
|
||||
retval: T_Retval
|
||||
|
||||
async def inner() -> None:
|
||||
nonlocal retval
|
||||
__tracebackhide__ = True
|
||||
retval = await fn(*args)
|
||||
|
||||
async with trio.open_nursery() as n:
|
||||
context.run(n.start_soon, inner)
|
||||
|
||||
__tracebackhide__ = True
|
||||
return retval # noqa: F821
|
||||
|
||||
context = copy_context()
|
||||
context.run(sniffio.current_async_library_cvar.set, "trio")
|
||||
return trio.from_thread.run(wrapper)
|
||||
|
||||
|
||||
def run_sync_from_thread(fn: Callable[..., T_Retval], *args: Any) -> T_Retval:
|
||||
# TODO: remove explicit context copying when trio 0.20 is the minimum requirement
|
||||
retval = trio.from_thread.run_sync(copy_context().run, fn, *args)
|
||||
return cast(T_Retval, retval)
|
||||
|
||||
|
||||
class BlockingPortal(abc.BlockingPortal):
|
||||
def __new__(cls) -> BlockingPortal:
|
||||
return object.__new__(cls)
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._token = trio.lowlevel.current_trio_token()
|
||||
|
||||
def _spawn_task_from_thread(
|
||||
self,
|
||||
func: Callable,
|
||||
args: tuple,
|
||||
kwargs: dict[str, Any],
|
||||
name: object,
|
||||
future: Future,
|
||||
) -> None:
|
||||
context = copy_context()
|
||||
context.run(sniffio.current_async_library_cvar.set, "trio")
|
||||
trio.from_thread.run_sync(
|
||||
context.run,
|
||||
partial(self._task_group.start_soon, name=name),
|
||||
self._call_func,
|
||||
func,
|
||||
args,
|
||||
kwargs,
|
||||
future,
|
||||
trio_token=self._token,
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Subprocesses
|
||||
#
|
||||
|
||||
|
||||
@dataclass(eq=False)
|
||||
class ReceiveStreamWrapper(abc.ByteReceiveStream):
|
||||
_stream: trio.abc.ReceiveStream
|
||||
|
||||
async def receive(self, max_bytes: int | None = None) -> bytes:
|
||||
try:
|
||||
data = await self._stream.receive_some(max_bytes)
|
||||
except trio.ClosedResourceError as exc:
|
||||
raise ClosedResourceError from exc.__cause__
|
||||
except trio.BrokenResourceError as exc:
|
||||
raise BrokenResourceError from exc.__cause__
|
||||
|
||||
if data:
|
||||
return data
|
||||
else:
|
||||
raise EndOfStream
|
||||
|
||||
async def aclose(self) -> None:
|
||||
await self._stream.aclose()
|
||||
|
||||
|
||||
@dataclass(eq=False)
|
||||
class SendStreamWrapper(abc.ByteSendStream):
|
||||
_stream: trio.abc.SendStream
|
||||
|
||||
async def send(self, item: bytes) -> None:
|
||||
try:
|
||||
await self._stream.send_all(item)
|
||||
except trio.ClosedResourceError as exc:
|
||||
raise ClosedResourceError from exc.__cause__
|
||||
except trio.BrokenResourceError as exc:
|
||||
raise BrokenResourceError from exc.__cause__
|
||||
|
||||
async def aclose(self) -> None:
|
||||
await self._stream.aclose()
|
||||
|
||||
|
||||
@dataclass(eq=False)
|
||||
class Process(abc.Process):
|
||||
_process: trio.Process
|
||||
_stdin: abc.ByteSendStream | None
|
||||
_stdout: abc.ByteReceiveStream | None
|
||||
_stderr: abc.ByteReceiveStream | None
|
||||
|
||||
async def aclose(self) -> None:
|
||||
if self._stdin:
|
||||
await self._stdin.aclose()
|
||||
if self._stdout:
|
||||
await self._stdout.aclose()
|
||||
if self._stderr:
|
||||
await self._stderr.aclose()
|
||||
|
||||
await self.wait()
|
||||
|
||||
async def wait(self) -> int:
|
||||
return await self._process.wait()
|
||||
|
||||
def terminate(self) -> None:
|
||||
self._process.terminate()
|
||||
|
||||
def kill(self) -> None:
|
||||
self._process.kill()
|
||||
|
||||
def send_signal(self, signal: Signals) -> None:
|
||||
self._process.send_signal(signal)
|
||||
|
||||
@property
|
||||
def pid(self) -> int:
|
||||
return self._process.pid
|
||||
|
||||
@property
|
||||
def returncode(self) -> int | None:
|
||||
return self._process.returncode
|
||||
|
||||
@property
|
||||
def stdin(self) -> abc.ByteSendStream | None:
|
||||
return self._stdin
|
||||
|
||||
@property
|
||||
def stdout(self) -> abc.ByteReceiveStream | None:
|
||||
return self._stdout
|
||||
|
||||
@property
|
||||
def stderr(self) -> abc.ByteReceiveStream | None:
|
||||
return self._stderr
|
||||
|
||||
|
||||
async def open_process(
|
||||
command: str | bytes | Sequence[str | bytes],
|
||||
*,
|
||||
shell: bool,
|
||||
stdin: int | IO[Any] | None,
|
||||
stdout: int | IO[Any] | None,
|
||||
stderr: int | IO[Any] | None,
|
||||
cwd: str | bytes | PathLike | None = None,
|
||||
env: Mapping[str, str] | None = None,
|
||||
start_new_session: bool = False,
|
||||
) -> Process:
|
||||
process = await trio_open_process( # type: ignore[misc]
|
||||
command, # type: ignore[arg-type]
|
||||
stdin=stdin,
|
||||
stdout=stdout,
|
||||
stderr=stderr,
|
||||
shell=shell,
|
||||
cwd=cwd,
|
||||
env=env,
|
||||
start_new_session=start_new_session,
|
||||
)
|
||||
stdin_stream = SendStreamWrapper(process.stdin) if process.stdin else None
|
||||
stdout_stream = ReceiveStreamWrapper(process.stdout) if process.stdout else None
|
||||
stderr_stream = ReceiveStreamWrapper(process.stderr) if process.stderr else None
|
||||
return Process(process, stdin_stream, stdout_stream, stderr_stream)
|
||||
|
||||
|
||||
class _ProcessPoolShutdownInstrument(trio.abc.Instrument):
|
||||
def after_run(self) -> None:
|
||||
super().after_run()
|
||||
|
||||
|
||||
current_default_worker_process_limiter: RunVar = RunVar(
|
||||
"current_default_worker_process_limiter"
|
||||
)
|
||||
|
||||
|
||||
async def _shutdown_process_pool(workers: set[Process]) -> None:
|
||||
process: Process
|
||||
try:
|
||||
await sleep(math.inf)
|
||||
except trio.Cancelled:
|
||||
for process in workers:
|
||||
if process.returncode is None:
|
||||
process.kill()
|
||||
|
||||
with CancelScope(shield=True):
|
||||
for process in workers:
|
||||
await process.aclose()
|
||||
|
||||
|
||||
def setup_process_pool_exit_at_shutdown(workers: set[Process]) -> None:
|
||||
trio.lowlevel.spawn_system_task(_shutdown_process_pool, workers)
|
||||
|
||||
|
||||
#
|
||||
# Sockets and networking
|
||||
#
|
||||
|
||||
|
||||
class _TrioSocketMixin(Generic[T_SockAddr]):
|
||||
def __init__(self, trio_socket: TrioSocketType) -> None:
|
||||
self._trio_socket = trio_socket
|
||||
self._closed = False
|
||||
|
||||
def _check_closed(self) -> None:
|
||||
if self._closed:
|
||||
raise ClosedResourceError
|
||||
if self._trio_socket.fileno() < 0:
|
||||
raise BrokenResourceError
|
||||
|
||||
@property
|
||||
def _raw_socket(self) -> socket.socket:
|
||||
return self._trio_socket._sock # type: ignore[attr-defined]
|
||||
|
||||
async def aclose(self) -> None:
|
||||
if self._trio_socket.fileno() >= 0:
|
||||
self._closed = True
|
||||
self._trio_socket.close()
|
||||
|
||||
def _convert_socket_error(self, exc: BaseException) -> NoReturn:
|
||||
if isinstance(exc, trio.ClosedResourceError):
|
||||
raise ClosedResourceError from exc
|
||||
elif self._trio_socket.fileno() < 0 and self._closed:
|
||||
raise ClosedResourceError from None
|
||||
elif isinstance(exc, OSError):
|
||||
raise BrokenResourceError from exc
|
||||
else:
|
||||
raise exc
|
||||
|
||||
|
||||
class SocketStream(_TrioSocketMixin, abc.SocketStream):
|
||||
def __init__(self, trio_socket: TrioSocketType) -> None:
|
||||
super().__init__(trio_socket)
|
||||
self._receive_guard = ResourceGuard("reading from")
|
||||
self._send_guard = ResourceGuard("writing to")
|
||||
|
||||
async def receive(self, max_bytes: int = 65536) -> bytes:
|
||||
with self._receive_guard:
|
||||
try:
|
||||
data = await self._trio_socket.recv(max_bytes)
|
||||
except BaseException as exc:
|
||||
self._convert_socket_error(exc)
|
||||
|
||||
if data:
|
||||
return data
|
||||
else:
|
||||
raise EndOfStream
|
||||
|
||||
async def send(self, item: bytes) -> None:
|
||||
with self._send_guard:
|
||||
view = memoryview(item)
|
||||
while view:
|
||||
try:
|
||||
bytes_sent = await self._trio_socket.send(view)
|
||||
except BaseException as exc:
|
||||
self._convert_socket_error(exc)
|
||||
|
||||
view = view[bytes_sent:]
|
||||
|
||||
async def send_eof(self) -> None:
|
||||
self._trio_socket.shutdown(socket.SHUT_WR)
|
||||
|
||||
|
||||
class UNIXSocketStream(SocketStream, abc.UNIXSocketStream):
|
||||
async def receive_fds(self, msglen: int, maxfds: int) -> tuple[bytes, list[int]]:
|
||||
if not isinstance(msglen, int) or msglen < 0:
|
||||
raise ValueError("msglen must be a non-negative integer")
|
||||
if not isinstance(maxfds, int) or maxfds < 1:
|
||||
raise ValueError("maxfds must be a positive integer")
|
||||
|
||||
fds = array.array("i")
|
||||
await checkpoint()
|
||||
with self._receive_guard:
|
||||
while True:
|
||||
try:
|
||||
message, ancdata, flags, addr = await self._trio_socket.recvmsg(
|
||||
msglen, socket.CMSG_LEN(maxfds * fds.itemsize)
|
||||
)
|
||||
except BaseException as exc:
|
||||
self._convert_socket_error(exc)
|
||||
else:
|
||||
if not message and not ancdata:
|
||||
raise EndOfStream
|
||||
|
||||
break
|
||||
|
||||
for cmsg_level, cmsg_type, cmsg_data in ancdata:
|
||||
if cmsg_level != socket.SOL_SOCKET or cmsg_type != socket.SCM_RIGHTS:
|
||||
raise RuntimeError(
|
||||
f"Received unexpected ancillary data; message = {message!r}, "
|
||||
f"cmsg_level = {cmsg_level}, cmsg_type = {cmsg_type}"
|
||||
)
|
||||
|
||||
fds.frombytes(cmsg_data[: len(cmsg_data) - (len(cmsg_data) % fds.itemsize)])
|
||||
|
||||
return message, list(fds)
|
||||
|
||||
async def send_fds(self, message: bytes, fds: Collection[int | IOBase]) -> None:
|
||||
if not message:
|
||||
raise ValueError("message must not be empty")
|
||||
if not fds:
|
||||
raise ValueError("fds must not be empty")
|
||||
|
||||
filenos: list[int] = []
|
||||
for fd in fds:
|
||||
if isinstance(fd, int):
|
||||
filenos.append(fd)
|
||||
elif isinstance(fd, IOBase):
|
||||
filenos.append(fd.fileno())
|
||||
|
||||
fdarray = array.array("i", filenos)
|
||||
await checkpoint()
|
||||
with self._send_guard:
|
||||
while True:
|
||||
try:
|
||||
await self._trio_socket.sendmsg(
|
||||
[message],
|
||||
[
|
||||
(
|
||||
socket.SOL_SOCKET,
|
||||
socket.SCM_RIGHTS, # type: ignore[list-item]
|
||||
fdarray,
|
||||
)
|
||||
],
|
||||
)
|
||||
break
|
||||
except BaseException as exc:
|
||||
self._convert_socket_error(exc)
|
||||
|
||||
|
||||
class TCPSocketListener(_TrioSocketMixin, abc.SocketListener):
|
||||
def __init__(self, raw_socket: socket.socket):
|
||||
super().__init__(trio.socket.from_stdlib_socket(raw_socket))
|
||||
self._accept_guard = ResourceGuard("accepting connections from")
|
||||
|
||||
async def accept(self) -> SocketStream:
|
||||
with self._accept_guard:
|
||||
try:
|
||||
trio_socket, _addr = await self._trio_socket.accept()
|
||||
except BaseException as exc:
|
||||
self._convert_socket_error(exc)
|
||||
|
||||
trio_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
return SocketStream(trio_socket)
|
||||
|
||||
|
||||
class UNIXSocketListener(_TrioSocketMixin, abc.SocketListener):
|
||||
def __init__(self, raw_socket: socket.socket):
|
||||
super().__init__(trio.socket.from_stdlib_socket(raw_socket))
|
||||
self._accept_guard = ResourceGuard("accepting connections from")
|
||||
|
||||
async def accept(self) -> UNIXSocketStream:
|
||||
with self._accept_guard:
|
||||
try:
|
||||
trio_socket, _addr = await self._trio_socket.accept()
|
||||
except BaseException as exc:
|
||||
self._convert_socket_error(exc)
|
||||
|
||||
return UNIXSocketStream(trio_socket)
|
||||
|
||||
|
||||
class UDPSocket(_TrioSocketMixin[IPSockAddrType], abc.UDPSocket):
|
||||
def __init__(self, trio_socket: TrioSocketType) -> None:
|
||||
super().__init__(trio_socket)
|
||||
self._receive_guard = ResourceGuard("reading from")
|
||||
self._send_guard = ResourceGuard("writing to")
|
||||
|
||||
async def receive(self) -> tuple[bytes, IPSockAddrType]:
|
||||
with self._receive_guard:
|
||||
try:
|
||||
data, addr = await self._trio_socket.recvfrom(65536)
|
||||
return data, convert_ipv6_sockaddr(addr)
|
||||
except BaseException as exc:
|
||||
self._convert_socket_error(exc)
|
||||
|
||||
async def send(self, item: UDPPacketType) -> None:
|
||||
with self._send_guard:
|
||||
try:
|
||||
await self._trio_socket.sendto(*item)
|
||||
except BaseException as exc:
|
||||
self._convert_socket_error(exc)
|
||||
|
||||
|
||||
class ConnectedUDPSocket(_TrioSocketMixin[IPSockAddrType], abc.ConnectedUDPSocket):
|
||||
def __init__(self, trio_socket: TrioSocketType) -> None:
|
||||
super().__init__(trio_socket)
|
||||
self._receive_guard = ResourceGuard("reading from")
|
||||
self._send_guard = ResourceGuard("writing to")
|
||||
|
||||
async def receive(self) -> bytes:
|
||||
with self._receive_guard:
|
||||
try:
|
||||
return await self._trio_socket.recv(65536)
|
||||
except BaseException as exc:
|
||||
self._convert_socket_error(exc)
|
||||
|
||||
async def send(self, item: bytes) -> None:
|
||||
with self._send_guard:
|
||||
try:
|
||||
await self._trio_socket.send(item)
|
||||
except BaseException as exc:
|
||||
self._convert_socket_error(exc)
|
||||
|
||||
|
||||
async def connect_tcp(
|
||||
host: str, port: int, local_address: IPSockAddrType | None = None
|
||||
) -> SocketStream:
|
||||
family = socket.AF_INET6 if ":" in host else socket.AF_INET
|
||||
trio_socket = trio.socket.socket(family)
|
||||
trio_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
if local_address:
|
||||
await trio_socket.bind(local_address)
|
||||
|
||||
try:
|
||||
await trio_socket.connect((host, port))
|
||||
except BaseException:
|
||||
trio_socket.close()
|
||||
raise
|
||||
|
||||
return SocketStream(trio_socket)
|
||||
|
||||
|
||||
async def connect_unix(path: str) -> UNIXSocketStream:
|
||||
trio_socket = trio.socket.socket(socket.AF_UNIX)
|
||||
try:
|
||||
await trio_socket.connect(path)
|
||||
except BaseException:
|
||||
trio_socket.close()
|
||||
raise
|
||||
|
||||
return UNIXSocketStream(trio_socket)
|
||||
|
||||
|
||||
async def create_udp_socket(
|
||||
family: socket.AddressFamily,
|
||||
local_address: IPSockAddrType | None,
|
||||
remote_address: IPSockAddrType | None,
|
||||
reuse_port: bool,
|
||||
) -> UDPSocket | ConnectedUDPSocket:
|
||||
trio_socket = trio.socket.socket(family=family, type=socket.SOCK_DGRAM)
|
||||
|
||||
if reuse_port:
|
||||
trio_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
||||
|
||||
if local_address:
|
||||
await trio_socket.bind(local_address)
|
||||
|
||||
if remote_address:
|
||||
await trio_socket.connect(remote_address)
|
||||
return ConnectedUDPSocket(trio_socket)
|
||||
else:
|
||||
return UDPSocket(trio_socket)
|
||||
|
||||
|
||||
getaddrinfo = trio.socket.getaddrinfo
|
||||
getnameinfo = trio.socket.getnameinfo
|
||||
|
||||
|
||||
async def wait_socket_readable(sock: socket.socket) -> None:
|
||||
try:
|
||||
await wait_readable(sock)
|
||||
except trio.ClosedResourceError as exc:
|
||||
raise ClosedResourceError().with_traceback(exc.__traceback__) from None
|
||||
except trio.BusyResourceError:
|
||||
raise BusyResourceError("reading from") from None
|
||||
|
||||
|
||||
async def wait_socket_writable(sock: socket.socket) -> None:
|
||||
try:
|
||||
await wait_writable(sock)
|
||||
except trio.ClosedResourceError as exc:
|
||||
raise ClosedResourceError().with_traceback(exc.__traceback__) from None
|
||||
except trio.BusyResourceError:
|
||||
raise BusyResourceError("writing to") from None
|
||||
|
||||
|
||||
#
|
||||
# Synchronization
|
||||
#
|
||||
|
||||
|
||||
class Event(BaseEvent):
|
||||
def __new__(cls) -> Event:
|
||||
return object.__new__(cls)
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.__original = trio.Event()
|
||||
|
||||
def is_set(self) -> bool:
|
||||
return self.__original.is_set()
|
||||
|
||||
async def wait(self) -> None:
|
||||
return await self.__original.wait()
|
||||
|
||||
def statistics(self) -> EventStatistics:
|
||||
orig_statistics = self.__original.statistics()
|
||||
return EventStatistics(tasks_waiting=orig_statistics.tasks_waiting)
|
||||
|
||||
def set(self) -> DeprecatedAwaitable:
|
||||
self.__original.set()
|
||||
return DeprecatedAwaitable(self.set)
|
||||
|
||||
|
||||
class CapacityLimiter(BaseCapacityLimiter):
|
||||
def __new__(cls, *args: object, **kwargs: object) -> CapacityLimiter:
|
||||
return object.__new__(cls)
|
||||
|
||||
def __init__(
|
||||
self, *args: Any, original: trio.CapacityLimiter | None = None
|
||||
) -> None:
|
||||
self.__original = original or trio.CapacityLimiter(*args)
|
||||
|
||||
async def __aenter__(self) -> None:
|
||||
return await self.__original.__aenter__()
|
||||
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> None:
|
||||
await self.__original.__aexit__(exc_type, exc_val, exc_tb)
|
||||
|
||||
@property
|
||||
def total_tokens(self) -> float:
|
||||
return self.__original.total_tokens
|
||||
|
||||
@total_tokens.setter
|
||||
def total_tokens(self, value: float) -> None:
|
||||
self.__original.total_tokens = value
|
||||
|
||||
@property
|
||||
def borrowed_tokens(self) -> int:
|
||||
return self.__original.borrowed_tokens
|
||||
|
||||
@property
|
||||
def available_tokens(self) -> float:
|
||||
return self.__original.available_tokens
|
||||
|
||||
def acquire_nowait(self) -> DeprecatedAwaitable:
|
||||
self.__original.acquire_nowait()
|
||||
return DeprecatedAwaitable(self.acquire_nowait)
|
||||
|
||||
def acquire_on_behalf_of_nowait(self, borrower: object) -> DeprecatedAwaitable:
|
||||
self.__original.acquire_on_behalf_of_nowait(borrower)
|
||||
return DeprecatedAwaitable(self.acquire_on_behalf_of_nowait)
|
||||
|
||||
async def acquire(self) -> None:
|
||||
await self.__original.acquire()
|
||||
|
||||
async def acquire_on_behalf_of(self, borrower: object) -> None:
|
||||
await self.__original.acquire_on_behalf_of(borrower)
|
||||
|
||||
def release(self) -> None:
|
||||
return self.__original.release()
|
||||
|
||||
def release_on_behalf_of(self, borrower: object) -> None:
|
||||
return self.__original.release_on_behalf_of(borrower)
|
||||
|
||||
def statistics(self) -> CapacityLimiterStatistics:
|
||||
orig = self.__original.statistics()
|
||||
return CapacityLimiterStatistics(
|
||||
borrowed_tokens=orig.borrowed_tokens,
|
||||
total_tokens=orig.total_tokens,
|
||||
borrowers=orig.borrowers,
|
||||
tasks_waiting=orig.tasks_waiting,
|
||||
)
|
||||
|
||||
|
||||
_capacity_limiter_wrapper: RunVar = RunVar("_capacity_limiter_wrapper")
|
||||
|
||||
|
||||
def current_default_thread_limiter() -> CapacityLimiter:
|
||||
try:
|
||||
return _capacity_limiter_wrapper.get()
|
||||
except LookupError:
|
||||
limiter = CapacityLimiter(
|
||||
original=trio.to_thread.current_default_thread_limiter()
|
||||
)
|
||||
_capacity_limiter_wrapper.set(limiter)
|
||||
return limiter
|
||||
|
||||
|
||||
#
|
||||
# Signal handling
|
||||
#
|
||||
|
||||
|
||||
class _SignalReceiver(DeprecatedAsyncContextManager["_SignalReceiver"]):
|
||||
_iterator: AsyncIterator[int]
|
||||
|
||||
def __init__(self, signals: tuple[Signals, ...]):
|
||||
self._signals = signals
|
||||
|
||||
def __enter__(self) -> _SignalReceiver:
|
||||
self._cm = trio.open_signal_receiver(*self._signals)
|
||||
self._iterator = self._cm.__enter__()
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> bool | None:
|
||||
return self._cm.__exit__(exc_type, exc_val, exc_tb)
|
||||
|
||||
def __aiter__(self) -> _SignalReceiver:
|
||||
return self
|
||||
|
||||
async def __anext__(self) -> Signals:
|
||||
signum = await self._iterator.__anext__()
|
||||
return Signals(signum)
|
||||
|
||||
|
||||
def open_signal_receiver(*signals: Signals) -> _SignalReceiver:
|
||||
return _SignalReceiver(signals)
|
||||
|
||||
|
||||
#
|
||||
# Testing and debugging
|
||||
#
|
||||
|
||||
|
||||
def get_current_task() -> TaskInfo:
|
||||
task = trio_lowlevel.current_task()
|
||||
|
||||
parent_id = None
|
||||
if task.parent_nursery and task.parent_nursery.parent_task:
|
||||
parent_id = id(task.parent_nursery.parent_task)
|
||||
|
||||
return TaskInfo(id(task), parent_id, task.name, task.coro)
|
||||
|
||||
|
||||
def get_running_tasks() -> list[TaskInfo]:
|
||||
root_task = trio_lowlevel.current_root_task()
|
||||
task_infos = [TaskInfo(id(root_task), None, root_task.name, root_task.coro)]
|
||||
nurseries = root_task.child_nurseries
|
||||
while nurseries:
|
||||
new_nurseries: list[trio.Nursery] = []
|
||||
for nursery in nurseries:
|
||||
for task in nursery.child_tasks:
|
||||
task_infos.append(
|
||||
TaskInfo(id(task), id(nursery.parent_task), task.name, task.coro)
|
||||
)
|
||||
new_nurseries.extend(task.child_nurseries)
|
||||
|
||||
nurseries = new_nurseries
|
||||
|
||||
return task_infos
|
||||
|
||||
|
||||
def wait_all_tasks_blocked() -> Awaitable[None]:
|
||||
import trio.testing
|
||||
|
||||
return trio.testing.wait_all_tasks_blocked()
|
||||
|
||||
|
||||
class TestRunner(abc.TestRunner):
|
||||
def __init__(self, **options: Any) -> None:
|
||||
from collections import deque
|
||||
from queue import Queue
|
||||
|
||||
self._call_queue: Queue[Callable[..., object]] = Queue()
|
||||
self._result_queue: deque[Outcome] = deque()
|
||||
self._stop_event: trio.Event | None = None
|
||||
self._nursery: trio.Nursery | None = None
|
||||
self._options = options
|
||||
|
||||
async def _trio_main(self) -> None:
|
||||
self._stop_event = trio.Event()
|
||||
async with trio.open_nursery() as self._nursery:
|
||||
await self._stop_event.wait()
|
||||
|
||||
async def _call_func(
|
||||
self, func: Callable[..., Awaitable[object]], args: tuple, kwargs: dict
|
||||
) -> None:
|
||||
try:
|
||||
retval = await func(*args, **kwargs)
|
||||
except BaseException as exc:
|
||||
self._result_queue.append(Error(exc))
|
||||
else:
|
||||
self._result_queue.append(Value(retval))
|
||||
|
||||
def _main_task_finished(self, outcome: object) -> None:
|
||||
self._nursery = None
|
||||
|
||||
def _get_nursery(self) -> trio.Nursery:
|
||||
if self._nursery is None:
|
||||
trio.lowlevel.start_guest_run(
|
||||
self._trio_main,
|
||||
run_sync_soon_threadsafe=self._call_queue.put,
|
||||
done_callback=self._main_task_finished,
|
||||
**self._options,
|
||||
)
|
||||
while self._nursery is None:
|
||||
self._call_queue.get()()
|
||||
|
||||
return self._nursery
|
||||
|
||||
def _call(
|
||||
self, func: Callable[..., Awaitable[T_Retval]], *args: object, **kwargs: object
|
||||
) -> T_Retval:
|
||||
self._get_nursery().start_soon(self._call_func, func, args, kwargs)
|
||||
while not self._result_queue:
|
||||
self._call_queue.get()()
|
||||
|
||||
outcome = self._result_queue.pop()
|
||||
return outcome.unwrap()
|
||||
|
||||
def close(self) -> None:
|
||||
if self._stop_event:
|
||||
self._stop_event.set()
|
||||
while self._nursery is not None:
|
||||
self._call_queue.get()()
|
||||
|
||||
def run_asyncgen_fixture(
|
||||
self,
|
||||
fixture_func: Callable[..., AsyncGenerator[T_Retval, Any]],
|
||||
kwargs: dict[str, Any],
|
||||
) -> Iterable[T_Retval]:
|
||||
async def fixture_runner(*, task_status: TaskStatus[T_Retval]) -> None:
|
||||
agen = fixture_func(**kwargs)
|
||||
retval = await agen.asend(None)
|
||||
task_status.started(retval)
|
||||
await teardown_event.wait()
|
||||
try:
|
||||
await agen.asend(None)
|
||||
except StopAsyncIteration:
|
||||
pass
|
||||
else:
|
||||
await agen.aclose()
|
||||
raise RuntimeError("Async generator fixture did not stop")
|
||||
|
||||
teardown_event = trio.Event()
|
||||
fixture_value = self._call(lambda: self._get_nursery().start(fixture_runner))
|
||||
yield fixture_value
|
||||
teardown_event.set()
|
||||
|
||||
def run_fixture(
|
||||
self,
|
||||
fixture_func: Callable[..., Coroutine[Any, Any, T_Retval]],
|
||||
kwargs: dict[str, Any],
|
||||
) -> T_Retval:
|
||||
return self._call(fixture_func, **kwargs)
|
||||
|
||||
def run_test(
|
||||
self, test_func: Callable[..., Coroutine[Any, Any, Any]], kwargs: dict[str, Any]
|
||||
) -> None:
|
||||
self._call(test_func, **kwargs)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,217 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from contextlib import AbstractContextManager
|
||||
from types import TracebackType
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
AsyncContextManager,
|
||||
Callable,
|
||||
ContextManager,
|
||||
Generator,
|
||||
Generic,
|
||||
Iterable,
|
||||
List,
|
||||
TypeVar,
|
||||
Union,
|
||||
overload,
|
||||
)
|
||||
from warnings import warn
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ._testing import TaskInfo
|
||||
else:
|
||||
TaskInfo = object
|
||||
|
||||
T = TypeVar("T")
|
||||
AnyDeprecatedAwaitable = Union[
|
||||
"DeprecatedAwaitable",
|
||||
"DeprecatedAwaitableFloat",
|
||||
"DeprecatedAwaitableList[T]",
|
||||
TaskInfo,
|
||||
]
|
||||
|
||||
|
||||
@overload
|
||||
async def maybe_async(__obj: TaskInfo) -> TaskInfo:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
async def maybe_async(__obj: DeprecatedAwaitableFloat) -> float:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
async def maybe_async(__obj: DeprecatedAwaitableList[T]) -> list[T]:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
async def maybe_async(__obj: DeprecatedAwaitable) -> None:
|
||||
...
|
||||
|
||||
|
||||
async def maybe_async(
|
||||
__obj: AnyDeprecatedAwaitable[T],
|
||||
) -> TaskInfo | float | list[T] | None:
|
||||
"""
|
||||
Await on the given object if necessary.
|
||||
|
||||
This function is intended to bridge the gap between AnyIO 2.x and 3.x where some functions and
|
||||
methods were converted from coroutine functions into regular functions.
|
||||
|
||||
Do **not** try to use this for any other purpose!
|
||||
|
||||
:return: the result of awaiting on the object if coroutine, or the object itself otherwise
|
||||
|
||||
.. versionadded:: 2.2
|
||||
|
||||
"""
|
||||
return __obj._unwrap()
|
||||
|
||||
|
||||
class _ContextManagerWrapper:
|
||||
def __init__(self, cm: ContextManager[T]):
|
||||
self._cm = cm
|
||||
|
||||
async def __aenter__(self) -> T:
|
||||
return self._cm.__enter__()
|
||||
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> bool | None:
|
||||
return self._cm.__exit__(exc_type, exc_val, exc_tb)
|
||||
|
||||
|
||||
def maybe_async_cm(
|
||||
cm: ContextManager[T] | AsyncContextManager[T],
|
||||
) -> AsyncContextManager[T]:
|
||||
"""
|
||||
Wrap a regular context manager as an async one if necessary.
|
||||
|
||||
This function is intended to bridge the gap between AnyIO 2.x and 3.x where some functions and
|
||||
methods were changed to return regular context managers instead of async ones.
|
||||
|
||||
:param cm: a regular or async context manager
|
||||
:return: an async context manager
|
||||
|
||||
.. versionadded:: 2.2
|
||||
|
||||
"""
|
||||
if not isinstance(cm, AbstractContextManager):
|
||||
raise TypeError("Given object is not an context manager")
|
||||
|
||||
return _ContextManagerWrapper(cm)
|
||||
|
||||
|
||||
def _warn_deprecation(
|
||||
awaitable: AnyDeprecatedAwaitable[Any], stacklevel: int = 1
|
||||
) -> None:
|
||||
warn(
|
||||
f'Awaiting on {awaitable._name}() is deprecated. Use "await '
|
||||
f"anyio.maybe_async({awaitable._name}(...)) if you have to support both AnyIO 2.x "
|
||||
f'and 3.x, or just remove the "await" if you are completely migrating to AnyIO 3+.',
|
||||
DeprecationWarning,
|
||||
stacklevel=stacklevel + 1,
|
||||
)
|
||||
|
||||
|
||||
class DeprecatedAwaitable:
|
||||
def __init__(self, func: Callable[..., DeprecatedAwaitable]):
|
||||
self._name = f"{func.__module__}.{func.__qualname__}"
|
||||
|
||||
def __await__(self) -> Generator[None, None, None]:
|
||||
_warn_deprecation(self)
|
||||
if False:
|
||||
yield
|
||||
|
||||
def __reduce__(self) -> tuple[type[None], tuple[()]]:
|
||||
return type(None), ()
|
||||
|
||||
def _unwrap(self) -> None:
|
||||
return None
|
||||
|
||||
|
||||
class DeprecatedAwaitableFloat(float):
|
||||
def __new__(
|
||||
cls, x: float, func: Callable[..., DeprecatedAwaitableFloat]
|
||||
) -> DeprecatedAwaitableFloat:
|
||||
return super().__new__(cls, x)
|
||||
|
||||
def __init__(self, x: float, func: Callable[..., DeprecatedAwaitableFloat]):
|
||||
self._name = f"{func.__module__}.{func.__qualname__}"
|
||||
|
||||
def __await__(self) -> Generator[None, None, float]:
|
||||
_warn_deprecation(self)
|
||||
if False:
|
||||
yield
|
||||
|
||||
return float(self)
|
||||
|
||||
def __reduce__(self) -> tuple[type[float], tuple[float]]:
|
||||
return float, (float(self),)
|
||||
|
||||
def _unwrap(self) -> float:
|
||||
return float(self)
|
||||
|
||||
|
||||
class DeprecatedAwaitableList(List[T]):
|
||||
def __init__(
|
||||
self,
|
||||
iterable: Iterable[T] = (),
|
||||
*,
|
||||
func: Callable[..., DeprecatedAwaitableList[T]],
|
||||
):
|
||||
super().__init__(iterable)
|
||||
self._name = f"{func.__module__}.{func.__qualname__}"
|
||||
|
||||
def __await__(self) -> Generator[None, None, list[T]]:
|
||||
_warn_deprecation(self)
|
||||
if False:
|
||||
yield
|
||||
|
||||
return list(self)
|
||||
|
||||
def __reduce__(self) -> tuple[type[list[T]], tuple[list[T]]]:
|
||||
return list, (list(self),)
|
||||
|
||||
def _unwrap(self) -> list[T]:
|
||||
return list(self)
|
||||
|
||||
|
||||
class DeprecatedAsyncContextManager(Generic[T], metaclass=ABCMeta):
|
||||
@abstractmethod
|
||||
def __enter__(self) -> T:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> bool | None:
|
||||
pass
|
||||
|
||||
async def __aenter__(self) -> T:
|
||||
warn(
|
||||
f"Using {self.__class__.__name__} as an async context manager has been deprecated. "
|
||||
f'Use "async with anyio.maybe_async_cm(yourcontextmanager) as foo:" if you have to '
|
||||
f'support both AnyIO 2.x and 3.x, or just remove the "async" from "async with" if '
|
||||
f"you are completely migrating to AnyIO 3+.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
return self.__enter__()
|
||||
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> bool | None:
|
||||
return self.__exit__(exc_type, exc_val, exc_tb)
|
||||
@@ -0,0 +1,153 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import sys
|
||||
import threading
|
||||
from contextlib import contextmanager
|
||||
from importlib import import_module
|
||||
from typing import (
|
||||
Any,
|
||||
Awaitable,
|
||||
Callable,
|
||||
Generator,
|
||||
TypeVar,
|
||||
)
|
||||
|
||||
import sniffio
|
||||
|
||||
# This must be updated when new backends are introduced
|
||||
from ._compat import DeprecatedAwaitableFloat
|
||||
|
||||
BACKENDS = "asyncio", "trio"
|
||||
|
||||
T_Retval = TypeVar("T_Retval")
|
||||
threadlocals = threading.local()
|
||||
|
||||
|
||||
def run(
|
||||
func: Callable[..., Awaitable[T_Retval]],
|
||||
*args: object,
|
||||
backend: str = "asyncio",
|
||||
backend_options: dict[str, Any] | None = None,
|
||||
) -> T_Retval:
|
||||
"""
|
||||
Run the given coroutine function in an asynchronous event loop.
|
||||
|
||||
The current thread must not be already running an event loop.
|
||||
|
||||
:param func: a coroutine function
|
||||
:param args: positional arguments to ``func``
|
||||
:param backend: name of the asynchronous event loop implementation – currently either
|
||||
``asyncio`` or ``trio``
|
||||
:param backend_options: keyword arguments to call the backend ``run()`` implementation with
|
||||
(documented :ref:`here <backend options>`)
|
||||
:return: the return value of the coroutine function
|
||||
:raises RuntimeError: if an asynchronous event loop is already running in this thread
|
||||
:raises LookupError: if the named backend is not found
|
||||
|
||||
"""
|
||||
try:
|
||||
asynclib_name = sniffio.current_async_library()
|
||||
except sniffio.AsyncLibraryNotFoundError:
|
||||
pass
|
||||
else:
|
||||
raise RuntimeError(f"Already running {asynclib_name} in this thread")
|
||||
|
||||
try:
|
||||
asynclib = import_module(f"..._backends._{backend}", package=__name__)
|
||||
except ImportError as exc:
|
||||
raise LookupError(f"No such backend: {backend}") from exc
|
||||
|
||||
token = None
|
||||
if sniffio.current_async_library_cvar.get(None) is None:
|
||||
# Since we're in control of the event loop, we can cache the name of the async library
|
||||
token = sniffio.current_async_library_cvar.set(backend)
|
||||
|
||||
try:
|
||||
backend_options = backend_options or {}
|
||||
return asynclib.run(func, *args, **backend_options)
|
||||
finally:
|
||||
if token:
|
||||
sniffio.current_async_library_cvar.reset(token)
|
||||
|
||||
|
||||
async def sleep(delay: float) -> None:
|
||||
"""
|
||||
Pause the current task for the specified duration.
|
||||
|
||||
:param delay: the duration, in seconds
|
||||
|
||||
"""
|
||||
return await get_asynclib().sleep(delay)
|
||||
|
||||
|
||||
async def sleep_forever() -> None:
|
||||
"""
|
||||
Pause the current task until it's cancelled.
|
||||
|
||||
This is a shortcut for ``sleep(math.inf)``.
|
||||
|
||||
.. versionadded:: 3.1
|
||||
|
||||
"""
|
||||
await sleep(math.inf)
|
||||
|
||||
|
||||
async def sleep_until(deadline: float) -> None:
|
||||
"""
|
||||
Pause the current task until the given time.
|
||||
|
||||
:param deadline: the absolute time to wake up at (according to the internal monotonic clock of
|
||||
the event loop)
|
||||
|
||||
.. versionadded:: 3.1
|
||||
|
||||
"""
|
||||
now = current_time()
|
||||
await sleep(max(deadline - now, 0))
|
||||
|
||||
|
||||
def current_time() -> DeprecatedAwaitableFloat:
|
||||
"""
|
||||
Return the current value of the event loop's internal clock.
|
||||
|
||||
:return: the clock value (seconds)
|
||||
|
||||
"""
|
||||
return DeprecatedAwaitableFloat(get_asynclib().current_time(), current_time)
|
||||
|
||||
|
||||
def get_all_backends() -> tuple[str, ...]:
|
||||
"""Return a tuple of the names of all built-in backends."""
|
||||
return BACKENDS
|
||||
|
||||
|
||||
def get_cancelled_exc_class() -> type[BaseException]:
|
||||
"""Return the current async library's cancellation exception class."""
|
||||
return get_asynclib().CancelledError
|
||||
|
||||
|
||||
#
|
||||
# Private API
|
||||
#
|
||||
|
||||
|
||||
@contextmanager
|
||||
def claim_worker_thread(backend: str) -> Generator[Any, None, None]:
|
||||
module = sys.modules["anyio._backends._" + backend]
|
||||
threadlocals.current_async_module = module
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
del threadlocals.current_async_module
|
||||
|
||||
|
||||
def get_asynclib(asynclib_name: str | None = None) -> Any:
|
||||
if asynclib_name is None:
|
||||
asynclib_name = sniffio.current_async_library()
|
||||
|
||||
modulename = "anyio._backends._" + asynclib_name
|
||||
try:
|
||||
return sys.modules[modulename]
|
||||
except KeyError:
|
||||
return import_module(modulename)
|
||||
@@ -0,0 +1,94 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from traceback import format_exception
|
||||
|
||||
|
||||
class BrokenResourceError(Exception):
|
||||
"""
|
||||
Raised when trying to use a resource that has been rendered unusable due to external causes
|
||||
(e.g. a send stream whose peer has disconnected).
|
||||
"""
|
||||
|
||||
|
||||
class BrokenWorkerProcess(Exception):
|
||||
"""
|
||||
Raised by :func:`run_sync_in_process` if the worker process terminates abruptly or otherwise
|
||||
misbehaves.
|
||||
"""
|
||||
|
||||
|
||||
class BusyResourceError(Exception):
|
||||
"""Raised when two tasks are trying to read from or write to the same resource concurrently."""
|
||||
|
||||
def __init__(self, action: str):
|
||||
super().__init__(f"Another task is already {action} this resource")
|
||||
|
||||
|
||||
class ClosedResourceError(Exception):
|
||||
"""Raised when trying to use a resource that has been closed."""
|
||||
|
||||
|
||||
class DelimiterNotFound(Exception):
|
||||
"""
|
||||
Raised during :meth:`~anyio.streams.buffered.BufferedByteReceiveStream.receive_until` if the
|
||||
maximum number of bytes has been read without the delimiter being found.
|
||||
"""
|
||||
|
||||
def __init__(self, max_bytes: int) -> None:
|
||||
super().__init__(
|
||||
f"The delimiter was not found among the first {max_bytes} bytes"
|
||||
)
|
||||
|
||||
|
||||
class EndOfStream(Exception):
|
||||
"""Raised when trying to read from a stream that has been closed from the other end."""
|
||||
|
||||
|
||||
class ExceptionGroup(BaseException):
|
||||
"""
|
||||
Raised when multiple exceptions have been raised in a task group.
|
||||
|
||||
:var ~typing.Sequence[BaseException] exceptions: the sequence of exceptions raised together
|
||||
"""
|
||||
|
||||
SEPARATOR = "----------------------------\n"
|
||||
|
||||
exceptions: list[BaseException]
|
||||
|
||||
def __str__(self) -> str:
|
||||
tracebacks = [
|
||||
"".join(format_exception(type(exc), exc, exc.__traceback__))
|
||||
for exc in self.exceptions
|
||||
]
|
||||
return (
|
||||
f"{len(self.exceptions)} exceptions were raised in the task group:\n"
|
||||
f"{self.SEPARATOR}{self.SEPARATOR.join(tracebacks)}"
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
exception_reprs = ", ".join(repr(exc) for exc in self.exceptions)
|
||||
return f"<{self.__class__.__name__}: {exception_reprs}>"
|
||||
|
||||
|
||||
class IncompleteRead(Exception):
|
||||
"""
|
||||
Raised during :meth:`~anyio.streams.buffered.BufferedByteReceiveStream.receive_exactly` or
|
||||
:meth:`~anyio.streams.buffered.BufferedByteReceiveStream.receive_until` if the
|
||||
connection is closed before the requested amount of bytes has been read.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
"The stream was closed before the read operation could be completed"
|
||||
)
|
||||
|
||||
|
||||
class TypedAttributeLookupError(LookupError):
|
||||
"""
|
||||
Raised by :meth:`~anyio.TypedAttributeProvider.extra` when the given typed attribute is not
|
||||
found and no default value has been given.
|
||||
"""
|
||||
|
||||
|
||||
class WouldBlock(Exception):
|
||||
"""Raised by ``X_nowait`` functions if ``X()`` would block."""
|
||||
@@ -0,0 +1,603 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from functools import partial
|
||||
from os import PathLike
|
||||
from typing import (
|
||||
IO,
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
AnyStr,
|
||||
AsyncIterator,
|
||||
Callable,
|
||||
Generic,
|
||||
Iterable,
|
||||
Iterator,
|
||||
Sequence,
|
||||
cast,
|
||||
overload,
|
||||
)
|
||||
|
||||
from .. import to_thread
|
||||
from ..abc import AsyncResource
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
from typing import Final
|
||||
else:
|
||||
from typing_extensions import Final
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _typeshed import OpenBinaryMode, OpenTextMode, ReadableBuffer, WriteableBuffer
|
||||
else:
|
||||
ReadableBuffer = OpenBinaryMode = OpenTextMode = WriteableBuffer = object
|
||||
|
||||
|
||||
class AsyncFile(AsyncResource, Generic[AnyStr]):
|
||||
"""
|
||||
An asynchronous file object.
|
||||
|
||||
This class wraps a standard file object and provides async friendly versions of the following
|
||||
blocking methods (where available on the original file object):
|
||||
|
||||
* read
|
||||
* read1
|
||||
* readline
|
||||
* readlines
|
||||
* readinto
|
||||
* readinto1
|
||||
* write
|
||||
* writelines
|
||||
* truncate
|
||||
* seek
|
||||
* tell
|
||||
* flush
|
||||
|
||||
All other methods are directly passed through.
|
||||
|
||||
This class supports the asynchronous context manager protocol which closes the underlying file
|
||||
at the end of the context block.
|
||||
|
||||
This class also supports asynchronous iteration::
|
||||
|
||||
async with await open_file(...) as f:
|
||||
async for line in f:
|
||||
print(line)
|
||||
"""
|
||||
|
||||
def __init__(self, fp: IO[AnyStr]) -> None:
|
||||
self._fp: Any = fp
|
||||
|
||||
def __getattr__(self, name: str) -> object:
|
||||
return getattr(self._fp, name)
|
||||
|
||||
@property
|
||||
def wrapped(self) -> IO[AnyStr]:
|
||||
"""The wrapped file object."""
|
||||
return self._fp
|
||||
|
||||
async def __aiter__(self) -> AsyncIterator[AnyStr]:
|
||||
while True:
|
||||
line = await self.readline()
|
||||
if line:
|
||||
yield line
|
||||
else:
|
||||
break
|
||||
|
||||
async def aclose(self) -> None:
|
||||
return await to_thread.run_sync(self._fp.close)
|
||||
|
||||
async def read(self, size: int = -1) -> AnyStr:
|
||||
return await to_thread.run_sync(self._fp.read, size)
|
||||
|
||||
async def read1(self: AsyncFile[bytes], size: int = -1) -> bytes:
|
||||
return await to_thread.run_sync(self._fp.read1, size)
|
||||
|
||||
async def readline(self) -> AnyStr:
|
||||
return await to_thread.run_sync(self._fp.readline)
|
||||
|
||||
async def readlines(self) -> list[AnyStr]:
|
||||
return await to_thread.run_sync(self._fp.readlines)
|
||||
|
||||
async def readinto(self: AsyncFile[bytes], b: WriteableBuffer) -> bytes:
|
||||
return await to_thread.run_sync(self._fp.readinto, b)
|
||||
|
||||
async def readinto1(self: AsyncFile[bytes], b: WriteableBuffer) -> bytes:
|
||||
return await to_thread.run_sync(self._fp.readinto1, b)
|
||||
|
||||
@overload
|
||||
async def write(self: AsyncFile[bytes], b: ReadableBuffer) -> int:
|
||||
...
|
||||
|
||||
@overload
|
||||
async def write(self: AsyncFile[str], b: str) -> int:
|
||||
...
|
||||
|
||||
async def write(self, b: ReadableBuffer | str) -> int:
|
||||
return await to_thread.run_sync(self._fp.write, b)
|
||||
|
||||
@overload
|
||||
async def writelines(
|
||||
self: AsyncFile[bytes], lines: Iterable[ReadableBuffer]
|
||||
) -> None:
|
||||
...
|
||||
|
||||
@overload
|
||||
async def writelines(self: AsyncFile[str], lines: Iterable[str]) -> None:
|
||||
...
|
||||
|
||||
async def writelines(self, lines: Iterable[ReadableBuffer] | Iterable[str]) -> None:
|
||||
return await to_thread.run_sync(self._fp.writelines, lines)
|
||||
|
||||
async def truncate(self, size: int | None = None) -> int:
|
||||
return await to_thread.run_sync(self._fp.truncate, size)
|
||||
|
||||
async def seek(self, offset: int, whence: int | None = os.SEEK_SET) -> int:
|
||||
return await to_thread.run_sync(self._fp.seek, offset, whence)
|
||||
|
||||
async def tell(self) -> int:
|
||||
return await to_thread.run_sync(self._fp.tell)
|
||||
|
||||
async def flush(self) -> None:
|
||||
return await to_thread.run_sync(self._fp.flush)
|
||||
|
||||
|
||||
@overload
|
||||
async def open_file(
|
||||
file: str | PathLike[str] | int,
|
||||
mode: OpenBinaryMode,
|
||||
buffering: int = ...,
|
||||
encoding: str | None = ...,
|
||||
errors: str | None = ...,
|
||||
newline: str | None = ...,
|
||||
closefd: bool = ...,
|
||||
opener: Callable[[str, int], int] | None = ...,
|
||||
) -> AsyncFile[bytes]:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
async def open_file(
|
||||
file: str | PathLike[str] | int,
|
||||
mode: OpenTextMode = ...,
|
||||
buffering: int = ...,
|
||||
encoding: str | None = ...,
|
||||
errors: str | None = ...,
|
||||
newline: str | None = ...,
|
||||
closefd: bool = ...,
|
||||
opener: Callable[[str, int], int] | None = ...,
|
||||
) -> AsyncFile[str]:
|
||||
...
|
||||
|
||||
|
||||
async def open_file(
|
||||
file: str | PathLike[str] | int,
|
||||
mode: str = "r",
|
||||
buffering: int = -1,
|
||||
encoding: str | None = None,
|
||||
errors: str | None = None,
|
||||
newline: str | None = None,
|
||||
closefd: bool = True,
|
||||
opener: Callable[[str, int], int] | None = None,
|
||||
) -> AsyncFile[Any]:
|
||||
"""
|
||||
Open a file asynchronously.
|
||||
|
||||
The arguments are exactly the same as for the builtin :func:`open`.
|
||||
|
||||
:return: an asynchronous file object
|
||||
|
||||
"""
|
||||
fp = await to_thread.run_sync(
|
||||
open, file, mode, buffering, encoding, errors, newline, closefd, opener
|
||||
)
|
||||
return AsyncFile(fp)
|
||||
|
||||
|
||||
def wrap_file(file: IO[AnyStr]) -> AsyncFile[AnyStr]:
|
||||
"""
|
||||
Wrap an existing file as an asynchronous file.
|
||||
|
||||
:param file: an existing file-like object
|
||||
:return: an asynchronous file object
|
||||
|
||||
"""
|
||||
return AsyncFile(file)
|
||||
|
||||
|
||||
@dataclass(eq=False)
|
||||
class _PathIterator(AsyncIterator["Path"]):
|
||||
iterator: Iterator[PathLike[str]]
|
||||
|
||||
async def __anext__(self) -> Path:
|
||||
nextval = await to_thread.run_sync(next, self.iterator, None, cancellable=True)
|
||||
if nextval is None:
|
||||
raise StopAsyncIteration from None
|
||||
|
||||
return Path(cast("PathLike[str]", nextval))
|
||||
|
||||
|
||||
class Path:
|
||||
"""
|
||||
An asynchronous version of :class:`pathlib.Path`.
|
||||
|
||||
This class cannot be substituted for :class:`pathlib.Path` or :class:`pathlib.PurePath`, but
|
||||
it is compatible with the :class:`os.PathLike` interface.
|
||||
|
||||
It implements the Python 3.10 version of :class:`pathlib.Path` interface, except for the
|
||||
deprecated :meth:`~pathlib.Path.link_to` method.
|
||||
|
||||
Any methods that do disk I/O need to be awaited on. These methods are:
|
||||
|
||||
* :meth:`~pathlib.Path.absolute`
|
||||
* :meth:`~pathlib.Path.chmod`
|
||||
* :meth:`~pathlib.Path.cwd`
|
||||
* :meth:`~pathlib.Path.exists`
|
||||
* :meth:`~pathlib.Path.expanduser`
|
||||
* :meth:`~pathlib.Path.group`
|
||||
* :meth:`~pathlib.Path.hardlink_to`
|
||||
* :meth:`~pathlib.Path.home`
|
||||
* :meth:`~pathlib.Path.is_block_device`
|
||||
* :meth:`~pathlib.Path.is_char_device`
|
||||
* :meth:`~pathlib.Path.is_dir`
|
||||
* :meth:`~pathlib.Path.is_fifo`
|
||||
* :meth:`~pathlib.Path.is_file`
|
||||
* :meth:`~pathlib.Path.is_mount`
|
||||
* :meth:`~pathlib.Path.lchmod`
|
||||
* :meth:`~pathlib.Path.lstat`
|
||||
* :meth:`~pathlib.Path.mkdir`
|
||||
* :meth:`~pathlib.Path.open`
|
||||
* :meth:`~pathlib.Path.owner`
|
||||
* :meth:`~pathlib.Path.read_bytes`
|
||||
* :meth:`~pathlib.Path.read_text`
|
||||
* :meth:`~pathlib.Path.readlink`
|
||||
* :meth:`~pathlib.Path.rename`
|
||||
* :meth:`~pathlib.Path.replace`
|
||||
* :meth:`~pathlib.Path.rmdir`
|
||||
* :meth:`~pathlib.Path.samefile`
|
||||
* :meth:`~pathlib.Path.stat`
|
||||
* :meth:`~pathlib.Path.touch`
|
||||
* :meth:`~pathlib.Path.unlink`
|
||||
* :meth:`~pathlib.Path.write_bytes`
|
||||
* :meth:`~pathlib.Path.write_text`
|
||||
|
||||
Additionally, the following methods return an async iterator yielding :class:`~.Path` objects:
|
||||
|
||||
* :meth:`~pathlib.Path.glob`
|
||||
* :meth:`~pathlib.Path.iterdir`
|
||||
* :meth:`~pathlib.Path.rglob`
|
||||
"""
|
||||
|
||||
__slots__ = "_path", "__weakref__"
|
||||
|
||||
__weakref__: Any
|
||||
|
||||
def __init__(self, *args: str | PathLike[str]) -> None:
|
||||
self._path: Final[pathlib.Path] = pathlib.Path(*args)
|
||||
|
||||
def __fspath__(self) -> str:
|
||||
return self._path.__fspath__()
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self._path.__str__()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({self.as_posix()!r})"
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
return self._path.__bytes__()
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return self._path.__hash__()
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
target = other._path if isinstance(other, Path) else other
|
||||
return self._path.__eq__(target)
|
||||
|
||||
def __lt__(self, other: Path) -> bool:
|
||||
target = other._path if isinstance(other, Path) else other
|
||||
return self._path.__lt__(target)
|
||||
|
||||
def __le__(self, other: Path) -> bool:
|
||||
target = other._path if isinstance(other, Path) else other
|
||||
return self._path.__le__(target)
|
||||
|
||||
def __gt__(self, other: Path) -> bool:
|
||||
target = other._path if isinstance(other, Path) else other
|
||||
return self._path.__gt__(target)
|
||||
|
||||
def __ge__(self, other: Path) -> bool:
|
||||
target = other._path if isinstance(other, Path) else other
|
||||
return self._path.__ge__(target)
|
||||
|
||||
def __truediv__(self, other: Any) -> Path:
|
||||
return Path(self._path / other)
|
||||
|
||||
def __rtruediv__(self, other: Any) -> Path:
|
||||
return Path(other) / self
|
||||
|
||||
@property
|
||||
def parts(self) -> tuple[str, ...]:
|
||||
return self._path.parts
|
||||
|
||||
@property
|
||||
def drive(self) -> str:
|
||||
return self._path.drive
|
||||
|
||||
@property
|
||||
def root(self) -> str:
|
||||
return self._path.root
|
||||
|
||||
@property
|
||||
def anchor(self) -> str:
|
||||
return self._path.anchor
|
||||
|
||||
@property
|
||||
def parents(self) -> Sequence[Path]:
|
||||
return tuple(Path(p) for p in self._path.parents)
|
||||
|
||||
@property
|
||||
def parent(self) -> Path:
|
||||
return Path(self._path.parent)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._path.name
|
||||
|
||||
@property
|
||||
def suffix(self) -> str:
|
||||
return self._path.suffix
|
||||
|
||||
@property
|
||||
def suffixes(self) -> list[str]:
|
||||
return self._path.suffixes
|
||||
|
||||
@property
|
||||
def stem(self) -> str:
|
||||
return self._path.stem
|
||||
|
||||
async def absolute(self) -> Path:
|
||||
path = await to_thread.run_sync(self._path.absolute)
|
||||
return Path(path)
|
||||
|
||||
def as_posix(self) -> str:
|
||||
return self._path.as_posix()
|
||||
|
||||
def as_uri(self) -> str:
|
||||
return self._path.as_uri()
|
||||
|
||||
def match(self, path_pattern: str) -> bool:
|
||||
return self._path.match(path_pattern)
|
||||
|
||||
def is_relative_to(self, *other: str | PathLike[str]) -> bool:
|
||||
try:
|
||||
self.relative_to(*other)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
async def chmod(self, mode: int, *, follow_symlinks: bool = True) -> None:
|
||||
func = partial(os.chmod, follow_symlinks=follow_symlinks)
|
||||
return await to_thread.run_sync(func, self._path, mode)
|
||||
|
||||
@classmethod
|
||||
async def cwd(cls) -> Path:
|
||||
path = await to_thread.run_sync(pathlib.Path.cwd)
|
||||
return cls(path)
|
||||
|
||||
async def exists(self) -> bool:
|
||||
return await to_thread.run_sync(self._path.exists, cancellable=True)
|
||||
|
||||
async def expanduser(self) -> Path:
|
||||
return Path(await to_thread.run_sync(self._path.expanduser, cancellable=True))
|
||||
|
||||
def glob(self, pattern: str) -> AsyncIterator[Path]:
|
||||
gen = self._path.glob(pattern)
|
||||
return _PathIterator(gen)
|
||||
|
||||
async def group(self) -> str:
|
||||
return await to_thread.run_sync(self._path.group, cancellable=True)
|
||||
|
||||
async def hardlink_to(self, target: str | pathlib.Path | Path) -> None:
|
||||
if isinstance(target, Path):
|
||||
target = target._path
|
||||
|
||||
await to_thread.run_sync(os.link, target, self)
|
||||
|
||||
@classmethod
|
||||
async def home(cls) -> Path:
|
||||
home_path = await to_thread.run_sync(pathlib.Path.home)
|
||||
return cls(home_path)
|
||||
|
||||
def is_absolute(self) -> bool:
|
||||
return self._path.is_absolute()
|
||||
|
||||
async def is_block_device(self) -> bool:
|
||||
return await to_thread.run_sync(self._path.is_block_device, cancellable=True)
|
||||
|
||||
async def is_char_device(self) -> bool:
|
||||
return await to_thread.run_sync(self._path.is_char_device, cancellable=True)
|
||||
|
||||
async def is_dir(self) -> bool:
|
||||
return await to_thread.run_sync(self._path.is_dir, cancellable=True)
|
||||
|
||||
async def is_fifo(self) -> bool:
|
||||
return await to_thread.run_sync(self._path.is_fifo, cancellable=True)
|
||||
|
||||
async def is_file(self) -> bool:
|
||||
return await to_thread.run_sync(self._path.is_file, cancellable=True)
|
||||
|
||||
async def is_mount(self) -> bool:
|
||||
return await to_thread.run_sync(os.path.ismount, self._path, cancellable=True)
|
||||
|
||||
def is_reserved(self) -> bool:
|
||||
return self._path.is_reserved()
|
||||
|
||||
async def is_socket(self) -> bool:
|
||||
return await to_thread.run_sync(self._path.is_socket, cancellable=True)
|
||||
|
||||
async def is_symlink(self) -> bool:
|
||||
return await to_thread.run_sync(self._path.is_symlink, cancellable=True)
|
||||
|
||||
def iterdir(self) -> AsyncIterator[Path]:
|
||||
gen = self._path.iterdir()
|
||||
return _PathIterator(gen)
|
||||
|
||||
def joinpath(self, *args: str | PathLike[str]) -> Path:
|
||||
return Path(self._path.joinpath(*args))
|
||||
|
||||
async def lchmod(self, mode: int) -> None:
|
||||
await to_thread.run_sync(self._path.lchmod, mode)
|
||||
|
||||
async def lstat(self) -> os.stat_result:
|
||||
return await to_thread.run_sync(self._path.lstat, cancellable=True)
|
||||
|
||||
async def mkdir(
|
||||
self, mode: int = 0o777, parents: bool = False, exist_ok: bool = False
|
||||
) -> None:
|
||||
await to_thread.run_sync(self._path.mkdir, mode, parents, exist_ok)
|
||||
|
||||
@overload
|
||||
async def open(
|
||||
self,
|
||||
mode: OpenBinaryMode,
|
||||
buffering: int = ...,
|
||||
encoding: str | None = ...,
|
||||
errors: str | None = ...,
|
||||
newline: str | None = ...,
|
||||
) -> AsyncFile[bytes]:
|
||||
...
|
||||
|
||||
@overload
|
||||
async def open(
|
||||
self,
|
||||
mode: OpenTextMode = ...,
|
||||
buffering: int = ...,
|
||||
encoding: str | None = ...,
|
||||
errors: str | None = ...,
|
||||
newline: str | None = ...,
|
||||
) -> AsyncFile[str]:
|
||||
...
|
||||
|
||||
async def open(
|
||||
self,
|
||||
mode: str = "r",
|
||||
buffering: int = -1,
|
||||
encoding: str | None = None,
|
||||
errors: str | None = None,
|
||||
newline: str | None = None,
|
||||
) -> AsyncFile[Any]:
|
||||
fp = await to_thread.run_sync(
|
||||
self._path.open, mode, buffering, encoding, errors, newline
|
||||
)
|
||||
return AsyncFile(fp)
|
||||
|
||||
async def owner(self) -> str:
|
||||
return await to_thread.run_sync(self._path.owner, cancellable=True)
|
||||
|
||||
async def read_bytes(self) -> bytes:
|
||||
return await to_thread.run_sync(self._path.read_bytes)
|
||||
|
||||
async def read_text(
|
||||
self, encoding: str | None = None, errors: str | None = None
|
||||
) -> str:
|
||||
return await to_thread.run_sync(self._path.read_text, encoding, errors)
|
||||
|
||||
def relative_to(self, *other: str | PathLike[str]) -> Path:
|
||||
return Path(self._path.relative_to(*other))
|
||||
|
||||
async def readlink(self) -> Path:
|
||||
target = await to_thread.run_sync(os.readlink, self._path)
|
||||
return Path(cast(str, target))
|
||||
|
||||
async def rename(self, target: str | pathlib.PurePath | Path) -> Path:
|
||||
if isinstance(target, Path):
|
||||
target = target._path
|
||||
|
||||
await to_thread.run_sync(self._path.rename, target)
|
||||
return Path(target)
|
||||
|
||||
async def replace(self, target: str | pathlib.PurePath | Path) -> Path:
|
||||
if isinstance(target, Path):
|
||||
target = target._path
|
||||
|
||||
await to_thread.run_sync(self._path.replace, target)
|
||||
return Path(target)
|
||||
|
||||
async def resolve(self, strict: bool = False) -> Path:
|
||||
func = partial(self._path.resolve, strict=strict)
|
||||
return Path(await to_thread.run_sync(func, cancellable=True))
|
||||
|
||||
def rglob(self, pattern: str) -> AsyncIterator[Path]:
|
||||
gen = self._path.rglob(pattern)
|
||||
return _PathIterator(gen)
|
||||
|
||||
async def rmdir(self) -> None:
|
||||
await to_thread.run_sync(self._path.rmdir)
|
||||
|
||||
async def samefile(
|
||||
self, other_path: str | bytes | int | pathlib.Path | Path
|
||||
) -> bool:
|
||||
if isinstance(other_path, Path):
|
||||
other_path = other_path._path
|
||||
|
||||
return await to_thread.run_sync(
|
||||
self._path.samefile, other_path, cancellable=True
|
||||
)
|
||||
|
||||
async def stat(self, *, follow_symlinks: bool = True) -> os.stat_result:
|
||||
func = partial(os.stat, follow_symlinks=follow_symlinks)
|
||||
return await to_thread.run_sync(func, self._path, cancellable=True)
|
||||
|
||||
async def symlink_to(
|
||||
self,
|
||||
target: str | pathlib.Path | Path,
|
||||
target_is_directory: bool = False,
|
||||
) -> None:
|
||||
if isinstance(target, Path):
|
||||
target = target._path
|
||||
|
||||
await to_thread.run_sync(self._path.symlink_to, target, target_is_directory)
|
||||
|
||||
async def touch(self, mode: int = 0o666, exist_ok: bool = True) -> None:
|
||||
await to_thread.run_sync(self._path.touch, mode, exist_ok)
|
||||
|
||||
async def unlink(self, missing_ok: bool = False) -> None:
|
||||
try:
|
||||
await to_thread.run_sync(self._path.unlink)
|
||||
except FileNotFoundError:
|
||||
if not missing_ok:
|
||||
raise
|
||||
|
||||
def with_name(self, name: str) -> Path:
|
||||
return Path(self._path.with_name(name))
|
||||
|
||||
def with_stem(self, stem: str) -> Path:
|
||||
return Path(self._path.with_name(stem + self._path.suffix))
|
||||
|
||||
def with_suffix(self, suffix: str) -> Path:
|
||||
return Path(self._path.with_suffix(suffix))
|
||||
|
||||
async def write_bytes(self, data: bytes) -> int:
|
||||
return await to_thread.run_sync(self._path.write_bytes, data)
|
||||
|
||||
async def write_text(
|
||||
self,
|
||||
data: str,
|
||||
encoding: str | None = None,
|
||||
errors: str | None = None,
|
||||
newline: str | None = None,
|
||||
) -> int:
|
||||
# Path.write_text() does not support the "newline" parameter before Python 3.10
|
||||
def sync_write_text() -> int:
|
||||
with self._path.open(
|
||||
"w", encoding=encoding, errors=errors, newline=newline
|
||||
) as fp:
|
||||
return fp.write(data)
|
||||
|
||||
return await to_thread.run_sync(sync_write_text)
|
||||
|
||||
|
||||
PathLike.register(Path)
|
||||
@@ -0,0 +1,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from ..abc import AsyncResource
|
||||
from ._tasks import CancelScope
|
||||
|
||||
|
||||
async def aclose_forcefully(resource: AsyncResource) -> None:
|
||||
"""
|
||||
Close an asynchronous resource in a cancelled scope.
|
||||
|
||||
Doing this closes the resource without waiting on anything.
|
||||
|
||||
:param resource: the resource to close
|
||||
|
||||
"""
|
||||
with CancelScope() as scope:
|
||||
scope.cancel()
|
||||
await resource.aclose()
|
||||
@@ -0,0 +1,26 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import AsyncIterator
|
||||
|
||||
from ._compat import DeprecatedAsyncContextManager
|
||||
from ._eventloop import get_asynclib
|
||||
|
||||
|
||||
def open_signal_receiver(
|
||||
*signals: int,
|
||||
) -> DeprecatedAsyncContextManager[AsyncIterator[int]]:
|
||||
"""
|
||||
Start receiving operating system signals.
|
||||
|
||||
:param signals: signals to receive (e.g. ``signal.SIGINT``)
|
||||
:return: an asynchronous context manager for an asynchronous iterator which yields signal
|
||||
numbers
|
||||
|
||||
.. warning:: Windows does not support signals natively so it is best to avoid relying on this
|
||||
in cross-platform applications.
|
||||
|
||||
.. warning:: On asyncio, this permanently replaces any previous signal handler for the given
|
||||
signals, as set via :meth:`~asyncio.loop.add_signal_handler`.
|
||||
|
||||
"""
|
||||
return get_asynclib().open_signal_receiver(*signals)
|
||||
@@ -0,0 +1,607 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import socket
|
||||
import ssl
|
||||
import sys
|
||||
from ipaddress import IPv6Address, ip_address
|
||||
from os import PathLike, chmod
|
||||
from pathlib import Path
|
||||
from socket import AddressFamily, SocketKind
|
||||
from typing import Awaitable, List, Tuple, cast, overload
|
||||
|
||||
from .. import to_thread
|
||||
from ..abc import (
|
||||
ConnectedUDPSocket,
|
||||
IPAddressType,
|
||||
IPSockAddrType,
|
||||
SocketListener,
|
||||
SocketStream,
|
||||
UDPSocket,
|
||||
UNIXSocketStream,
|
||||
)
|
||||
from ..streams.stapled import MultiListener
|
||||
from ..streams.tls import TLSStream
|
||||
from ._eventloop import get_asynclib
|
||||
from ._resources import aclose_forcefully
|
||||
from ._synchronization import Event
|
||||
from ._tasks import create_task_group, move_on_after
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
from typing import Literal
|
||||
else:
|
||||
from typing_extensions import Literal
|
||||
|
||||
IPPROTO_IPV6 = getattr(socket, "IPPROTO_IPV6", 41) # https://bugs.python.org/issue29515
|
||||
|
||||
GetAddrInfoReturnType = List[
|
||||
Tuple[AddressFamily, SocketKind, int, str, Tuple[str, int]]
|
||||
]
|
||||
AnyIPAddressFamily = Literal[
|
||||
AddressFamily.AF_UNSPEC, AddressFamily.AF_INET, AddressFamily.AF_INET6
|
||||
]
|
||||
IPAddressFamily = Literal[AddressFamily.AF_INET, AddressFamily.AF_INET6]
|
||||
|
||||
|
||||
# tls_hostname given
|
||||
@overload
|
||||
async def connect_tcp(
|
||||
remote_host: IPAddressType,
|
||||
remote_port: int,
|
||||
*,
|
||||
local_host: IPAddressType | None = ...,
|
||||
ssl_context: ssl.SSLContext | None = ...,
|
||||
tls_standard_compatible: bool = ...,
|
||||
tls_hostname: str,
|
||||
happy_eyeballs_delay: float = ...,
|
||||
) -> TLSStream:
|
||||
...
|
||||
|
||||
|
||||
# ssl_context given
|
||||
@overload
|
||||
async def connect_tcp(
|
||||
remote_host: IPAddressType,
|
||||
remote_port: int,
|
||||
*,
|
||||
local_host: IPAddressType | None = ...,
|
||||
ssl_context: ssl.SSLContext,
|
||||
tls_standard_compatible: bool = ...,
|
||||
tls_hostname: str | None = ...,
|
||||
happy_eyeballs_delay: float = ...,
|
||||
) -> TLSStream:
|
||||
...
|
||||
|
||||
|
||||
# tls=True
|
||||
@overload
|
||||
async def connect_tcp(
|
||||
remote_host: IPAddressType,
|
||||
remote_port: int,
|
||||
*,
|
||||
local_host: IPAddressType | None = ...,
|
||||
tls: Literal[True],
|
||||
ssl_context: ssl.SSLContext | None = ...,
|
||||
tls_standard_compatible: bool = ...,
|
||||
tls_hostname: str | None = ...,
|
||||
happy_eyeballs_delay: float = ...,
|
||||
) -> TLSStream:
|
||||
...
|
||||
|
||||
|
||||
# tls=False
|
||||
@overload
|
||||
async def connect_tcp(
|
||||
remote_host: IPAddressType,
|
||||
remote_port: int,
|
||||
*,
|
||||
local_host: IPAddressType | None = ...,
|
||||
tls: Literal[False],
|
||||
ssl_context: ssl.SSLContext | None = ...,
|
||||
tls_standard_compatible: bool = ...,
|
||||
tls_hostname: str | None = ...,
|
||||
happy_eyeballs_delay: float = ...,
|
||||
) -> SocketStream:
|
||||
...
|
||||
|
||||
|
||||
# No TLS arguments
|
||||
@overload
|
||||
async def connect_tcp(
|
||||
remote_host: IPAddressType,
|
||||
remote_port: int,
|
||||
*,
|
||||
local_host: IPAddressType | None = ...,
|
||||
happy_eyeballs_delay: float = ...,
|
||||
) -> SocketStream:
|
||||
...
|
||||
|
||||
|
||||
async def connect_tcp(
|
||||
remote_host: IPAddressType,
|
||||
remote_port: int,
|
||||
*,
|
||||
local_host: IPAddressType | None = None,
|
||||
tls: bool = False,
|
||||
ssl_context: ssl.SSLContext | None = None,
|
||||
tls_standard_compatible: bool = True,
|
||||
tls_hostname: str | None = None,
|
||||
happy_eyeballs_delay: float = 0.25,
|
||||
) -> SocketStream | TLSStream:
|
||||
"""
|
||||
Connect to a host using the TCP protocol.
|
||||
|
||||
This function implements the stateless version of the Happy Eyeballs algorithm (RFC
|
||||
6555). If ``remote_host`` is a host name that resolves to multiple IP addresses,
|
||||
each one is tried until one connection attempt succeeds. If the first attempt does
|
||||
not connected within 250 milliseconds, a second attempt is started using the next
|
||||
address in the list, and so on. On IPv6 enabled systems, an IPv6 address (if
|
||||
available) is tried first.
|
||||
|
||||
When the connection has been established, a TLS handshake will be done if either
|
||||
``ssl_context`` or ``tls_hostname`` is not ``None``, or if ``tls`` is ``True``.
|
||||
|
||||
:param remote_host: the IP address or host name to connect to
|
||||
:param remote_port: port on the target host to connect to
|
||||
:param local_host: the interface address or name to bind the socket to before connecting
|
||||
:param tls: ``True`` to do a TLS handshake with the connected stream and return a
|
||||
:class:`~anyio.streams.tls.TLSStream` instead
|
||||
:param ssl_context: the SSL context object to use (if omitted, a default context is created)
|
||||
:param tls_standard_compatible: If ``True``, performs the TLS shutdown handshake before closing
|
||||
the stream and requires that the server does this as well. Otherwise,
|
||||
:exc:`~ssl.SSLEOFError` may be raised during reads from the stream.
|
||||
Some protocols, such as HTTP, require this option to be ``False``.
|
||||
See :meth:`~ssl.SSLContext.wrap_socket` for details.
|
||||
:param tls_hostname: host name to check the server certificate against (defaults to the value
|
||||
of ``remote_host``)
|
||||
:param happy_eyeballs_delay: delay (in seconds) before starting the next connection attempt
|
||||
:return: a socket stream object if no TLS handshake was done, otherwise a TLS stream
|
||||
:raises OSError: if the connection attempt fails
|
||||
|
||||
"""
|
||||
# Placed here due to https://github.com/python/mypy/issues/7057
|
||||
connected_stream: SocketStream | None = None
|
||||
|
||||
async def try_connect(remote_host: str, event: Event) -> None:
|
||||
nonlocal connected_stream
|
||||
try:
|
||||
stream = await asynclib.connect_tcp(remote_host, remote_port, local_address)
|
||||
except OSError as exc:
|
||||
oserrors.append(exc)
|
||||
return
|
||||
else:
|
||||
if connected_stream is None:
|
||||
connected_stream = stream
|
||||
tg.cancel_scope.cancel()
|
||||
else:
|
||||
await stream.aclose()
|
||||
finally:
|
||||
event.set()
|
||||
|
||||
asynclib = get_asynclib()
|
||||
local_address: IPSockAddrType | None = None
|
||||
family = socket.AF_UNSPEC
|
||||
if local_host:
|
||||
gai_res = await getaddrinfo(str(local_host), None)
|
||||
family, *_, local_address = gai_res[0]
|
||||
|
||||
target_host = str(remote_host)
|
||||
try:
|
||||
addr_obj = ip_address(remote_host)
|
||||
except ValueError:
|
||||
# getaddrinfo() will raise an exception if name resolution fails
|
||||
gai_res = await getaddrinfo(
|
||||
target_host, remote_port, family=family, type=socket.SOCK_STREAM
|
||||
)
|
||||
|
||||
# Organize the list so that the first address is an IPv6 address (if available) and the
|
||||
# second one is an IPv4 addresses. The rest can be in whatever order.
|
||||
v6_found = v4_found = False
|
||||
target_addrs: list[tuple[socket.AddressFamily, str]] = []
|
||||
for af, *rest, sa in gai_res:
|
||||
if af == socket.AF_INET6 and not v6_found:
|
||||
v6_found = True
|
||||
target_addrs.insert(0, (af, sa[0]))
|
||||
elif af == socket.AF_INET and not v4_found and v6_found:
|
||||
v4_found = True
|
||||
target_addrs.insert(1, (af, sa[0]))
|
||||
else:
|
||||
target_addrs.append((af, sa[0]))
|
||||
else:
|
||||
if isinstance(addr_obj, IPv6Address):
|
||||
target_addrs = [(socket.AF_INET6, addr_obj.compressed)]
|
||||
else:
|
||||
target_addrs = [(socket.AF_INET, addr_obj.compressed)]
|
||||
|
||||
oserrors: list[OSError] = []
|
||||
async with create_task_group() as tg:
|
||||
for i, (af, addr) in enumerate(target_addrs):
|
||||
event = Event()
|
||||
tg.start_soon(try_connect, addr, event)
|
||||
with move_on_after(happy_eyeballs_delay):
|
||||
await event.wait()
|
||||
|
||||
if connected_stream is None:
|
||||
cause = oserrors[0] if len(oserrors) == 1 else asynclib.ExceptionGroup(oserrors)
|
||||
raise OSError("All connection attempts failed") from cause
|
||||
|
||||
if tls or tls_hostname or ssl_context:
|
||||
try:
|
||||
return await TLSStream.wrap(
|
||||
connected_stream,
|
||||
server_side=False,
|
||||
hostname=tls_hostname or str(remote_host),
|
||||
ssl_context=ssl_context,
|
||||
standard_compatible=tls_standard_compatible,
|
||||
)
|
||||
except BaseException:
|
||||
await aclose_forcefully(connected_stream)
|
||||
raise
|
||||
|
||||
return connected_stream
|
||||
|
||||
|
||||
async def connect_unix(path: str | PathLike[str]) -> UNIXSocketStream:
|
||||
"""
|
||||
Connect to the given UNIX socket.
|
||||
|
||||
Not available on Windows.
|
||||
|
||||
:param path: path to the socket
|
||||
:return: a socket stream object
|
||||
|
||||
"""
|
||||
path = str(Path(path))
|
||||
return await get_asynclib().connect_unix(path)
|
||||
|
||||
|
||||
async def create_tcp_listener(
|
||||
*,
|
||||
local_host: IPAddressType | None = None,
|
||||
local_port: int = 0,
|
||||
family: AnyIPAddressFamily = socket.AddressFamily.AF_UNSPEC,
|
||||
backlog: int = 65536,
|
||||
reuse_port: bool = False,
|
||||
) -> MultiListener[SocketStream]:
|
||||
"""
|
||||
Create a TCP socket listener.
|
||||
|
||||
:param local_port: port number to listen on
|
||||
:param local_host: IP address of the interface to listen on. If omitted, listen on
|
||||
all IPv4 and IPv6 interfaces. To listen on all interfaces on a specific address
|
||||
family, use ``0.0.0.0`` for IPv4 or ``::`` for IPv6.
|
||||
:param family: address family (used if ``local_host`` was omitted)
|
||||
:param backlog: maximum number of queued incoming connections (up to a maximum of
|
||||
2**16, or 65536)
|
||||
:param reuse_port: ``True`` to allow multiple sockets to bind to the same
|
||||
address/port (not supported on Windows)
|
||||
:return: a list of listener objects
|
||||
|
||||
"""
|
||||
asynclib = get_asynclib()
|
||||
backlog = min(backlog, 65536)
|
||||
local_host = str(local_host) if local_host is not None else None
|
||||
gai_res = await getaddrinfo(
|
||||
local_host, # type: ignore[arg-type]
|
||||
local_port,
|
||||
family=family,
|
||||
type=socket.SocketKind.SOCK_STREAM if sys.platform == "win32" else 0,
|
||||
flags=socket.AI_PASSIVE | socket.AI_ADDRCONFIG,
|
||||
)
|
||||
listeners: list[SocketListener] = []
|
||||
try:
|
||||
# The set() is here to work around a glibc bug:
|
||||
# https://sourceware.org/bugzilla/show_bug.cgi?id=14969
|
||||
sockaddr: tuple[str, int] | tuple[str, int, int, int]
|
||||
for fam, kind, *_, sockaddr in sorted(set(gai_res)):
|
||||
# Workaround for an uvloop bug where we don't get the correct scope ID for
|
||||
# IPv6 link-local addresses when passing type=socket.SOCK_STREAM to
|
||||
# getaddrinfo(): https://github.com/MagicStack/uvloop/issues/539
|
||||
if sys.platform != "win32" and kind is not SocketKind.SOCK_STREAM:
|
||||
continue
|
||||
|
||||
raw_socket = socket.socket(fam)
|
||||
raw_socket.setblocking(False)
|
||||
|
||||
# For Windows, enable exclusive address use. For others, enable address reuse.
|
||||
if sys.platform == "win32":
|
||||
raw_socket.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1)
|
||||
else:
|
||||
raw_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
|
||||
if reuse_port:
|
||||
raw_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
||||
|
||||
# If only IPv6 was requested, disable dual stack operation
|
||||
if fam == socket.AF_INET6:
|
||||
raw_socket.setsockopt(IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
|
||||
|
||||
# Workaround for #554
|
||||
if "%" in sockaddr[0]:
|
||||
addr, scope_id = sockaddr[0].split("%", 1)
|
||||
sockaddr = (addr, sockaddr[1], 0, int(scope_id))
|
||||
|
||||
raw_socket.bind(sockaddr)
|
||||
raw_socket.listen(backlog)
|
||||
listener = asynclib.TCPSocketListener(raw_socket)
|
||||
listeners.append(listener)
|
||||
except BaseException:
|
||||
for listener in listeners:
|
||||
await listener.aclose()
|
||||
|
||||
raise
|
||||
|
||||
return MultiListener(listeners)
|
||||
|
||||
|
||||
async def create_unix_listener(
|
||||
path: str | PathLike[str],
|
||||
*,
|
||||
mode: int | None = None,
|
||||
backlog: int = 65536,
|
||||
) -> SocketListener:
|
||||
"""
|
||||
Create a UNIX socket listener.
|
||||
|
||||
Not available on Windows.
|
||||
|
||||
:param path: path of the socket
|
||||
:param mode: permissions to set on the socket
|
||||
:param backlog: maximum number of queued incoming connections (up to a maximum of 2**16, or
|
||||
65536)
|
||||
:return: a listener object
|
||||
|
||||
.. versionchanged:: 3.0
|
||||
If a socket already exists on the file system in the given path, it will be removed first.
|
||||
|
||||
"""
|
||||
path_str = str(path)
|
||||
path = Path(path)
|
||||
if path.is_socket():
|
||||
path.unlink()
|
||||
|
||||
backlog = min(backlog, 65536)
|
||||
raw_socket = socket.socket(socket.AF_UNIX)
|
||||
raw_socket.setblocking(False)
|
||||
try:
|
||||
await to_thread.run_sync(raw_socket.bind, path_str, cancellable=True)
|
||||
if mode is not None:
|
||||
await to_thread.run_sync(chmod, path_str, mode, cancellable=True)
|
||||
|
||||
raw_socket.listen(backlog)
|
||||
return get_asynclib().UNIXSocketListener(raw_socket)
|
||||
except BaseException:
|
||||
raw_socket.close()
|
||||
raise
|
||||
|
||||
|
||||
async def create_udp_socket(
|
||||
family: AnyIPAddressFamily = AddressFamily.AF_UNSPEC,
|
||||
*,
|
||||
local_host: IPAddressType | None = None,
|
||||
local_port: int = 0,
|
||||
reuse_port: bool = False,
|
||||
) -> UDPSocket:
|
||||
"""
|
||||
Create a UDP socket.
|
||||
|
||||
If ``local_port`` has been given, the socket will be bound to this port on the local
|
||||
machine, making this socket suitable for providing UDP based services.
|
||||
|
||||
:param family: address family (``AF_INET`` or ``AF_INET6``) – automatically determined from
|
||||
``local_host`` if omitted
|
||||
:param local_host: IP address or host name of the local interface to bind to
|
||||
:param local_port: local port to bind to
|
||||
:param reuse_port: ``True`` to allow multiple sockets to bind to the same address/port
|
||||
(not supported on Windows)
|
||||
:return: a UDP socket
|
||||
|
||||
"""
|
||||
if family is AddressFamily.AF_UNSPEC and not local_host:
|
||||
raise ValueError('Either "family" or "local_host" must be given')
|
||||
|
||||
if local_host:
|
||||
gai_res = await getaddrinfo(
|
||||
str(local_host),
|
||||
local_port,
|
||||
family=family,
|
||||
type=socket.SOCK_DGRAM,
|
||||
flags=socket.AI_PASSIVE | socket.AI_ADDRCONFIG,
|
||||
)
|
||||
family = cast(AnyIPAddressFamily, gai_res[0][0])
|
||||
local_address = gai_res[0][-1]
|
||||
elif family is AddressFamily.AF_INET6:
|
||||
local_address = ("::", 0)
|
||||
else:
|
||||
local_address = ("0.0.0.0", 0)
|
||||
|
||||
return await get_asynclib().create_udp_socket(
|
||||
family, local_address, None, reuse_port
|
||||
)
|
||||
|
||||
|
||||
async def create_connected_udp_socket(
|
||||
remote_host: IPAddressType,
|
||||
remote_port: int,
|
||||
*,
|
||||
family: AnyIPAddressFamily = AddressFamily.AF_UNSPEC,
|
||||
local_host: IPAddressType | None = None,
|
||||
local_port: int = 0,
|
||||
reuse_port: bool = False,
|
||||
) -> ConnectedUDPSocket:
|
||||
"""
|
||||
Create a connected UDP socket.
|
||||
|
||||
Connected UDP sockets can only communicate with the specified remote host/port, and any packets
|
||||
sent from other sources are dropped.
|
||||
|
||||
:param remote_host: remote host to set as the default target
|
||||
:param remote_port: port on the remote host to set as the default target
|
||||
:param family: address family (``AF_INET`` or ``AF_INET6``) – automatically determined from
|
||||
``local_host`` or ``remote_host`` if omitted
|
||||
:param local_host: IP address or host name of the local interface to bind to
|
||||
:param local_port: local port to bind to
|
||||
:param reuse_port: ``True`` to allow multiple sockets to bind to the same address/port
|
||||
(not supported on Windows)
|
||||
:return: a connected UDP socket
|
||||
|
||||
"""
|
||||
local_address = None
|
||||
if local_host:
|
||||
gai_res = await getaddrinfo(
|
||||
str(local_host),
|
||||
local_port,
|
||||
family=family,
|
||||
type=socket.SOCK_DGRAM,
|
||||
flags=socket.AI_PASSIVE | socket.AI_ADDRCONFIG,
|
||||
)
|
||||
family = cast(AnyIPAddressFamily, gai_res[0][0])
|
||||
local_address = gai_res[0][-1]
|
||||
|
||||
gai_res = await getaddrinfo(
|
||||
str(remote_host), remote_port, family=family, type=socket.SOCK_DGRAM
|
||||
)
|
||||
family = cast(AnyIPAddressFamily, gai_res[0][0])
|
||||
remote_address = gai_res[0][-1]
|
||||
|
||||
return await get_asynclib().create_udp_socket(
|
||||
family, local_address, remote_address, reuse_port
|
||||
)
|
||||
|
||||
|
||||
async def getaddrinfo(
|
||||
host: bytearray | bytes | str,
|
||||
port: str | int | None,
|
||||
*,
|
||||
family: int | AddressFamily = 0,
|
||||
type: int | SocketKind = 0,
|
||||
proto: int = 0,
|
||||
flags: int = 0,
|
||||
) -> GetAddrInfoReturnType:
|
||||
"""
|
||||
Look up a numeric IP address given a host name.
|
||||
|
||||
Internationalized domain names are translated according to the (non-transitional) IDNA 2008
|
||||
standard.
|
||||
|
||||
.. note:: 4-tuple IPv6 socket addresses are automatically converted to 2-tuples of
|
||||
(host, port), unlike what :func:`socket.getaddrinfo` does.
|
||||
|
||||
:param host: host name
|
||||
:param port: port number
|
||||
:param family: socket family (`'AF_INET``, ...)
|
||||
:param type: socket type (``SOCK_STREAM``, ...)
|
||||
:param proto: protocol number
|
||||
:param flags: flags to pass to upstream ``getaddrinfo()``
|
||||
:return: list of tuples containing (family, type, proto, canonname, sockaddr)
|
||||
|
||||
.. seealso:: :func:`socket.getaddrinfo`
|
||||
|
||||
"""
|
||||
# Handle unicode hostnames
|
||||
if isinstance(host, str):
|
||||
try:
|
||||
encoded_host = host.encode("ascii")
|
||||
except UnicodeEncodeError:
|
||||
import idna
|
||||
|
||||
encoded_host = idna.encode(host, uts46=True)
|
||||
else:
|
||||
encoded_host = host
|
||||
|
||||
gai_res = await get_asynclib().getaddrinfo(
|
||||
encoded_host, port, family=family, type=type, proto=proto, flags=flags
|
||||
)
|
||||
return [
|
||||
(family, type, proto, canonname, convert_ipv6_sockaddr(sockaddr))
|
||||
for family, type, proto, canonname, sockaddr in gai_res
|
||||
]
|
||||
|
||||
|
||||
def getnameinfo(sockaddr: IPSockAddrType, flags: int = 0) -> Awaitable[tuple[str, str]]:
|
||||
"""
|
||||
Look up the host name of an IP address.
|
||||
|
||||
:param sockaddr: socket address (e.g. (ipaddress, port) for IPv4)
|
||||
:param flags: flags to pass to upstream ``getnameinfo()``
|
||||
:return: a tuple of (host name, service name)
|
||||
|
||||
.. seealso:: :func:`socket.getnameinfo`
|
||||
|
||||
"""
|
||||
return get_asynclib().getnameinfo(sockaddr, flags)
|
||||
|
||||
|
||||
def wait_socket_readable(sock: socket.socket) -> Awaitable[None]:
|
||||
"""
|
||||
Wait until the given socket has data to be read.
|
||||
|
||||
This does **NOT** work on Windows when using the asyncio backend with a proactor event loop
|
||||
(default on py3.8+).
|
||||
|
||||
.. warning:: Only use this on raw sockets that have not been wrapped by any higher level
|
||||
constructs like socket streams!
|
||||
|
||||
:param sock: a socket object
|
||||
:raises ~anyio.ClosedResourceError: if the socket was closed while waiting for the
|
||||
socket to become readable
|
||||
:raises ~anyio.BusyResourceError: if another task is already waiting for the socket
|
||||
to become readable
|
||||
|
||||
"""
|
||||
return get_asynclib().wait_socket_readable(sock)
|
||||
|
||||
|
||||
def wait_socket_writable(sock: socket.socket) -> Awaitable[None]:
|
||||
"""
|
||||
Wait until the given socket can be written to.
|
||||
|
||||
This does **NOT** work on Windows when using the asyncio backend with a proactor event loop
|
||||
(default on py3.8+).
|
||||
|
||||
.. warning:: Only use this on raw sockets that have not been wrapped by any higher level
|
||||
constructs like socket streams!
|
||||
|
||||
:param sock: a socket object
|
||||
:raises ~anyio.ClosedResourceError: if the socket was closed while waiting for the
|
||||
socket to become writable
|
||||
:raises ~anyio.BusyResourceError: if another task is already waiting for the socket
|
||||
to become writable
|
||||
|
||||
"""
|
||||
return get_asynclib().wait_socket_writable(sock)
|
||||
|
||||
|
||||
#
|
||||
# Private API
|
||||
#
|
||||
|
||||
|
||||
def convert_ipv6_sockaddr(
|
||||
sockaddr: tuple[str, int, int, int] | tuple[str, int]
|
||||
) -> tuple[str, int]:
|
||||
"""
|
||||
Convert a 4-tuple IPv6 socket address to a 2-tuple (address, port) format.
|
||||
|
||||
If the scope ID is nonzero, it is added to the address, separated with ``%``.
|
||||
Otherwise the flow id and scope id are simply cut off from the tuple.
|
||||
Any other kinds of socket addresses are returned as-is.
|
||||
|
||||
:param sockaddr: the result of :meth:`~socket.socket.getsockname`
|
||||
:return: the converted socket address
|
||||
|
||||
"""
|
||||
# This is more complicated than it should be because of MyPy
|
||||
if isinstance(sockaddr, tuple) and len(sockaddr) == 4:
|
||||
host, port, flowinfo, scope_id = cast(Tuple[str, int, int, int], sockaddr)
|
||||
if scope_id:
|
||||
# PyPy (as of v7.3.11) leaves the interface name in the result, so
|
||||
# we discard it and only get the scope ID from the end
|
||||
# (https://foss.heptapod.net/pypy/pypy/-/issues/3938)
|
||||
host = host.split("%")[0]
|
||||
|
||||
# Add scope_id to the address
|
||||
return f"{host}%{scope_id}", port
|
||||
else:
|
||||
return host, port
|
||||
else:
|
||||
return cast(Tuple[str, int], sockaddr)
|
||||
@@ -0,0 +1,47 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from typing import Any, TypeVar, overload
|
||||
|
||||
from ..streams.memory import (
|
||||
MemoryObjectReceiveStream,
|
||||
MemoryObjectSendStream,
|
||||
MemoryObjectStreamState,
|
||||
)
|
||||
|
||||
T_Item = TypeVar("T_Item")
|
||||
|
||||
|
||||
@overload
|
||||
def create_memory_object_stream(
|
||||
max_buffer_size: float = ...,
|
||||
) -> tuple[MemoryObjectSendStream[Any], MemoryObjectReceiveStream[Any]]:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def create_memory_object_stream(
|
||||
max_buffer_size: float = ..., item_type: type[T_Item] = ...
|
||||
) -> tuple[MemoryObjectSendStream[T_Item], MemoryObjectReceiveStream[T_Item]]:
|
||||
...
|
||||
|
||||
|
||||
def create_memory_object_stream(
|
||||
max_buffer_size: float = 0, item_type: type[T_Item] | None = None
|
||||
) -> tuple[MemoryObjectSendStream[Any], MemoryObjectReceiveStream[Any]]:
|
||||
"""
|
||||
Create a memory object stream.
|
||||
|
||||
:param max_buffer_size: number of items held in the buffer until ``send()`` starts blocking
|
||||
:param item_type: type of item, for marking the streams with the right generic type for
|
||||
static typing (not used at run time)
|
||||
:return: a tuple of (send stream, receive stream)
|
||||
|
||||
"""
|
||||
if max_buffer_size != math.inf and not isinstance(max_buffer_size, int):
|
||||
raise ValueError("max_buffer_size must be either an integer or math.inf")
|
||||
if max_buffer_size < 0:
|
||||
raise ValueError("max_buffer_size cannot be negative")
|
||||
|
||||
state: MemoryObjectStreamState = MemoryObjectStreamState(max_buffer_size)
|
||||
return MemoryObjectSendStream(state), MemoryObjectReceiveStream(state)
|
||||
@@ -0,0 +1,135 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from io import BytesIO
|
||||
from os import PathLike
|
||||
from subprocess import DEVNULL, PIPE, CalledProcessError, CompletedProcess
|
||||
from typing import (
|
||||
IO,
|
||||
Any,
|
||||
AsyncIterable,
|
||||
Mapping,
|
||||
Sequence,
|
||||
cast,
|
||||
)
|
||||
|
||||
from ..abc import Process
|
||||
from ._eventloop import get_asynclib
|
||||
from ._tasks import create_task_group
|
||||
|
||||
|
||||
async def run_process(
|
||||
command: str | bytes | Sequence[str | bytes],
|
||||
*,
|
||||
input: bytes | None = None,
|
||||
stdout: int | IO[Any] | None = PIPE,
|
||||
stderr: int | IO[Any] | None = PIPE,
|
||||
check: bool = True,
|
||||
cwd: str | bytes | PathLike[str] | None = None,
|
||||
env: Mapping[str, str] | None = None,
|
||||
start_new_session: bool = False,
|
||||
) -> CompletedProcess[bytes]:
|
||||
"""
|
||||
Run an external command in a subprocess and wait until it completes.
|
||||
|
||||
.. seealso:: :func:`subprocess.run`
|
||||
|
||||
:param command: either a string to pass to the shell, or an iterable of strings containing the
|
||||
executable name or path and its arguments
|
||||
:param input: bytes passed to the standard input of the subprocess
|
||||
:param stdout: either :data:`subprocess.PIPE` or :data:`subprocess.DEVNULL`
|
||||
:param stderr: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL` or
|
||||
:data:`subprocess.STDOUT`
|
||||
:param check: if ``True``, raise :exc:`~subprocess.CalledProcessError` if the process
|
||||
terminates with a return code other than 0
|
||||
:param cwd: If not ``None``, change the working directory to this before running the command
|
||||
:param env: if not ``None``, this mapping replaces the inherited environment variables from the
|
||||
parent process
|
||||
:param start_new_session: if ``true`` the setsid() system call will be made in the child
|
||||
process prior to the execution of the subprocess. (POSIX only)
|
||||
:return: an object representing the completed process
|
||||
:raises ~subprocess.CalledProcessError: if ``check`` is ``True`` and the process exits with a
|
||||
nonzero return code
|
||||
|
||||
"""
|
||||
|
||||
async def drain_stream(stream: AsyncIterable[bytes], index: int) -> None:
|
||||
buffer = BytesIO()
|
||||
async for chunk in stream:
|
||||
buffer.write(chunk)
|
||||
|
||||
stream_contents[index] = buffer.getvalue()
|
||||
|
||||
async with await open_process(
|
||||
command,
|
||||
stdin=PIPE if input else DEVNULL,
|
||||
stdout=stdout,
|
||||
stderr=stderr,
|
||||
cwd=cwd,
|
||||
env=env,
|
||||
start_new_session=start_new_session,
|
||||
) as process:
|
||||
stream_contents: list[bytes | None] = [None, None]
|
||||
try:
|
||||
async with create_task_group() as tg:
|
||||
if process.stdout:
|
||||
tg.start_soon(drain_stream, process.stdout, 0)
|
||||
if process.stderr:
|
||||
tg.start_soon(drain_stream, process.stderr, 1)
|
||||
if process.stdin and input:
|
||||
await process.stdin.send(input)
|
||||
await process.stdin.aclose()
|
||||
|
||||
await process.wait()
|
||||
except BaseException:
|
||||
process.kill()
|
||||
raise
|
||||
|
||||
output, errors = stream_contents
|
||||
if check and process.returncode != 0:
|
||||
raise CalledProcessError(cast(int, process.returncode), command, output, errors)
|
||||
|
||||
return CompletedProcess(command, cast(int, process.returncode), output, errors)
|
||||
|
||||
|
||||
async def open_process(
|
||||
command: str | bytes | Sequence[str | bytes],
|
||||
*,
|
||||
stdin: int | IO[Any] | None = PIPE,
|
||||
stdout: int | IO[Any] | None = PIPE,
|
||||
stderr: int | IO[Any] | None = PIPE,
|
||||
cwd: str | bytes | PathLike[str] | None = None,
|
||||
env: Mapping[str, str] | None = None,
|
||||
start_new_session: bool = False,
|
||||
) -> Process:
|
||||
"""
|
||||
Start an external command in a subprocess.
|
||||
|
||||
.. seealso:: :class:`subprocess.Popen`
|
||||
|
||||
:param command: either a string to pass to the shell, or an iterable of strings containing the
|
||||
executable name or path and its arguments
|
||||
:param stdin: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`, a
|
||||
file-like object, or ``None``
|
||||
:param stdout: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`,
|
||||
a file-like object, or ``None``
|
||||
:param stderr: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`,
|
||||
:data:`subprocess.STDOUT`, a file-like object, or ``None``
|
||||
:param cwd: If not ``None``, the working directory is changed before executing
|
||||
:param env: If env is not ``None``, it must be a mapping that defines the environment
|
||||
variables for the new process
|
||||
:param start_new_session: if ``true`` the setsid() system call will be made in the child
|
||||
process prior to the execution of the subprocess. (POSIX only)
|
||||
:return: an asynchronous process object
|
||||
|
||||
"""
|
||||
shell = isinstance(command, str)
|
||||
return await get_asynclib().open_process(
|
||||
command,
|
||||
shell=shell,
|
||||
stdin=stdin,
|
||||
stdout=stdout,
|
||||
stderr=stderr,
|
||||
cwd=cwd,
|
||||
env=env,
|
||||
start_new_session=start_new_session,
|
||||
)
|
||||
@@ -0,0 +1,596 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import deque
|
||||
from dataclasses import dataclass
|
||||
from types import TracebackType
|
||||
from warnings import warn
|
||||
|
||||
from ..lowlevel import cancel_shielded_checkpoint, checkpoint, checkpoint_if_cancelled
|
||||
from ._compat import DeprecatedAwaitable
|
||||
from ._eventloop import get_asynclib
|
||||
from ._exceptions import BusyResourceError, WouldBlock
|
||||
from ._tasks import CancelScope
|
||||
from ._testing import TaskInfo, get_current_task
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EventStatistics:
|
||||
"""
|
||||
:ivar int tasks_waiting: number of tasks waiting on :meth:`~.Event.wait`
|
||||
"""
|
||||
|
||||
tasks_waiting: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CapacityLimiterStatistics:
|
||||
"""
|
||||
:ivar int borrowed_tokens: number of tokens currently borrowed by tasks
|
||||
:ivar float total_tokens: total number of available tokens
|
||||
:ivar tuple borrowers: tasks or other objects currently holding tokens borrowed from this
|
||||
limiter
|
||||
:ivar int tasks_waiting: number of tasks waiting on :meth:`~.CapacityLimiter.acquire` or
|
||||
:meth:`~.CapacityLimiter.acquire_on_behalf_of`
|
||||
"""
|
||||
|
||||
borrowed_tokens: int
|
||||
total_tokens: float
|
||||
borrowers: tuple[object, ...]
|
||||
tasks_waiting: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LockStatistics:
|
||||
"""
|
||||
:ivar bool locked: flag indicating if this lock is locked or not
|
||||
:ivar ~anyio.TaskInfo owner: task currently holding the lock (or ``None`` if the lock is not
|
||||
held by any task)
|
||||
:ivar int tasks_waiting: number of tasks waiting on :meth:`~.Lock.acquire`
|
||||
"""
|
||||
|
||||
locked: bool
|
||||
owner: TaskInfo | None
|
||||
tasks_waiting: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ConditionStatistics:
|
||||
"""
|
||||
:ivar int tasks_waiting: number of tasks blocked on :meth:`~.Condition.wait`
|
||||
:ivar ~anyio.LockStatistics lock_statistics: statistics of the underlying :class:`~.Lock`
|
||||
"""
|
||||
|
||||
tasks_waiting: int
|
||||
lock_statistics: LockStatistics
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SemaphoreStatistics:
|
||||
"""
|
||||
:ivar int tasks_waiting: number of tasks waiting on :meth:`~.Semaphore.acquire`
|
||||
|
||||
"""
|
||||
|
||||
tasks_waiting: int
|
||||
|
||||
|
||||
class Event:
|
||||
def __new__(cls) -> Event:
|
||||
return get_asynclib().Event()
|
||||
|
||||
def set(self) -> DeprecatedAwaitable:
|
||||
"""Set the flag, notifying all listeners."""
|
||||
raise NotImplementedError
|
||||
|
||||
def is_set(self) -> bool:
|
||||
"""Return ``True`` if the flag is set, ``False`` if not."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def wait(self) -> None:
|
||||
"""
|
||||
Wait until the flag has been set.
|
||||
|
||||
If the flag has already been set when this method is called, it returns immediately.
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def statistics(self) -> EventStatistics:
|
||||
"""Return statistics about the current state of this event."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class Lock:
|
||||
_owner_task: TaskInfo | None = None
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._waiters: deque[tuple[TaskInfo, Event]] = deque()
|
||||
|
||||
async def __aenter__(self) -> None:
|
||||
await self.acquire()
|
||||
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> None:
|
||||
self.release()
|
||||
|
||||
async def acquire(self) -> None:
|
||||
"""Acquire the lock."""
|
||||
await checkpoint_if_cancelled()
|
||||
try:
|
||||
self.acquire_nowait()
|
||||
except WouldBlock:
|
||||
task = get_current_task()
|
||||
event = Event()
|
||||
token = task, event
|
||||
self._waiters.append(token)
|
||||
try:
|
||||
await event.wait()
|
||||
except BaseException:
|
||||
if not event.is_set():
|
||||
self._waiters.remove(token)
|
||||
elif self._owner_task == task:
|
||||
self.release()
|
||||
|
||||
raise
|
||||
|
||||
assert self._owner_task == task
|
||||
else:
|
||||
try:
|
||||
await cancel_shielded_checkpoint()
|
||||
except BaseException:
|
||||
self.release()
|
||||
raise
|
||||
|
||||
def acquire_nowait(self) -> None:
|
||||
"""
|
||||
Acquire the lock, without blocking.
|
||||
|
||||
:raises ~anyio.WouldBlock: if the operation would block
|
||||
|
||||
"""
|
||||
task = get_current_task()
|
||||
if self._owner_task == task:
|
||||
raise RuntimeError("Attempted to acquire an already held Lock")
|
||||
|
||||
if self._owner_task is not None:
|
||||
raise WouldBlock
|
||||
|
||||
self._owner_task = task
|
||||
|
||||
def release(self) -> DeprecatedAwaitable:
|
||||
"""Release the lock."""
|
||||
if self._owner_task != get_current_task():
|
||||
raise RuntimeError("The current task is not holding this lock")
|
||||
|
||||
if self._waiters:
|
||||
self._owner_task, event = self._waiters.popleft()
|
||||
event.set()
|
||||
else:
|
||||
del self._owner_task
|
||||
|
||||
return DeprecatedAwaitable(self.release)
|
||||
|
||||
def locked(self) -> bool:
|
||||
"""Return True if the lock is currently held."""
|
||||
return self._owner_task is not None
|
||||
|
||||
def statistics(self) -> LockStatistics:
|
||||
"""
|
||||
Return statistics about the current state of this lock.
|
||||
|
||||
.. versionadded:: 3.0
|
||||
"""
|
||||
return LockStatistics(self.locked(), self._owner_task, len(self._waiters))
|
||||
|
||||
|
||||
class Condition:
|
||||
_owner_task: TaskInfo | None = None
|
||||
|
||||
def __init__(self, lock: Lock | None = None):
|
||||
self._lock = lock or Lock()
|
||||
self._waiters: deque[Event] = deque()
|
||||
|
||||
async def __aenter__(self) -> None:
|
||||
await self.acquire()
|
||||
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> None:
|
||||
self.release()
|
||||
|
||||
def _check_acquired(self) -> None:
|
||||
if self._owner_task != get_current_task():
|
||||
raise RuntimeError("The current task is not holding the underlying lock")
|
||||
|
||||
async def acquire(self) -> None:
|
||||
"""Acquire the underlying lock."""
|
||||
await self._lock.acquire()
|
||||
self._owner_task = get_current_task()
|
||||
|
||||
def acquire_nowait(self) -> None:
|
||||
"""
|
||||
Acquire the underlying lock, without blocking.
|
||||
|
||||
:raises ~anyio.WouldBlock: if the operation would block
|
||||
|
||||
"""
|
||||
self._lock.acquire_nowait()
|
||||
self._owner_task = get_current_task()
|
||||
|
||||
def release(self) -> DeprecatedAwaitable:
|
||||
"""Release the underlying lock."""
|
||||
self._lock.release()
|
||||
return DeprecatedAwaitable(self.release)
|
||||
|
||||
def locked(self) -> bool:
|
||||
"""Return True if the lock is set."""
|
||||
return self._lock.locked()
|
||||
|
||||
def notify(self, n: int = 1) -> None:
|
||||
"""Notify exactly n listeners."""
|
||||
self._check_acquired()
|
||||
for _ in range(n):
|
||||
try:
|
||||
event = self._waiters.popleft()
|
||||
except IndexError:
|
||||
break
|
||||
|
||||
event.set()
|
||||
|
||||
def notify_all(self) -> None:
|
||||
"""Notify all the listeners."""
|
||||
self._check_acquired()
|
||||
for event in self._waiters:
|
||||
event.set()
|
||||
|
||||
self._waiters.clear()
|
||||
|
||||
async def wait(self) -> None:
|
||||
"""Wait for a notification."""
|
||||
await checkpoint()
|
||||
event = Event()
|
||||
self._waiters.append(event)
|
||||
self.release()
|
||||
try:
|
||||
await event.wait()
|
||||
except BaseException:
|
||||
if not event.is_set():
|
||||
self._waiters.remove(event)
|
||||
|
||||
raise
|
||||
finally:
|
||||
with CancelScope(shield=True):
|
||||
await self.acquire()
|
||||
|
||||
def statistics(self) -> ConditionStatistics:
|
||||
"""
|
||||
Return statistics about the current state of this condition.
|
||||
|
||||
.. versionadded:: 3.0
|
||||
"""
|
||||
return ConditionStatistics(len(self._waiters), self._lock.statistics())
|
||||
|
||||
|
||||
class Semaphore:
|
||||
def __init__(self, initial_value: int, *, max_value: int | None = None):
|
||||
if not isinstance(initial_value, int):
|
||||
raise TypeError("initial_value must be an integer")
|
||||
if initial_value < 0:
|
||||
raise ValueError("initial_value must be >= 0")
|
||||
if max_value is not None:
|
||||
if not isinstance(max_value, int):
|
||||
raise TypeError("max_value must be an integer or None")
|
||||
if max_value < initial_value:
|
||||
raise ValueError(
|
||||
"max_value must be equal to or higher than initial_value"
|
||||
)
|
||||
|
||||
self._value = initial_value
|
||||
self._max_value = max_value
|
||||
self._waiters: deque[Event] = deque()
|
||||
|
||||
async def __aenter__(self) -> Semaphore:
|
||||
await self.acquire()
|
||||
return self
|
||||
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> None:
|
||||
self.release()
|
||||
|
||||
async def acquire(self) -> None:
|
||||
"""Decrement the semaphore value, blocking if necessary."""
|
||||
await checkpoint_if_cancelled()
|
||||
try:
|
||||
self.acquire_nowait()
|
||||
except WouldBlock:
|
||||
event = Event()
|
||||
self._waiters.append(event)
|
||||
try:
|
||||
await event.wait()
|
||||
except BaseException:
|
||||
if not event.is_set():
|
||||
self._waiters.remove(event)
|
||||
else:
|
||||
self.release()
|
||||
|
||||
raise
|
||||
else:
|
||||
try:
|
||||
await cancel_shielded_checkpoint()
|
||||
except BaseException:
|
||||
self.release()
|
||||
raise
|
||||
|
||||
def acquire_nowait(self) -> None:
|
||||
"""
|
||||
Acquire the underlying lock, without blocking.
|
||||
|
||||
:raises ~anyio.WouldBlock: if the operation would block
|
||||
|
||||
"""
|
||||
if self._value == 0:
|
||||
raise WouldBlock
|
||||
|
||||
self._value -= 1
|
||||
|
||||
def release(self) -> DeprecatedAwaitable:
|
||||
"""Increment the semaphore value."""
|
||||
if self._max_value is not None and self._value == self._max_value:
|
||||
raise ValueError("semaphore released too many times")
|
||||
|
||||
if self._waiters:
|
||||
self._waiters.popleft().set()
|
||||
else:
|
||||
self._value += 1
|
||||
|
||||
return DeprecatedAwaitable(self.release)
|
||||
|
||||
@property
|
||||
def value(self) -> int:
|
||||
"""The current value of the semaphore."""
|
||||
return self._value
|
||||
|
||||
@property
|
||||
def max_value(self) -> int | None:
|
||||
"""The maximum value of the semaphore."""
|
||||
return self._max_value
|
||||
|
||||
def statistics(self) -> SemaphoreStatistics:
|
||||
"""
|
||||
Return statistics about the current state of this semaphore.
|
||||
|
||||
.. versionadded:: 3.0
|
||||
"""
|
||||
return SemaphoreStatistics(len(self._waiters))
|
||||
|
||||
|
||||
class CapacityLimiter:
|
||||
def __new__(cls, total_tokens: float) -> CapacityLimiter:
|
||||
return get_asynclib().CapacityLimiter(total_tokens)
|
||||
|
||||
async def __aenter__(self) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> bool | None:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def total_tokens(self) -> float:
|
||||
"""
|
||||
The total number of tokens available for borrowing.
|
||||
|
||||
This is a read-write property. If the total number of tokens is increased, the
|
||||
proportionate number of tasks waiting on this limiter will be granted their tokens.
|
||||
|
||||
.. versionchanged:: 3.0
|
||||
The property is now writable.
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@total_tokens.setter
|
||||
def total_tokens(self, value: float) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
async def set_total_tokens(self, value: float) -> None:
|
||||
warn(
|
||||
"CapacityLimiter.set_total_tokens has been deprecated. Set the value of the"
|
||||
'"total_tokens" attribute directly.',
|
||||
DeprecationWarning,
|
||||
)
|
||||
self.total_tokens = value
|
||||
|
||||
@property
|
||||
def borrowed_tokens(self) -> int:
|
||||
"""The number of tokens that have currently been borrowed."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def available_tokens(self) -> float:
|
||||
"""The number of tokens currently available to be borrowed"""
|
||||
raise NotImplementedError
|
||||
|
||||
def acquire_nowait(self) -> DeprecatedAwaitable:
|
||||
"""
|
||||
Acquire a token for the current task without waiting for one to become available.
|
||||
|
||||
:raises ~anyio.WouldBlock: if there are no tokens available for borrowing
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def acquire_on_behalf_of_nowait(self, borrower: object) -> DeprecatedAwaitable:
|
||||
"""
|
||||
Acquire a token without waiting for one to become available.
|
||||
|
||||
:param borrower: the entity borrowing a token
|
||||
:raises ~anyio.WouldBlock: if there are no tokens available for borrowing
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
async def acquire(self) -> None:
|
||||
"""
|
||||
Acquire a token for the current task, waiting if necessary for one to become available.
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
async def acquire_on_behalf_of(self, borrower: object) -> None:
|
||||
"""
|
||||
Acquire a token, waiting if necessary for one to become available.
|
||||
|
||||
:param borrower: the entity borrowing a token
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def release(self) -> None:
|
||||
"""
|
||||
Release the token held by the current task.
|
||||
:raises RuntimeError: if the current task has not borrowed a token from this limiter.
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def release_on_behalf_of(self, borrower: object) -> None:
|
||||
"""
|
||||
Release the token held by the given borrower.
|
||||
|
||||
:raises RuntimeError: if the borrower has not borrowed a token from this limiter.
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def statistics(self) -> CapacityLimiterStatistics:
|
||||
"""
|
||||
Return statistics about the current state of this limiter.
|
||||
|
||||
.. versionadded:: 3.0
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def create_lock() -> Lock:
|
||||
"""
|
||||
Create an asynchronous lock.
|
||||
|
||||
:return: a lock object
|
||||
|
||||
.. deprecated:: 3.0
|
||||
Use :class:`~Lock` directly.
|
||||
|
||||
"""
|
||||
warn("create_lock() is deprecated -- use Lock() directly", DeprecationWarning)
|
||||
return Lock()
|
||||
|
||||
|
||||
def create_condition(lock: Lock | None = None) -> Condition:
|
||||
"""
|
||||
Create an asynchronous condition.
|
||||
|
||||
:param lock: the lock to base the condition object on
|
||||
:return: a condition object
|
||||
|
||||
.. deprecated:: 3.0
|
||||
Use :class:`~Condition` directly.
|
||||
|
||||
"""
|
||||
warn(
|
||||
"create_condition() is deprecated -- use Condition() directly",
|
||||
DeprecationWarning,
|
||||
)
|
||||
return Condition(lock=lock)
|
||||
|
||||
|
||||
def create_event() -> Event:
|
||||
"""
|
||||
Create an asynchronous event object.
|
||||
|
||||
:return: an event object
|
||||
|
||||
.. deprecated:: 3.0
|
||||
Use :class:`~Event` directly.
|
||||
|
||||
"""
|
||||
warn("create_event() is deprecated -- use Event() directly", DeprecationWarning)
|
||||
return get_asynclib().Event()
|
||||
|
||||
|
||||
def create_semaphore(value: int, *, max_value: int | None = None) -> Semaphore:
|
||||
"""
|
||||
Create an asynchronous semaphore.
|
||||
|
||||
:param value: the semaphore's initial value
|
||||
:param max_value: if set, makes this a "bounded" semaphore that raises :exc:`ValueError` if the
|
||||
semaphore's value would exceed this number
|
||||
:return: a semaphore object
|
||||
|
||||
.. deprecated:: 3.0
|
||||
Use :class:`~Semaphore` directly.
|
||||
|
||||
"""
|
||||
warn(
|
||||
"create_semaphore() is deprecated -- use Semaphore() directly",
|
||||
DeprecationWarning,
|
||||
)
|
||||
return Semaphore(value, max_value=max_value)
|
||||
|
||||
|
||||
def create_capacity_limiter(total_tokens: float) -> CapacityLimiter:
|
||||
"""
|
||||
Create a capacity limiter.
|
||||
|
||||
:param total_tokens: the total number of tokens available for borrowing (can be an integer or
|
||||
:data:`math.inf`)
|
||||
:return: a capacity limiter object
|
||||
|
||||
.. deprecated:: 3.0
|
||||
Use :class:`~CapacityLimiter` directly.
|
||||
|
||||
"""
|
||||
warn(
|
||||
"create_capacity_limiter() is deprecated -- use CapacityLimiter() directly",
|
||||
DeprecationWarning,
|
||||
)
|
||||
return get_asynclib().CapacityLimiter(total_tokens)
|
||||
|
||||
|
||||
class ResourceGuard:
|
||||
__slots__ = "action", "_guarded"
|
||||
|
||||
def __init__(self, action: str):
|
||||
self.action = action
|
||||
self._guarded = False
|
||||
|
||||
def __enter__(self) -> None:
|
||||
if self._guarded:
|
||||
raise BusyResourceError(self.action)
|
||||
|
||||
self._guarded = True
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> bool | None:
|
||||
self._guarded = False
|
||||
return None
|
||||
180
jokes_bot/venv/lib/python3.9/site-packages/anyio/_core/_tasks.py
Normal file
180
jokes_bot/venv/lib/python3.9/site-packages/anyio/_core/_tasks.py
Normal file
@@ -0,0 +1,180 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from types import TracebackType
|
||||
from warnings import warn
|
||||
|
||||
from ..abc._tasks import TaskGroup, TaskStatus
|
||||
from ._compat import (
|
||||
DeprecatedAsyncContextManager,
|
||||
DeprecatedAwaitable,
|
||||
DeprecatedAwaitableFloat,
|
||||
)
|
||||
from ._eventloop import get_asynclib
|
||||
|
||||
|
||||
class _IgnoredTaskStatus(TaskStatus[object]):
|
||||
def started(self, value: object = None) -> None:
|
||||
pass
|
||||
|
||||
|
||||
TASK_STATUS_IGNORED = _IgnoredTaskStatus()
|
||||
|
||||
|
||||
class CancelScope(DeprecatedAsyncContextManager["CancelScope"]):
|
||||
"""
|
||||
Wraps a unit of work that can be made separately cancellable.
|
||||
|
||||
:param deadline: The time (clock value) when this scope is cancelled automatically
|
||||
:param shield: ``True`` to shield the cancel scope from external cancellation
|
||||
"""
|
||||
|
||||
def __new__(
|
||||
cls, *, deadline: float = math.inf, shield: bool = False
|
||||
) -> CancelScope:
|
||||
return get_asynclib().CancelScope(shield=shield, deadline=deadline)
|
||||
|
||||
def cancel(self) -> DeprecatedAwaitable:
|
||||
"""Cancel this scope immediately."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def deadline(self) -> float:
|
||||
"""
|
||||
The time (clock value) when this scope is cancelled automatically.
|
||||
|
||||
Will be ``float('inf')`` if no timeout has been set.
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@deadline.setter
|
||||
def deadline(self, value: float) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def cancel_called(self) -> bool:
|
||||
"""``True`` if :meth:`cancel` has been called."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def shield(self) -> bool:
|
||||
"""
|
||||
``True`` if this scope is shielded from external cancellation.
|
||||
|
||||
While a scope is shielded, it will not receive cancellations from outside.
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@shield.setter
|
||||
def shield(self, value: bool) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def __enter__(self) -> CancelScope:
|
||||
raise NotImplementedError
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> bool | None:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def open_cancel_scope(*, shield: bool = False) -> CancelScope:
|
||||
"""
|
||||
Open a cancel scope.
|
||||
|
||||
:param shield: ``True`` to shield the cancel scope from external cancellation
|
||||
:return: a cancel scope
|
||||
|
||||
.. deprecated:: 3.0
|
||||
Use :class:`~CancelScope` directly.
|
||||
|
||||
"""
|
||||
warn(
|
||||
"open_cancel_scope() is deprecated -- use CancelScope() directly",
|
||||
DeprecationWarning,
|
||||
)
|
||||
return get_asynclib().CancelScope(shield=shield)
|
||||
|
||||
|
||||
class FailAfterContextManager(DeprecatedAsyncContextManager[CancelScope]):
|
||||
def __init__(self, cancel_scope: CancelScope):
|
||||
self._cancel_scope = cancel_scope
|
||||
|
||||
def __enter__(self) -> CancelScope:
|
||||
return self._cancel_scope.__enter__()
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> bool | None:
|
||||
retval = self._cancel_scope.__exit__(exc_type, exc_val, exc_tb)
|
||||
if self._cancel_scope.cancel_called:
|
||||
raise TimeoutError
|
||||
|
||||
return retval
|
||||
|
||||
|
||||
def fail_after(delay: float | None, shield: bool = False) -> FailAfterContextManager:
|
||||
"""
|
||||
Create a context manager which raises a :class:`TimeoutError` if does not finish in time.
|
||||
|
||||
:param delay: maximum allowed time (in seconds) before raising the exception, or ``None`` to
|
||||
disable the timeout
|
||||
:param shield: ``True`` to shield the cancel scope from external cancellation
|
||||
:return: a context manager that yields a cancel scope
|
||||
:rtype: :class:`~typing.ContextManager`\\[:class:`~anyio.CancelScope`\\]
|
||||
|
||||
"""
|
||||
deadline = (
|
||||
(get_asynclib().current_time() + delay) if delay is not None else math.inf
|
||||
)
|
||||
cancel_scope = get_asynclib().CancelScope(deadline=deadline, shield=shield)
|
||||
return FailAfterContextManager(cancel_scope)
|
||||
|
||||
|
||||
def move_on_after(delay: float | None, shield: bool = False) -> CancelScope:
|
||||
"""
|
||||
Create a cancel scope with a deadline that expires after the given delay.
|
||||
|
||||
:param delay: maximum allowed time (in seconds) before exiting the context block, or ``None``
|
||||
to disable the timeout
|
||||
:param shield: ``True`` to shield the cancel scope from external cancellation
|
||||
:return: a cancel scope
|
||||
|
||||
"""
|
||||
deadline = (
|
||||
(get_asynclib().current_time() + delay) if delay is not None else math.inf
|
||||
)
|
||||
return get_asynclib().CancelScope(deadline=deadline, shield=shield)
|
||||
|
||||
|
||||
def current_effective_deadline() -> DeprecatedAwaitableFloat:
|
||||
"""
|
||||
Return the nearest deadline among all the cancel scopes effective for the current task.
|
||||
|
||||
:return: a clock value from the event loop's internal clock (or ``float('inf')`` if
|
||||
there is no deadline in effect, or ``float('-inf')`` if the current scope has
|
||||
been cancelled)
|
||||
:rtype: float
|
||||
|
||||
"""
|
||||
return DeprecatedAwaitableFloat(
|
||||
get_asynclib().current_effective_deadline(), current_effective_deadline
|
||||
)
|
||||
|
||||
|
||||
def create_task_group() -> TaskGroup:
|
||||
"""
|
||||
Create a task group.
|
||||
|
||||
:return: a task group
|
||||
|
||||
"""
|
||||
return get_asynclib().TaskGroup()
|
||||
@@ -0,0 +1,82 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Awaitable, Generator
|
||||
|
||||
from ._compat import DeprecatedAwaitableList, _warn_deprecation
|
||||
from ._eventloop import get_asynclib
|
||||
|
||||
|
||||
class TaskInfo:
|
||||
"""
|
||||
Represents an asynchronous task.
|
||||
|
||||
:ivar int id: the unique identifier of the task
|
||||
:ivar parent_id: the identifier of the parent task, if any
|
||||
:vartype parent_id: Optional[int]
|
||||
:ivar str name: the description of the task (if any)
|
||||
:ivar ~collections.abc.Coroutine coro: the coroutine object of the task
|
||||
"""
|
||||
|
||||
__slots__ = "_name", "id", "parent_id", "name", "coro"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
id: int,
|
||||
parent_id: int | None,
|
||||
name: str | None,
|
||||
coro: Generator[Any, Any, Any] | Awaitable[Any],
|
||||
):
|
||||
func = get_current_task
|
||||
self._name = f"{func.__module__}.{func.__qualname__}"
|
||||
self.id: int = id
|
||||
self.parent_id: int | None = parent_id
|
||||
self.name: str | None = name
|
||||
self.coro: Generator[Any, Any, Any] | Awaitable[Any] = coro
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if isinstance(other, TaskInfo):
|
||||
return self.id == other.id
|
||||
|
||||
return NotImplemented
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.id)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}(id={self.id!r}, name={self.name!r})"
|
||||
|
||||
def __await__(self) -> Generator[None, None, TaskInfo]:
|
||||
_warn_deprecation(self)
|
||||
if False:
|
||||
yield
|
||||
|
||||
return self
|
||||
|
||||
def _unwrap(self) -> TaskInfo:
|
||||
return self
|
||||
|
||||
|
||||
def get_current_task() -> TaskInfo:
|
||||
"""
|
||||
Return the current task.
|
||||
|
||||
:return: a representation of the current task
|
||||
|
||||
"""
|
||||
return get_asynclib().get_current_task()
|
||||
|
||||
|
||||
def get_running_tasks() -> DeprecatedAwaitableList[TaskInfo]:
|
||||
"""
|
||||
Return a list of running tasks in the current event loop.
|
||||
|
||||
:return: a list of task info objects
|
||||
|
||||
"""
|
||||
tasks = get_asynclib().get_running_tasks()
|
||||
return DeprecatedAwaitableList(tasks, func=get_running_tasks)
|
||||
|
||||
|
||||
async def wait_all_tasks_blocked() -> None:
|
||||
"""Wait until all other tasks are waiting for something."""
|
||||
await get_asynclib().wait_all_tasks_blocked()
|
||||
@@ -0,0 +1,83 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from typing import Any, Callable, Mapping, TypeVar, overload
|
||||
|
||||
from ._exceptions import TypedAttributeLookupError
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
from typing import final
|
||||
else:
|
||||
from typing_extensions import final
|
||||
|
||||
T_Attr = TypeVar("T_Attr")
|
||||
T_Default = TypeVar("T_Default")
|
||||
undefined = object()
|
||||
|
||||
|
||||
def typed_attribute() -> Any:
|
||||
"""Return a unique object, used to mark typed attributes."""
|
||||
return object()
|
||||
|
||||
|
||||
class TypedAttributeSet:
|
||||
"""
|
||||
Superclass for typed attribute collections.
|
||||
|
||||
Checks that every public attribute of every subclass has a type annotation.
|
||||
"""
|
||||
|
||||
def __init_subclass__(cls) -> None:
|
||||
annotations: dict[str, Any] = getattr(cls, "__annotations__", {})
|
||||
for attrname in dir(cls):
|
||||
if not attrname.startswith("_") and attrname not in annotations:
|
||||
raise TypeError(
|
||||
f"Attribute {attrname!r} is missing its type annotation"
|
||||
)
|
||||
|
||||
super().__init_subclass__()
|
||||
|
||||
|
||||
class TypedAttributeProvider:
|
||||
"""Base class for classes that wish to provide typed extra attributes."""
|
||||
|
||||
@property
|
||||
def extra_attributes(self) -> Mapping[T_Attr, Callable[[], T_Attr]]:
|
||||
"""
|
||||
A mapping of the extra attributes to callables that return the corresponding values.
|
||||
|
||||
If the provider wraps another provider, the attributes from that wrapper should also be
|
||||
included in the returned mapping (but the wrapper may override the callables from the
|
||||
wrapped instance).
|
||||
|
||||
"""
|
||||
return {}
|
||||
|
||||
@overload
|
||||
def extra(self, attribute: T_Attr) -> T_Attr:
|
||||
...
|
||||
|
||||
@overload
|
||||
def extra(self, attribute: T_Attr, default: T_Default) -> T_Attr | T_Default:
|
||||
...
|
||||
|
||||
@final
|
||||
def extra(self, attribute: Any, default: object = undefined) -> object:
|
||||
"""
|
||||
extra(attribute, default=undefined)
|
||||
|
||||
Return the value of the given typed extra attribute.
|
||||
|
||||
:param attribute: the attribute (member of a :class:`~TypedAttributeSet`) to look for
|
||||
:param default: the value that should be returned if no value is found for the attribute
|
||||
:raises ~anyio.TypedAttributeLookupError: if the search failed and no default value was
|
||||
given
|
||||
|
||||
"""
|
||||
try:
|
||||
return self.extra_attributes[attribute]()
|
||||
except KeyError:
|
||||
if default is undefined:
|
||||
raise TypedAttributeLookupError("Attribute not found") from None
|
||||
else:
|
||||
return default
|
||||
@@ -0,0 +1,90 @@
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = (
|
||||
"AsyncResource",
|
||||
"IPAddressType",
|
||||
"IPSockAddrType",
|
||||
"SocketAttribute",
|
||||
"SocketStream",
|
||||
"SocketListener",
|
||||
"UDPSocket",
|
||||
"UNIXSocketStream",
|
||||
"UDPPacketType",
|
||||
"ConnectedUDPSocket",
|
||||
"UnreliableObjectReceiveStream",
|
||||
"UnreliableObjectSendStream",
|
||||
"UnreliableObjectStream",
|
||||
"ObjectReceiveStream",
|
||||
"ObjectSendStream",
|
||||
"ObjectStream",
|
||||
"ByteReceiveStream",
|
||||
"ByteSendStream",
|
||||
"ByteStream",
|
||||
"AnyUnreliableByteReceiveStream",
|
||||
"AnyUnreliableByteSendStream",
|
||||
"AnyUnreliableByteStream",
|
||||
"AnyByteReceiveStream",
|
||||
"AnyByteSendStream",
|
||||
"AnyByteStream",
|
||||
"Listener",
|
||||
"Process",
|
||||
"Event",
|
||||
"Condition",
|
||||
"Lock",
|
||||
"Semaphore",
|
||||
"CapacityLimiter",
|
||||
"CancelScope",
|
||||
"TaskGroup",
|
||||
"TaskStatus",
|
||||
"TestRunner",
|
||||
"BlockingPortal",
|
||||
)
|
||||
|
||||
from typing import Any
|
||||
|
||||
from ._resources import AsyncResource
|
||||
from ._sockets import (
|
||||
ConnectedUDPSocket,
|
||||
IPAddressType,
|
||||
IPSockAddrType,
|
||||
SocketAttribute,
|
||||
SocketListener,
|
||||
SocketStream,
|
||||
UDPPacketType,
|
||||
UDPSocket,
|
||||
UNIXSocketStream,
|
||||
)
|
||||
from ._streams import (
|
||||
AnyByteReceiveStream,
|
||||
AnyByteSendStream,
|
||||
AnyByteStream,
|
||||
AnyUnreliableByteReceiveStream,
|
||||
AnyUnreliableByteSendStream,
|
||||
AnyUnreliableByteStream,
|
||||
ByteReceiveStream,
|
||||
ByteSendStream,
|
||||
ByteStream,
|
||||
Listener,
|
||||
ObjectReceiveStream,
|
||||
ObjectSendStream,
|
||||
ObjectStream,
|
||||
UnreliableObjectReceiveStream,
|
||||
UnreliableObjectSendStream,
|
||||
UnreliableObjectStream,
|
||||
)
|
||||
from ._subprocesses import Process
|
||||
from ._tasks import TaskGroup, TaskStatus
|
||||
from ._testing import TestRunner
|
||||
|
||||
# Re-exported here, for backwards compatibility
|
||||
# isort: off
|
||||
from .._core._synchronization import CapacityLimiter, Condition, Event, Lock, Semaphore
|
||||
from .._core._tasks import CancelScope
|
||||
from ..from_thread import BlockingPortal
|
||||
|
||||
# Re-export imports so they look like they live directly in this package
|
||||
key: str
|
||||
value: Any
|
||||
for key, value in list(locals().items()):
|
||||
if getattr(value, "__module__", "").startswith("anyio.abc."):
|
||||
value.__module__ = __name__
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user