Annoucement Web -Depreciated
@@ -0,0 +1,15 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"name": "Launch Chrome against localhost",
|
||||
"url": "http://localhost:8080",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
import os
|
||||
import json
|
||||
import re
|
||||
import random
|
||||
import time # for generating a simple unique id
|
||||
from datetime import datetime, timezone
|
||||
from flask import Flask, render_template, request, url_for, jsonify
|
||||
from flask_socketio import SocketIO
|
||||
from flask_cors import CORS
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
app = Flask(__name__)
|
||||
# Adjust SERVER_NAME and SQLALCHEMY_DATABASE_URI as needed.
|
||||
app.config['SERVER_NAME'] = '10.30.0.2:5000'
|
||||
app.config['PREFERRED_URL_SCHEME'] = 'http'
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////Users/mediarogc/Documents/Annoucements Web/bible.db'
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
|
||||
CORS(app)
|
||||
socketio = SocketIO(app, cors_allowed_origins="*")
|
||||
db = SQLAlchemy(app)
|
||||
|
||||
# ---------------------------
|
||||
# Global configuration & Folders
|
||||
# ---------------------------
|
||||
# Bible verse linking and frequency features have been removed.
|
||||
STATUS_FILE = 'slides_status.json'
|
||||
SLIDES_FOLDER = os.path.join('static', 'slides')
|
||||
ALLOWED_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif'}
|
||||
|
||||
# Folder for Bible backgrounds (used elsewhere).
|
||||
BIBLE_BACKGROUNDS_FOLDER = os.path.join('static', 'bible_backgrounds')
|
||||
ALLOWED_BG_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif'}
|
||||
|
||||
# Bible JSON file for added Bible verses (will store verse text and reference)
|
||||
BIBLE_JSON_FILE = 'bible.json'
|
||||
|
||||
def get_slides():
|
||||
if not os.path.exists(SLIDES_FOLDER):
|
||||
os.makedirs(SLIDES_FOLDER)
|
||||
slides = []
|
||||
for filename in os.listdir(SLIDES_FOLDER):
|
||||
ext = os.path.splitext(filename)[1].lower()
|
||||
if ext in ALLOWED_EXTENSIONS:
|
||||
slides.append(filename)
|
||||
return slides
|
||||
|
||||
def load_slide_statuses():
|
||||
if os.path.exists(STATUS_FILE):
|
||||
with open(STATUS_FILE, 'r') as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
|
||||
def save_slide_statuses(statuses):
|
||||
with open(STATUS_FILE, 'w') as f:
|
||||
json.dump(statuses, f)
|
||||
|
||||
def get_slide_statuses():
|
||||
statuses = load_slide_statuses()
|
||||
slides = get_slides()
|
||||
new_statuses = {}
|
||||
max_order = max((data.get("order", 0) for data in statuses.values()), default=0)
|
||||
for slide in slides:
|
||||
if slide in statuses:
|
||||
new_statuses[slide] = statuses[slide]
|
||||
else:
|
||||
max_order += 1
|
||||
new_statuses[slide] = {
|
||||
"active": True,
|
||||
"order": max_order,
|
||||
"duration": 7,
|
||||
"title": slide.split('.')[0]
|
||||
}
|
||||
save_slide_statuses(new_statuses)
|
||||
return new_statuses
|
||||
|
||||
# ---------------------------
|
||||
# Announcement Endpoints
|
||||
# ---------------------------
|
||||
@app.route('/')
|
||||
def index():
|
||||
statuses = get_slide_statuses()
|
||||
sorted_slides = sorted(statuses.items(), key=lambda x: x[1]['order'])
|
||||
active_slides = [slide for slide, data in sorted_slides if data.get("active", False)]
|
||||
slide_urls = [url_for('static', filename='slides/' + slide) for slide in active_slides]
|
||||
return render_template('index.html', slide_urls=slide_urls)
|
||||
|
||||
@app.route('/control')
|
||||
def control():
|
||||
statuses = get_slide_statuses()
|
||||
slide_urls = {slide: url_for('static', filename='slides/' + slide) for slide in statuses}
|
||||
sorted_statuses = sorted(statuses.items(), key=lambda item: item[1]["order"])
|
||||
return render_template('control.html', statuses=sorted_statuses, slide_urls=slide_urls)
|
||||
|
||||
@app.route('/update_order', methods=['POST'])
|
||||
def update_order():
|
||||
statuses = get_slide_statuses()
|
||||
data = request.get_json()
|
||||
order_list = data.get("order", [])
|
||||
counter = 1
|
||||
for slide in order_list:
|
||||
if slide in statuses:
|
||||
statuses[slide]["order"] = counter
|
||||
counter += 1
|
||||
remaining = [slide for slide in statuses if slide not in order_list]
|
||||
remaining.sort(key=lambda s: statuses[s]["order"])
|
||||
for slide in remaining:
|
||||
statuses[slide]["order"] = counter
|
||||
counter += 1
|
||||
save_slide_statuses(statuses)
|
||||
socketio.emit('refresh_index')
|
||||
return jsonify({"status": "success"})
|
||||
|
||||
@app.route('/update_active', methods=['POST'])
|
||||
def update_active():
|
||||
statuses = get_slide_statuses()
|
||||
data = request.get_json()
|
||||
slide = data.get("slide")
|
||||
active = data.get("active")
|
||||
if slide in statuses:
|
||||
statuses[slide]["active"] = active
|
||||
save_slide_statuses(statuses)
|
||||
return jsonify({"status": "success"})
|
||||
return jsonify({"status": "error", "message": "Slide not found."}), 400
|
||||
|
||||
@app.route('/update_duration', methods=['POST'])
|
||||
def update_duration():
|
||||
statuses = get_slide_statuses()
|
||||
data = request.get_json()
|
||||
slide = data.get("slide")
|
||||
duration = data.get("duration")
|
||||
if slide in statuses:
|
||||
try:
|
||||
duration_val = float(duration)
|
||||
except ValueError:
|
||||
return jsonify({"status": "error", "message": "Invalid duration."}), 400
|
||||
statuses[slide]["duration"] = duration_val
|
||||
save_slide_statuses(statuses)
|
||||
return jsonify({"status": "success"})
|
||||
return jsonify({"status": "error", "message": "Slide not found."}), 400
|
||||
|
||||
@app.route('/update_title', methods=['POST'])
|
||||
def update_title():
|
||||
statuses = get_slide_statuses()
|
||||
data = request.get_json()
|
||||
slide = data.get("slide")
|
||||
title = data.get("title")
|
||||
if slide in statuses:
|
||||
statuses[slide]["title"] = title
|
||||
save_slide_statuses(statuses)
|
||||
return jsonify({"status": "success"})
|
||||
return jsonify({"status": "error", "message": "Slide not found."}), 400
|
||||
|
||||
@app.route('/update_end_date', methods=['POST'])
|
||||
def update_end_date():
|
||||
statuses = get_slide_statuses()
|
||||
data = request.get_json()
|
||||
slide = data.get("slide")
|
||||
end_date = data.get("end_date")
|
||||
if slide in statuses:
|
||||
statuses[slide]["end_date"] = end_date
|
||||
save_slide_statuses(statuses)
|
||||
return jsonify({"status": "success"})
|
||||
return jsonify({"status": "error", "message": "Slide not found."}), 400
|
||||
|
||||
@app.route('/delete_slide', methods=['POST'])
|
||||
def delete_slide():
|
||||
statuses = get_slide_statuses()
|
||||
data = request.get_json()
|
||||
slide = data.get("slide")
|
||||
if slide in statuses:
|
||||
del statuses[slide]
|
||||
save_slide_statuses(statuses)
|
||||
file_path = os.path.join(SLIDES_FOLDER, slide)
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
return jsonify({"status": "success"})
|
||||
return jsonify({"status": "error", "message": "Slide not found."}), 400
|
||||
|
||||
# ---------------------------
|
||||
# Bible Verses Integration (Library and Addition to Announcements)
|
||||
# ---------------------------
|
||||
class BibleVerse(db.Model):
|
||||
__tablename__ = 'bible_verse'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
book = db.Column(db.String(64), nullable=False)
|
||||
chapter = db.Column(db.Integer, nullable=False)
|
||||
verse = db.Column(db.Integer, nullable=False)
|
||||
text = db.Column(db.Text, nullable=False)
|
||||
translation = db.Column(db.String(64), nullable=False, default='KJV')
|
||||
# Removed color and background_url since background handling is via bible.json
|
||||
active_in_rotation = db.Column(db.Boolean, default=False)
|
||||
|
||||
@app.route('/search_verses')
|
||||
def search_verses():
|
||||
query = request.args.get('q', '').strip()
|
||||
translation = request.args.get('translation', None)
|
||||
pattern = r'^([A-Za-z0-9\s]+)\s+(\d+):(\d+)$'
|
||||
if re.match(pattern, query):
|
||||
match = re.match(pattern, query)
|
||||
book, chapter, verse = match.groups()
|
||||
chapter = int(chapter)
|
||||
verse = int(verse)
|
||||
q = BibleVerse.query.filter(
|
||||
BibleVerse.book.ilike(book.strip()),
|
||||
BibleVerse.chapter == chapter,
|
||||
BibleVerse.verse == verse
|
||||
)
|
||||
if translation:
|
||||
q = q.filter(BibleVerse.translation == translation)
|
||||
verses = q.all()
|
||||
else:
|
||||
if query == "":
|
||||
verses = BibleVerse.query.filter_by(active_in_rotation=True).all()
|
||||
else:
|
||||
q = BibleVerse.query.filter(BibleVerse.text.ilike(f'%{query}%'))
|
||||
if translation:
|
||||
q = q.filter(BibleVerse.translation == translation)
|
||||
verses = q.all()
|
||||
# Only return the required fields.
|
||||
results = [{
|
||||
'book': v.book,
|
||||
'chapter': v.chapter,
|
||||
'verse': v.verse,
|
||||
'text': v.text,
|
||||
'translation': v.translation
|
||||
} for v in verses]
|
||||
return jsonify(results)
|
||||
|
||||
def load_bible_json():
|
||||
if os.path.exists(BIBLE_JSON_FILE):
|
||||
with open(BIBLE_JSON_FILE, 'r') as f:
|
||||
try:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
print("Error loading bible.json:", e)
|
||||
return []
|
||||
return []
|
||||
|
||||
def save_bible_json(data):
|
||||
with open(BIBLE_JSON_FILE, 'w') as f:
|
||||
json.dump(data, f)
|
||||
|
||||
@app.route('/add_bible_verse', methods=['POST'])
|
||||
def add_bible_verse():
|
||||
"""
|
||||
Expects JSON payload:
|
||||
{
|
||||
"verse": "For God so loved the word that whomever believes in Him shall not perish but have everlasting life.",
|
||||
"reference": "John 3:16"
|
||||
}
|
||||
"""
|
||||
data = request.get_json()
|
||||
new_entry = {
|
||||
"id": int(time.time()), # Simple unique id generation; adjust if needed.
|
||||
"verse": data.get("verse"),
|
||||
"reference": data.get("reference"),
|
||||
"background": "" # By default, an empty background.
|
||||
}
|
||||
bible_data = load_bible_json()
|
||||
bible_data.append(new_entry)
|
||||
save_bible_json(bible_data)
|
||||
return jsonify({"status": "success", "entry": new_entry})
|
||||
|
||||
@app.route('/remove_bible_verse', methods=['POST'])
|
||||
def remove_bible_verse():
|
||||
"""
|
||||
Expects JSON payload:
|
||||
{ "id": 1234567890 }
|
||||
"""
|
||||
data = request.get_json()
|
||||
remove_id = data.get("id")
|
||||
bible_data = load_bible_json()
|
||||
# Filter out the entry with matching id
|
||||
new_data = [entry for entry in bible_data if entry.get("id") != remove_id]
|
||||
save_bible_json(new_data)
|
||||
return jsonify({"status": "success"})
|
||||
|
||||
@app.route('/bible_registry')
|
||||
def bible_registry():
|
||||
"""
|
||||
Returns the current list of added bible verses (from bible.json).
|
||||
"""
|
||||
data = load_bible_json()
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# Bible Background Helpers
|
||||
# ---------------------------
|
||||
def get_bible_background_images():
|
||||
if not os.path.exists(BIBLE_BACKGROUNDS_FOLDER):
|
||||
os.makedirs(BIBLE_BACKGROUNDS_FOLDER)
|
||||
images = []
|
||||
for filename in os.listdir(BIBLE_BACKGROUNDS_FOLDER):
|
||||
ext = os.path.splitext(filename)[1].lower()
|
||||
if ext in ALLOWED_BG_EXTENSIONS:
|
||||
images.append(filename)
|
||||
return images
|
||||
|
||||
def get_random_bible_background():
|
||||
imgs = get_bible_background_images()
|
||||
if imgs:
|
||||
bg = random.choice(imgs)
|
||||
return url_for('static', filename='bible_backgrounds/' + bg)
|
||||
return ""
|
||||
|
||||
# ---------------------------
|
||||
# Background Tasks
|
||||
# ---------------------------
|
||||
current_slide_index = 0
|
||||
|
||||
def background_slide_sync():
|
||||
global current_slide_index
|
||||
while True:
|
||||
with app.app_context():
|
||||
statuses = get_slide_statuses()
|
||||
sorted_slides = sorted(statuses.items(), key=lambda x: x[1]['order'])
|
||||
active_list = [(slide, data) for slide, data in sorted_slides if data.get("active", False)]
|
||||
if active_list:
|
||||
current_slide_index = (current_slide_index + 1) % len(active_list)
|
||||
slide, data = active_list[current_slide_index]
|
||||
slide_url = url_for('static', filename='slides/' + slide)
|
||||
socketio.emit('sync_slide', {"current_slide_index": current_slide_index, "slide_url": slide_url})
|
||||
duration = float(data.get("duration", 7))
|
||||
else:
|
||||
duration = 7
|
||||
socketio.sleep(duration)
|
||||
|
||||
@socketio.on('connect')
|
||||
def on_connect(auth=None):
|
||||
global current_slide_index
|
||||
with app.app_context():
|
||||
statuses = get_slide_statuses()
|
||||
sorted_slides = sorted(statuses.items(), key=lambda x: x[1]['order'])
|
||||
active_list = [(slide, data) for slide, data in sorted_slides if data.get("active", False)]
|
||||
if active_list:
|
||||
index = current_slide_index % len(active_list)
|
||||
slide, data = active_list[index]
|
||||
slide_url = url_for('static', filename='slides/' + slide)
|
||||
socketio.emit('sync_slide', {"current_slide_index": index, "slide_url": slide_url})
|
||||
|
||||
def scheduled_deletion_check():
|
||||
while True:
|
||||
with app.app_context():
|
||||
statuses = get_slide_statuses()
|
||||
to_delete = []
|
||||
now = datetime.now(timezone.utc)
|
||||
for slide, data in statuses.items():
|
||||
end_date_str = data.get("end_date")
|
||||
if end_date_str:
|
||||
try:
|
||||
end_date = datetime.fromisoformat(end_date_str)
|
||||
if end_date.tzinfo is None:
|
||||
end_date = end_date.replace(tzinfo=timezone.utc)
|
||||
if now >= end_date:
|
||||
to_delete.append(slide)
|
||||
except Exception as e:
|
||||
print(f"Error parsing end_date for {slide}: {e}")
|
||||
if to_delete:
|
||||
for slide in to_delete:
|
||||
del statuses[slide]
|
||||
file_path = os.path.join(SLIDES_FOLDER, slide)
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
save_slide_statuses(statuses)
|
||||
socketio.emit('refresh_index')
|
||||
socketio.sleep(60)
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Uncomment the following line on first run (or when models change)
|
||||
# db.create_all()
|
||||
socketio.start_background_task(background_slide_sync)
|
||||
socketio.start_background_task(scheduled_deletion_check)
|
||||
socketio.run(app, host='0.0.0.0', port=5000, debug=True, use_reloader=False)
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1 @@
|
||||
{"Are You New.png": {"active": false, "order": 2, "duration": 7.0, "title": "Are You New", "linked_verse": "26137"}, "Men's Bible Study.png": {"active": false, "order": 4, "duration": 7, "title": "Men's Bible Study"}, "Ways To Watch.png": {"active": true, "order": 5, "duration": 7, "title": "Ways To Watch", "linked_verse": "26137"}, "Master's Class.png": {"active": true, "order": 13, "duration": 7, "title": "Master's Class"}, "Mobile App.png": {"active": true, "order": 6, "duration": 7, "title": "Mobile App", "linked_verse": ""}, "Connect.png": {"active": true, "order": 7, "duration": 7, "title": "Connect"}, "Verse 2.png": {"active": false, "order": 3, "duration": 7, "title": "Verse 2"}, "Verse 3.png": {"active": false, "order": 16, "duration": 7, "title": "Verse 3"}, "Verse 1.png": {"active": true, "order": 9, "duration": 7, "title": "Verse 1"}, "Young Adults.png": {"active": false, "order": 11, "duration": 7, "title": "Young Adults"}, "Band Reeves 1920x1080.png": {"active": true, "order": 17, "duration": 7, "title": "Band Reeves 1920x1080"}, "Healing River Center.png": {"active": true, "order": 12, "duration": 7, "title": "Healing River Center"}, "Volunteers Needed.png": {"active": false, "order": 14, "duration": 7, "title": "Volunteers Needed"}, "Ways to Give.png": {"active": true, "order": 10, "duration": 7, "title": "Ways to Give", "linked_verse": "26137"}, "Welcome.png": {"active": false, "order": 1, "duration": 7, "title": "Welcome"}}
|
||||
|
After Width: | Height: | Size: 861 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 861 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
@@ -0,0 +1,51 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.slideshow-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.slideshow-container img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
transition: opacity 1s ease-in-out;
|
||||
opacity: 1;
|
||||
|
||||
|
||||
table {
|
||||
width: 80%;
|
||||
margin: 20px auto;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
table, th, td {
|
||||
border: 1px solid #fff;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* For larger screens (tablets, desktops) */
|
||||
@media (min-width: 768px) {
|
||||
.slideshow-container {
|
||||
height: 100vh; /* Full viewport height on desktops if needed */
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 2.9 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 581 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 2.8 MiB |
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 3.8 MiB |
|
After Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 3.6 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 3.5 MiB |
@@ -0,0 +1,311 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Control Panel - Church Announcements</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<!-- SortableJS & Flatpickr CSS -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.15.0/Sortable.min.js"></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
|
||||
<style>
|
||||
/* Basic styling */
|
||||
body { background: #222; color: #fff; font-family: sans-serif; margin: 0; padding: 0; }
|
||||
#pageTitle { text-align: center; margin: 1rem 0; font-family: 'Roboto', sans-serif; text-transform: uppercase; font-weight: bold; letter-spacing: 1px; font-size: 2rem; }
|
||||
.preview-container { max-width: 1000px; margin: 0 auto 1rem; text-align: center; }
|
||||
.preview-iframe { width: 50%; aspect-ratio: 16/9; border: 3px solid #fff; border-radius: 10px; box-shadow: 0 0 10px rgba(0,0,0,0.5); }
|
||||
.tab-nav { display: flex; justify-content: center; margin-bottom: 1rem; }
|
||||
.tab-nav button { background: #444; border: none; color: #fff; padding: .5rem 1rem; margin: 0 .5rem; cursor: pointer; font-size: 1rem; border-radius: 4px; transition: background .3s; }
|
||||
.tab-nav button:hover { background: #666; }
|
||||
.active-tab { background: #4caf50 !important; }
|
||||
#announcementsSection { display: block; }
|
||||
#bibleSection { display: none; max-width: 1000px; margin: 0 auto; padding: 1rem 0; }
|
||||
#bibleSection h2 { text-align: center; margin-bottom: 1rem; }
|
||||
|
||||
/* Slider-style toggle */
|
||||
#activeFilterContainer { text-align: right; padding: 0 1rem; }
|
||||
.toggle-label { position: relative; display: inline-block; width: 60px; height: 34px; }
|
||||
.toggle-label input { opacity: 0; width: 0; height: 0; position: absolute; }
|
||||
.toggle-slider { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: #888; border-radius: 34px; transition: .4s; cursor: pointer; }
|
||||
.toggle-slider:before { content: ""; position: absolute; height: 26px; width: 26px; left: 4px; bottom: 4px; background: #fff; border-radius: 50%; transition: .4s; }
|
||||
.toggle-label input:checked + .toggle-slider { background: #4caf50; }
|
||||
.toggle-label input:checked + .toggle-slider:before { transform: translateX(26px); }
|
||||
.toggle-slider .icon { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 14px; pointer-events: none; opacity: 0; transition: opacity .2s; color: #4caf50; }
|
||||
.toggle-label input:checked + .toggle-slider .icon.check { opacity: 1; }
|
||||
.toggle-label input:not(:checked) + .toggle-slider .icon.cross { opacity: 1; }
|
||||
|
||||
/* Slides grid */
|
||||
.slides-container { display: grid; grid-template-columns: repeat(auto-fill, minmax(325px,1fr)); gap: 2rem; padding: 1rem; justify-items: center; }
|
||||
.slide-item { width: 150px; cursor: pointer; position: relative; transition: outline-color .3s ease; }
|
||||
.slide-item img { width: 100%; border-radius: 10px; outline: 3px solid transparent; outline-offset: -3px; box-shadow: 0 0 10px rgba(0,0,0,0.5); transition: outline-color .3s ease; }
|
||||
.slide-item.active img { outline-color: lime; }
|
||||
@media(min-width:768px){ .slide-item{ width:325px; } }
|
||||
|
||||
/* Bible search table */
|
||||
#bibleSearchContainer { text-align: center; margin-bottom: 1rem; }
|
||||
#bibleSearchInput { width: 60%; padding: .5rem; font-size:1rem; background:#333; border:1px solid #555; border-radius:4px; color:#fff; }
|
||||
#bibleSearchButton { padding:.5rem 1rem; margin-left:.5rem; background:#4caf50; border:none; border-radius:4px; color:#fff; cursor:pointer; }
|
||||
#bibleTable { width:100%; border-collapse:collapse; }
|
||||
#bibleTable th, #bibleTable td { border:1px solid #555; padding:.5rem; text-align:left; }
|
||||
#bibleTable th { background:#333; }
|
||||
|
||||
.custom-context-menu { position:absolute; display:none; z-index:9999; background:#333; border:1px solid #444; border-radius:8px; padding:8px; box-shadow:0 2px 5px rgba(0,0,0,0.5); min-width:160px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1 id="pageTitle">Announcements</h1>
|
||||
|
||||
<div class="preview-container">
|
||||
<iframe id="previewIframe" class="preview-iframe" src="http://127.0.0.1:5000/"></iframe>
|
||||
</div>
|
||||
|
||||
<!-- Slider toggle -->
|
||||
<div id="activeFilterContainer">
|
||||
<label class="toggle-label">
|
||||
<input type="checkbox" id="activeFilterToggle">
|
||||
<span class="toggle-slider">
|
||||
<span class="icon check">✔</span>
|
||||
<span class="icon cross">✖</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="tab-nav">
|
||||
<button id="announcementsTab" class="active-tab">Announcements</button>
|
||||
<button id="bibleTab">Bible Verses</button>
|
||||
</div>
|
||||
|
||||
<!-- Announcements Section -->
|
||||
<div id="announcementsSection">
|
||||
<div class="slides-container" id="slidesContainer">
|
||||
{% for slide, data in statuses %}
|
||||
<div class="slide-item {% if data.active %}active{% endif %}"
|
||||
data-slide="{{ slide }}"
|
||||
data-duration="{{ data.duration|default(7) }}"
|
||||
data-title="{{ data.title|default(slide.split('.')[0]) }}"
|
||||
data-end_date="{{ data.end_date|default('') }}">
|
||||
<img src="{{ slide_urls[slide] }}" alt="{{ slide }}">
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bible Verses Section -->
|
||||
<div id="bibleSection">
|
||||
<h2>Bible Verses</h2>
|
||||
<div id="bibleSearchContainer">
|
||||
<input type="text" id="bibleSearchInput" placeholder="Search verses..." />
|
||||
<button id="bibleSearchButton">Search</button>
|
||||
</div>
|
||||
<table id="bibleTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Book</th><th>Chapter</th><th>Verse</th><th>Text</th><th>Translation</th><th>Add/Remove</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Context Menu -->
|
||||
<div class="custom-context-menu" id="customContextMenu">
|
||||
<!-- existing context-menu items + submenus -->
|
||||
</div>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.5.4/socket.io.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
|
||||
<script>
|
||||
// 1) Load persisted registry
|
||||
let bibleRegistry = [];
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
fetch('/bible_registry')
|
||||
.then(r => r.json())
|
||||
.then(data => { bibleRegistry = data; })
|
||||
.catch(console.error);
|
||||
});
|
||||
|
||||
// 2) Tab switching
|
||||
const announcementsTab = document.getElementById('announcementsTab'),
|
||||
bibleTab = document.getElementById('bibleTab'),
|
||||
announcementsSec = document.getElementById('announcementsSection'),
|
||||
bibleSec = document.getElementById('bibleSection'),
|
||||
pageTitle = document.getElementById('pageTitle'),
|
||||
activeToggle = document.getElementById('activeFilterToggle'),
|
||||
slidesContainer = document.getElementById('slidesContainer');
|
||||
|
||||
announcementsTab.addEventListener('click', () => {
|
||||
announcementsSec.style.display = 'block';
|
||||
bibleSec.style.display = 'none';
|
||||
announcementsTab.classList.add('active-tab');
|
||||
bibleTab.classList.remove('active-tab');
|
||||
pageTitle.innerText = 'Announcements';
|
||||
activeToggle.parentElement.style.display = 'block';
|
||||
});
|
||||
bibleTab.addEventListener('click', () => {
|
||||
announcementsSec.style.display = 'none';
|
||||
bibleSec.style.display = 'block';
|
||||
bibleTab.classList.add('active-tab');
|
||||
announcementsTab.classList.remove('active-tab');
|
||||
pageTitle.innerText = 'Bible Verses';
|
||||
activeToggle.parentElement.style.display = 'none';
|
||||
});
|
||||
|
||||
// 3) Drag-and-drop ordering
|
||||
let isDragging = false;
|
||||
Sortable.create(slidesContainer, {
|
||||
animation: 150, delay: 50,
|
||||
onStart: () => { isDragging = true; },
|
||||
onEnd: () => {
|
||||
isDragging = false;
|
||||
const order = Array.from(slidesContainer.children).map(c => c.dataset.slide);
|
||||
fetch('/update_order', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ order })
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 4) Toggle hiding inactive slides
|
||||
activeToggle.addEventListener('change', () => {
|
||||
slidesContainer.querySelectorAll('.slide-item').forEach(item => {
|
||||
item.style.display = activeToggle.checked && !item.classList.contains('active')
|
||||
? 'none' : 'block';
|
||||
});
|
||||
});
|
||||
|
||||
// 5) Click slide to activate/deactivate
|
||||
slidesContainer.addEventListener('click', e => {
|
||||
if (isDragging) return;
|
||||
const item = e.target.closest('.slide-item');
|
||||
if (item && e.button === 0) {
|
||||
item.classList.toggle('active');
|
||||
fetch('/update_active', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
slide: item.dataset.slide,
|
||||
active: item.classList.contains('active')
|
||||
})
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 6) Bible search & add/remove logic
|
||||
const bibleSearchInput = document.getElementById('bibleSearchInput'),
|
||||
bibleSearchButton = document.getElementById('bibleSearchButton'),
|
||||
bibleTableBody = document.querySelector('#bibleTable tbody');
|
||||
|
||||
bibleSearchButton.addEventListener('click', () => {
|
||||
const q = bibleSearchInput.value.trim();
|
||||
bibleTableBody.innerHTML = '';
|
||||
|
||||
// Empty query → show what's in registry
|
||||
if (!q) {
|
||||
bibleRegistry.forEach(entry => {
|
||||
const idx = entry.reference.lastIndexOf(' '),
|
||||
book = entry.reference.slice(0, idx),
|
||||
[chapter, verse] = entry.reference.slice(idx + 1).split(':'),
|
||||
tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td>${book}</td>
|
||||
<td>${chapter}</td>
|
||||
<td>${verse}</td>
|
||||
<td>${entry.verse}</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
`;
|
||||
const btn = document.createElement('button');
|
||||
btn.textContent = '–';
|
||||
btn.style.width = '2rem';
|
||||
btn.dataset.id = entry.id;
|
||||
btn.addEventListener('click', () => {
|
||||
fetch('/remove_bible_verse', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id: Number(btn.dataset.id) })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(resp => {
|
||||
if (resp.status === 'success') {
|
||||
bibleRegistry = bibleRegistry.filter(e => e.id !== entry.id);
|
||||
tr.remove();
|
||||
}
|
||||
});
|
||||
});
|
||||
tr.cells[5].appendChild(btn);
|
||||
bibleTableBody.appendChild(tr);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Non-empty → query DB
|
||||
fetch('/search_verses?q=' + encodeURIComponent(q))
|
||||
.then(r => r.json())
|
||||
.then(results => {
|
||||
results.forEach(verse => {
|
||||
const ref = `${verse.book} ${verse.chapter}:${verse.verse}`,
|
||||
exist = bibleRegistry.find(e => e.reference === ref),
|
||||
tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td>${verse.book}</td>
|
||||
<td>${verse.chapter}</td>
|
||||
<td>${verse.verse}</td>
|
||||
<td>${verse.text}</td>
|
||||
<td>${verse.translation}</td>
|
||||
<td></td>
|
||||
`;
|
||||
const btn = document.createElement('button');
|
||||
btn.style.width = '2rem';
|
||||
if (exist) {
|
||||
btn.textContent = '–';
|
||||
btn.dataset.id = exist.id;
|
||||
} else {
|
||||
btn.textContent = '+';
|
||||
}
|
||||
btn.addEventListener('click', () => {
|
||||
if (btn.textContent === '+') {
|
||||
// Add
|
||||
fetch('/add_bible_verse', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ verse: verse.text, reference: ref })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(resp => {
|
||||
if (resp.status === 'success') {
|
||||
btn.textContent = '–';
|
||||
btn.dataset.id = resp.entry.id;
|
||||
bibleRegistry.push(resp.entry);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Remove
|
||||
const id = Number(btn.dataset.id);
|
||||
fetch('/remove_bible_verse', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(resp => {
|
||||
if (resp.status === 'success') {
|
||||
bibleRegistry = bibleRegistry.filter(e => e.id !== id);
|
||||
btn.textContent = '+';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
tr.cells[5].appendChild(btn);
|
||||
bibleTableBody.appendChild(tr);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
bibleSearchInput.addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') bibleSearchButton.click();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,79 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Church Announcements</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #000;
|
||||
}
|
||||
.slideshow-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
/* Both images overlap exactly */
|
||||
.slideshow-container img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
transition: opacity 1s ease-in-out;
|
||||
opacity: 0;
|
||||
}
|
||||
/* The active image is visible */
|
||||
.slideshow-container img.active {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="slideshow-container">
|
||||
<img id="slide1" src="" alt="Announcement Slide">
|
||||
<img id="slide2" src="" alt="Announcement Slide">
|
||||
</div>
|
||||
<!-- Include Socket.IO client library -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.5.4/socket.io.min.js"></script>
|
||||
<script>
|
||||
const socket = io();
|
||||
// Two image elements for crossfade
|
||||
const slide1 = document.getElementById("slide1");
|
||||
const slide2 = document.getElementById("slide2");
|
||||
let currentVisible = 1; // track which image is currently visible
|
||||
|
||||
socket.on('sync_slide', function(data) {
|
||||
console.log("sync_slide received:", data);
|
||||
const newUrl = data.slide_url;
|
||||
// Determine which image is hidden and which is visible.
|
||||
let currentImg = currentVisible === 1 ? slide1 : slide2;
|
||||
let nextImg = currentVisible === 1 ? slide2 : slide1;
|
||||
|
||||
// Preload the new image.
|
||||
const preloadedImage = new Image();
|
||||
preloadedImage.onload = function() {
|
||||
// When loaded, set the source of the hidden image.
|
||||
nextImg.src = newUrl;
|
||||
// Force a reflow to ensure the transition applies.
|
||||
void nextImg.offsetWidth;
|
||||
// Fade in the hidden image and fade out the visible one.
|
||||
nextImg.classList.add("active");
|
||||
currentImg.classList.remove("active");
|
||||
// Update the tracking variable.
|
||||
currentVisible = currentVisible === 1 ? 2 : 1;
|
||||
};
|
||||
preloadedImage.onerror = function() {
|
||||
console.error("Error loading image:", newUrl);
|
||||
};
|
||||
preloadedImage.src = newUrl;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,247 @@
|
||||
<#
|
||||
.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 VIRTUAL_ENV_PROMPT altogether.
|
||||
if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
|
||||
Remove-Item -Path env:VIRTUAL_ENV_PROMPT
|
||||
}
|
||||
|
||||
# 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
|
||||
}
|
||||
$env:VIRTUAL_ENV_PROMPT = $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"
|
||||
@@ -0,0 +1,70 @@
|
||||
# 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
|
||||
|
||||
# Call hash to forget past commands. Without forgetting
|
||||
# past commands the $PATH changes we made may not be respected
|
||||
hash -r 2> /dev/null
|
||||
|
||||
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
|
||||
PS1="${_OLD_VIRTUAL_PS1:-}"
|
||||
export PS1
|
||||
unset _OLD_VIRTUAL_PS1
|
||||
fi
|
||||
|
||||
unset VIRTUAL_ENV
|
||||
unset VIRTUAL_ENV_PROMPT
|
||||
if [ ! "${1:-}" = "nondestructive" ] ; then
|
||||
# Self destruct!
|
||||
unset -f deactivate
|
||||
fi
|
||||
}
|
||||
|
||||
# unset irrelevant variables
|
||||
deactivate nondestructive
|
||||
|
||||
# on Windows, a path can contain colons and backslashes and has to be converted:
|
||||
if [ "${OSTYPE:-}" = "cygwin" ] || [ "${OSTYPE:-}" = "msys" ] ; then
|
||||
# transform D:\path\to\venv to /d/path/to/venv on MSYS
|
||||
# and to /cygdrive/d/path/to/venv on Cygwin
|
||||
export VIRTUAL_ENV=$(cygpath "/Users/mediarogc/Documents/Annoucements Web/venv")
|
||||
else
|
||||
# use the path as-is
|
||||
export VIRTUAL_ENV="/Users/mediarogc/Documents/Annoucements Web/venv"
|
||||
fi
|
||||
|
||||
_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
|
||||
VIRTUAL_ENV_PROMPT="(venv) "
|
||||
export VIRTUAL_ENV_PROMPT
|
||||
fi
|
||||
|
||||
# Call hash to forget past commands. Without forgetting
|
||||
# past commands the $PATH changes we made may not be respected
|
||||
hash -r 2> /dev/null
|
||||
@@ -0,0 +1,27 @@
|
||||
# This file must be used with "source bin/activate.csh" *from csh*.
|
||||
# You cannot run it directly.
|
||||
|
||||
# Created by Davide Di Blasi <[email protected]>.
|
||||
# Ported to Python 3.3 venv by Andrew Svetlov <[email protected]>
|
||||
|
||||
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; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate'
|
||||
|
||||
# Unset irrelevant variables.
|
||||
deactivate nondestructive
|
||||
|
||||
setenv VIRTUAL_ENV "/Users/mediarogc/Documents/Annoucements Web/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"
|
||||
setenv VIRTUAL_ENV_PROMPT "(venv) "
|
||||
endif
|
||||
|
||||
alias pydoc python -m pydoc
|
||||
|
||||
rehash
|
||||
@@ -0,0 +1,69 @@
|
||||
# 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"
|
||||
set -e _OLD_FISH_PROMPT_OVERRIDE
|
||||
# prevents error when using nested fish instances (Issue #93858)
|
||||
if functions -q _old_fish_prompt
|
||||
functions -e fish_prompt
|
||||
functions -c _old_fish_prompt fish_prompt
|
||||
functions -e _old_fish_prompt
|
||||
end
|
||||
end
|
||||
|
||||
set -e VIRTUAL_ENV
|
||||
set -e VIRTUAL_ENV_PROMPT
|
||||
if test "$argv[1]" != "nondestructive"
|
||||
# Self-destruct!
|
||||
functions -e deactivate
|
||||
end
|
||||
end
|
||||
|
||||
# Unset irrelevant variables.
|
||||
deactivate nondestructive
|
||||
|
||||
set -gx VIRTUAL_ENV "/Users/mediarogc/Documents/Annoucements Web/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"
|
||||
set -gx VIRTUAL_ENV_PROMPT "(venv) "
|
||||
end
|
||||
@@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
'''exec' "/Users/mediarogc/Documents/Annoucements Web/venv/bin/python3.12" "$0" "$@"
|
||||
' '''
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from flask.cli import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
'''exec' "/Users/mediarogc/Documents/Annoucements Web/venv/bin/python3.12" "$0" "$@"
|
||||
' '''
|
||||
# -*- 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())
|
||||
@@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
'''exec' "/Users/mediarogc/Documents/Annoucements Web/venv/bin/python3.12" "$0" "$@"
|
||||
' '''
|
||||
# -*- 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())
|
||||
@@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
'''exec' "/Users/mediarogc/Documents/Annoucements Web/venv/bin/python3.12" "$0" "$@"
|
||||
' '''
|
||||
# -*- 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())
|
||||
@@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
'''exec' "/Users/mediarogc/Documents/Annoucements Web/venv/bin/python3.12" "$0" "$@"
|
||||
' '''
|
||||
# -*- 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())
|
||||
@@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
'''exec' "/Users/mediarogc/Documents/Annoucements Web/venv/bin/python3.12" "$0" "$@"
|
||||
' '''
|
||||
# -*- 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())
|
||||
@@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
'''exec' "/Users/mediarogc/Documents/Annoucements Web/venv/bin/python3.12" "$0" "$@"
|
||||
' '''
|
||||
# -*- 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())
|
||||
@@ -0,0 +1 @@
|
||||
python3.12
|
||||
@@ -0,0 +1 @@
|
||||
python3.12
|
||||
@@ -0,0 +1 @@
|
||||
/Library/Frameworks/Python.framework/Versions/3.12/bin/python3.12
|
||||
@@ -0,0 +1 @@
|
||||
pip
|
||||
@@ -0,0 +1,20 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Miguel Grinberg
|
||||
|
||||
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,76 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: Flask-SocketIO
|
||||
Version: 5.5.1
|
||||
Summary: Socket.IO integration for Flask applications
|
||||
Author-email: Miguel Grinberg <[email protected]>
|
||||
Project-URL: Homepage, https://github.com/miguelgrinberg/flask-socketio
|
||||
Project-URL: Bug Tracker, https://github.com/miguelgrinberg/flask-socketio/issues
|
||||
Classifier: Environment :: Web Environment
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: Operating System :: OS Independent
|
||||
Requires-Python: >=3.6
|
||||
Description-Content-Type: text/markdown
|
||||
License-File: LICENSE
|
||||
Requires-Dist: Flask>=0.9
|
||||
Requires-Dist: python-socketio>=5.12.0
|
||||
Provides-Extra: docs
|
||||
Requires-Dist: sphinx; extra == "docs"
|
||||
|
||||
Flask-SocketIO
|
||||
==============
|
||||
|
||||
[](https://github.com/miguelgrinberg/Flask-SocketIO/actions) [](https://codecov.io/gh/miguelgrinberg/flask-socketio)
|
||||
|
||||
Socket.IO integration for Flask applications.
|
||||
|
||||
Sponsors
|
||||
--------
|
||||
|
||||
The following organizations are funding this project:
|
||||
|
||||
<br>[Socket.IO](https://socket.io) | [Add your company here!](https://github.com/sponsors/miguelgrinberg)|
|
||||
-|-
|
||||
|
||||
Many individual sponsors also support this project through small ongoing contributions. Why not [join them](https://github.com/sponsors/miguelgrinberg)?
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
You can install this package as usual with pip:
|
||||
|
||||
pip install flask-socketio
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
```py
|
||||
from flask import Flask, render_template
|
||||
from flask_socketio import SocketIO, emit
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['SECRET_KEY'] = 'secret!'
|
||||
socketio = SocketIO(app)
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return render_template('index.html')
|
||||
|
||||
@socketio.event
|
||||
def my_event(message):
|
||||
emit('my response', {'data': 'got it!'})
|
||||
|
||||
if __name__ == '__main__':
|
||||
socketio.run(app)
|
||||
```
|
||||
|
||||
Resources
|
||||
---------
|
||||
|
||||
- [Tutorial](http://blog.miguelgrinberg.com/post/easy-websockets-with-flask-and-gevent)
|
||||
- [Documentation](http://flask-socketio.readthedocs.io/en/latest/)
|
||||
- [PyPI](https://pypi.python.org/pypi/Flask-SocketIO)
|
||||
- [Change Log](https://github.com/miguelgrinberg/Flask-SocketIO/blob/main/CHANGES.md)
|
||||
- Questions? See the [questions](https://stackoverflow.com/questions/tagged/flask-socketio) others have asked on Stack Overflow, or [ask](https://stackoverflow.com/questions/ask?tags=python+flask-socketio+python-socketio) your own question.
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
Flask_SocketIO-5.5.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
Flask_SocketIO-5.5.1.dist-info/LICENSE,sha256=aNCWbkgKjS_T1cJtACyZbvCM36KxWnfQ0LWTuavuYKQ,1082
|
||||
Flask_SocketIO-5.5.1.dist-info/METADATA,sha256=7YA8ZKizrtJiaCqqdDiTU6t1xWWdTmNw3CqBxSMcW3k,2635
|
||||
Flask_SocketIO-5.5.1.dist-info/RECORD,,
|
||||
Flask_SocketIO-5.5.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
Flask_SocketIO-5.5.1.dist-info/WHEEL,sha256=A3WOREP4zgxI0fKrHUG8DC8013e3dK3n7a6HDbcEIwE,91
|
||||
Flask_SocketIO-5.5.1.dist-info/top_level.txt,sha256=C1ugzQBJ3HHUJsWGzyt70XRVOX-y4CUAR8MWKjwJOQ8,15
|
||||
flask_socketio/__init__.py,sha256=5hN0LE0hfGMUDcX4FheZrtXERJ1IBEPagv0pgeqdtlU,54904
|
||||
flask_socketio/__pycache__/__init__.cpython-312.pyc,,
|
||||
flask_socketio/__pycache__/namespace.cpython-312.pyc,,
|
||||
flask_socketio/__pycache__/test_client.cpython-312.pyc,,
|
||||
flask_socketio/namespace.py,sha256=UkVryJvFYgnCMKWSF35GVfGdyh2cXRDyRbfmEPPchVA,2329
|
||||
flask_socketio/test_client.py,sha256=rClk02TSRqgidH8IyeohspKVKdpRx7gcZBjg1YUtZpA,11026
|
||||
@@ -0,0 +1,5 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: setuptools (75.7.0)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
flask_socketio
|
||||
@@ -0,0 +1 @@
|
||||
pip
|
||||
@@ -0,0 +1,28 @@
|
||||
Copyright 2010 Pallets
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
||||
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -0,0 +1,92 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: MarkupSafe
|
||||
Version: 3.0.2
|
||||
Summary: Safely add untrusted strings to HTML/XML markup.
|
||||
Maintainer-email: Pallets <[email protected]>
|
||||
License: Copyright 2010 Pallets
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
||||
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Project-URL: Donate, https://palletsprojects.com/donate
|
||||
Project-URL: Documentation, https://markupsafe.palletsprojects.com/
|
||||
Project-URL: Changes, https://markupsafe.palletsprojects.com/changes/
|
||||
Project-URL: Source, https://github.com/pallets/markupsafe/
|
||||
Project-URL: Chat, https://discord.gg/pallets
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Environment :: Web Environment
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: BSD License
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
||||
Classifier: Topic :: Text Processing :: Markup :: HTML
|
||||
Classifier: Typing :: Typed
|
||||
Requires-Python: >=3.9
|
||||
Description-Content-Type: text/markdown
|
||||
License-File: LICENSE.txt
|
||||
|
||||
# MarkupSafe
|
||||
|
||||
MarkupSafe implements a text object that escapes characters so it is
|
||||
safe to use in HTML and XML. Characters that have special meanings are
|
||||
replaced so that they display as the actual characters. This mitigates
|
||||
injection attacks, meaning untrusted user input can safely be displayed
|
||||
on a page.
|
||||
|
||||
|
||||
## Examples
|
||||
|
||||
```pycon
|
||||
>>> from markupsafe import Markup, escape
|
||||
|
||||
>>> # escape replaces special characters and wraps in Markup
|
||||
>>> escape("<script>alert(document.cookie);</script>")
|
||||
Markup('<script>alert(document.cookie);</script>')
|
||||
|
||||
>>> # wrap in Markup to mark text "safe" and prevent escaping
|
||||
>>> Markup("<strong>Hello</strong>")
|
||||
Markup('<strong>hello</strong>')
|
||||
|
||||
>>> escape(Markup("<strong>Hello</strong>"))
|
||||
Markup('<strong>hello</strong>')
|
||||
|
||||
>>> # Markup is a str subclass
|
||||
>>> # methods and operators escape their arguments
|
||||
>>> template = Markup("Hello <em>{name}</em>")
|
||||
>>> template.format(name='"World"')
|
||||
Markup('Hello <em>"World"</em>')
|
||||
```
|
||||
|
||||
## Donate
|
||||
|
||||
The Pallets organization develops and supports MarkupSafe and other
|
||||
popular packages. In order to grow the community of contributors and
|
||||
users, and allow the maintainers to devote more time to the projects,
|
||||
[please donate today][].
|
||||
|
||||
[please donate today]: https://palletsprojects.com/donate
|
||||
@@ -0,0 +1,14 @@
|
||||
MarkupSafe-3.0.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
MarkupSafe-3.0.2.dist-info/LICENSE.txt,sha256=SJqOEQhQntmKN7uYPhHg9-HTHwvY-Zp5yESOf_N9B-o,1475
|
||||
MarkupSafe-3.0.2.dist-info/METADATA,sha256=aAwbZhSmXdfFuMM-rEHpeiHRkBOGESyVLJIuwzHP-nw,3975
|
||||
MarkupSafe-3.0.2.dist-info/RECORD,,
|
||||
MarkupSafe-3.0.2.dist-info/WHEEL,sha256=T94HOVPNbYE6jyG6QmiIglWJ01nwJvHIWFgubY8hhjc,109
|
||||
MarkupSafe-3.0.2.dist-info/top_level.txt,sha256=qy0Plje5IJuvsCBjejJyhDCjEAdcDLK_2agVcex8Z6U,11
|
||||
markupsafe/__init__.py,sha256=sr-U6_27DfaSrj5jnHYxWN-pvhM27sjlDplMDPZKm7k,13214
|
||||
markupsafe/__pycache__/__init__.cpython-312.pyc,,
|
||||
markupsafe/__pycache__/_native.cpython-312.pyc,,
|
||||
markupsafe/_native.py,sha256=hSLs8Jmz5aqayuengJJ3kdT5PwNpBWpKrmQSdipndC8,210
|
||||
markupsafe/_speedups.c,sha256=O7XulmTo-epI6n2FtMVOrJXl8EAaIwD2iNYmBI5SEoQ,4149
|
||||
markupsafe/_speedups.cpython-312-darwin.so,sha256=LAX7ul6PQsfO_zZxF631eSzN6U7PoGVTR_Wwd2pGav8,50624
|
||||
markupsafe/_speedups.pyi,sha256=ENd1bYe7gbBUf2ywyYWOGUpnXOHNJ-cgTNqetlW8h5k,41
|
||||
markupsafe/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
@@ -0,0 +1,5 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: setuptools (75.2.0)
|
||||
Root-Is-Purelib: false
|
||||
Tag: cp312-cp312-macosx_11_0_arm64
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
markupsafe
|
||||
@@ -0,0 +1 @@
|
||||
pip
|
||||
@@ -0,0 +1,376 @@
|
||||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
Copyright 2009-2024 Joshua Bronson. All rights reserved.
|
||||
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
||||
@@ -0,0 +1,260 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: bidict
|
||||
Version: 0.23.1
|
||||
Summary: The bidirectional mapping library for Python.
|
||||
Author-email: Joshua Bronson <[email protected]>
|
||||
License: MPL 2.0
|
||||
Project-URL: Changelog, https://bidict.readthedocs.io/changelog.html
|
||||
Project-URL: Documentation, https://bidict.readthedocs.io
|
||||
Project-URL: Funding, https://bidict.readthedocs.io/#sponsoring
|
||||
Project-URL: Repository, https://github.com/jab/bidict
|
||||
Keywords: bidict,bimap,bidirectional,dict,dictionary,mapping,collections
|
||||
Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Programming Language :: Python :: Implementation :: CPython
|
||||
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
||||
Classifier: Typing :: Typed
|
||||
Requires-Python: >=3.8
|
||||
Description-Content-Type: text/x-rst
|
||||
License-File: LICENSE
|
||||
|
||||
.. role:: doc
|
||||
.. (Forward declaration for the "doc" role that Sphinx defines for interop with renderers that
|
||||
are often used to show this doc and that are unaware of Sphinx (GitHub.com, PyPI.org, etc.).
|
||||
Use :doc: rather than :ref: here for better interop as well.)
|
||||
|
||||
|
||||
bidict
|
||||
======
|
||||
|
||||
*The bidirectional mapping library for Python.*
|
||||
|
||||
|
||||
Status
|
||||
------
|
||||
|
||||
.. image:: https://img.shields.io/pypi/v/bidict.svg
|
||||
:target: https://pypi.org/project/bidict
|
||||
:alt: Latest release
|
||||
|
||||
.. image:: https://img.shields.io/readthedocs/bidict/main.svg
|
||||
:target: https://bidict.readthedocs.io/en/main/
|
||||
:alt: Documentation
|
||||
|
||||
.. image:: https://github.com/jab/bidict/actions/workflows/test.yml/badge.svg
|
||||
:target: https://github.com/jab/bidict/actions/workflows/test.yml?query=branch%3Amain
|
||||
:alt: GitHub Actions CI status
|
||||
|
||||
.. image:: https://img.shields.io/pypi/l/bidict.svg
|
||||
:target: https://raw.githubusercontent.com/jab/bidict/main/LICENSE
|
||||
:alt: License
|
||||
|
||||
.. image:: https://static.pepy.tech/badge/bidict
|
||||
:target: https://pepy.tech/project/bidict
|
||||
:alt: PyPI Downloads
|
||||
|
||||
.. image:: https://img.shields.io/badge/GitHub-sponsor-ff69b4
|
||||
:target: https://github.com/sponsors/jab
|
||||
:alt: Sponsor
|
||||
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- Mature: Depended on by
|
||||
Google, Venmo, CERN, Baidu, Tencent,
|
||||
and teams across the world since 2009
|
||||
|
||||
- Familiar, Pythonic APIs
|
||||
that are carefully designed for
|
||||
safety, simplicity, flexibility, and ergonomics
|
||||
|
||||
- Lightweight, with no runtime dependencies
|
||||
outside Python's standard library
|
||||
|
||||
- Implemented in
|
||||
concise, well-factored, fully type-hinted Python code
|
||||
that is optimized for running efficiently
|
||||
as well as for long-term maintenance and stability
|
||||
(as well as `joy <#learning-from-bidict>`__)
|
||||
|
||||
- Extensively `documented <https://bidict.readthedocs.io>`__
|
||||
|
||||
- 100% test coverage
|
||||
running continuously across all supported Python versions
|
||||
(including property-based tests and benchmarks)
|
||||
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
``pip install bidict``
|
||||
|
||||
|
||||
Quick Start
|
||||
-----------
|
||||
|
||||
.. code:: python
|
||||
|
||||
>>> from bidict import bidict
|
||||
>>> element_by_symbol = bidict({'H': 'hydrogen'})
|
||||
>>> element_by_symbol['H']
|
||||
'hydrogen'
|
||||
>>> element_by_symbol.inverse['hydrogen']
|
||||
'H'
|
||||
|
||||
|
||||
For more usage documentation,
|
||||
head to the :doc:`intro` [#fn-intro]_
|
||||
and proceed from there.
|
||||
|
||||
|
||||
Enterprise Support
|
||||
------------------
|
||||
|
||||
Enterprise-level support for bidict can be obtained via the
|
||||
`Tidelift subscription <https://tidelift.com/subscription/pkg/pypi-bidict?utm_source=pypi-bidict&utm_medium=referral&utm_campaign=readme>`__
|
||||
or by `contacting me directly <mailto:[email protected]>`__.
|
||||
|
||||
I have a US-based LLC set up for invoicing,
|
||||
and I have 15+ years of professional experience
|
||||
delivering software and support to companies successfully.
|
||||
|
||||
You can also sponsor my work through several platforms, including GitHub Sponsors.
|
||||
See the `Sponsoring <#sponsoring>`__ section below for details,
|
||||
including rationale and examples of companies
|
||||
supporting the open source projects they depend on.
|
||||
|
||||
|
||||
Voluntary Community Support
|
||||
---------------------------
|
||||
|
||||
Please search through already-asked questions and answers
|
||||
in `GitHub Discussions <https://github.com/jab/bidict/discussions>`__
|
||||
and the `issue tracker <https://github.com/jab/bidict/issues?q=is%3Aissue>`__
|
||||
in case your question has already been addressed.
|
||||
|
||||
Otherwise, please feel free to
|
||||
`start a new discussion <https://github.com/jab/bidict/discussions>`__
|
||||
or `create a new issue <https://github.com/jab/bidict/issues/new>`__ on GitHub
|
||||
for voluntary community support.
|
||||
|
||||
|
||||
Notice of Usage
|
||||
---------------
|
||||
|
||||
If you use bidict,
|
||||
and especially if your usage or your organization is significant in some way,
|
||||
please let me know in any of the following ways:
|
||||
|
||||
- `star bidict on GitHub <https://github.com/jab/bidict>`__
|
||||
- post in `GitHub Discussions <https://github.com/jab/bidict/discussions>`__
|
||||
- `email me <mailto:[email protected]>`__
|
||||
|
||||
|
||||
Changelog
|
||||
---------
|
||||
|
||||
For bidict release notes, see the :doc:`changelog`. [#fn-changelog]_
|
||||
|
||||
|
||||
Release Notifications
|
||||
---------------------
|
||||
|
||||
.. duplicated in CHANGELOG.rst:
|
||||
(Would use `.. include::` but GitHub's renderer doesn't support it.)
|
||||
|
||||
Watch `bidict releases on GitHub <https://github.com/jab/bidict/releases>`__
|
||||
to be notified when new versions of bidict are published.
|
||||
Click the "Watch" dropdown, choose "Custom", and then choose "Releases".
|
||||
|
||||
|
||||
Learning from bidict
|
||||
--------------------
|
||||
|
||||
One of the best things about bidict
|
||||
is that it touches a surprising number of
|
||||
interesting Python corners,
|
||||
especially given its small size and scope.
|
||||
|
||||
Check out :doc:`learning-from-bidict` [#fn-learning]_
|
||||
if you're interested in learning more.
|
||||
|
||||
|
||||
Contributing
|
||||
------------
|
||||
|
||||
I have been bidict's sole maintainer
|
||||
and `active contributor <https://github.com/jab/bidict/graphs/contributors>`__
|
||||
since I started the project ~15 years ago.
|
||||
|
||||
Your help would be most welcome!
|
||||
See the :doc:`contributors-guide` [#fn-contributing]_
|
||||
for more information.
|
||||
|
||||
|
||||
Sponsoring
|
||||
----------
|
||||
|
||||
.. duplicated in CONTRIBUTING.rst
|
||||
(Would use `.. include::` but GitHub's renderer doesn't support it.)
|
||||
|
||||
.. image:: https://img.shields.io/badge/GitHub-sponsor-ff69b4
|
||||
:target: https://github.com/sponsors/jab
|
||||
:alt: Sponsor through GitHub
|
||||
|
||||
Bidict is the product of thousands of hours of my unpaid work
|
||||
over the 15+ years that I've been the sole maintainer.
|
||||
|
||||
If bidict has helped you or your company accomplish your work,
|
||||
please sponsor my work through one of the following,
|
||||
and/or ask your company to do the same:
|
||||
|
||||
- `GitHub <https://github.com/sponsors/jab>`__
|
||||
- `PayPal <https://www.paypal.com/cgi-bin/webscr?cmd=_xclick&business=jabronson%40gmail%2ecom&lc=US&item_name=Sponsor%20bidict>`__
|
||||
- `Tidelift <https://tidelift.com>`__
|
||||
- `thanks.dev <https://thanks.dev>`__
|
||||
- `Gumroad <https://gumroad.com/l/bidict>`__
|
||||
- `a support engagement with my LLC <#enterprise-support>`__
|
||||
|
||||
If you're not sure which to use, GitHub is an easy option,
|
||||
especially if you already have a GitHub account.
|
||||
Just choose a monthly or one-time amount, and GitHub handles everything else.
|
||||
Your bidict sponsorship on GitHub will automatically go
|
||||
on the same regular bill as any other GitHub charges you pay for.
|
||||
PayPal is another easy option for one-time contributions.
|
||||
|
||||
See the following for rationale and examples of companies
|
||||
supporting the open source projects they depend on
|
||||
in this manner:
|
||||
|
||||
- `<https://engineering.atspotify.com/2022/04/announcing-the-spotify-foss-fund/>`__
|
||||
- `<https://blog.sentry.io/2021/10/21/we-just-gave-154-999-dollars-and-89-cents-to-open-source-maintainers>`__
|
||||
- `<https://engineering.indeedblog.com/blog/2019/07/foss-fund-six-months-in/>`__
|
||||
|
||||
.. - `<https://sethmlarson.dev/blog/people-in-your-software-supply-chain>`__
|
||||
.. - `<https://www.cognitect.com/blog/supporting-open-source-developers>`__
|
||||
.. - `<https://vorpus.org/blog/the-unreasonable-effectiveness-of-investment-in-open-source-infrastructure/>`__
|
||||
|
||||
|
||||
Finding Documentation
|
||||
---------------------
|
||||
|
||||
If you're viewing this on `<https://bidict.readthedocs.io>`__,
|
||||
note that multiple versions of the documentation are available,
|
||||
and you can choose a different version using the popup menu at the bottom-right.
|
||||
Please make sure you're viewing the version of the documentation
|
||||
that corresponds to the version of bidict you'd like to use.
|
||||
|
||||
If you're viewing this on GitHub, PyPI, or some other place
|
||||
that can't render and link this documentation properly
|
||||
and are seeing broken links,
|
||||
try these alternate links instead:
|
||||
|
||||
.. [#fn-intro] `<https://bidict.readthedocs.io/intro.html>`__ | `<docs/intro.rst>`__
|
||||
|
||||
.. [#fn-changelog] `<https://bidict.readthedocs.io/changelog.html>`__ | `<CHANGELOG.rst>`__
|
||||
|
||||
.. [#fn-learning] `<https://bidict.readthedocs.io/learning-from-bidict.html>`__ | `<docs/learning-from-bidict.rst>`__
|
||||
|
||||
.. [#fn-contributing] `<https://bidict.readthedocs.io/contributors-guide.html>`__ | `<CONTRIBUTING.rst>`__
|
||||
@@ -0,0 +1,31 @@
|
||||
bidict-0.23.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
bidict-0.23.1.dist-info/LICENSE,sha256=8_U63OyqSNc6ZuI4-lupBstBh2eDtF0ooTRrMULuvZo,16784
|
||||
bidict-0.23.1.dist-info/METADATA,sha256=2ovIRm6Df8gdwAMekGqkeBSF5TWj2mv1jpmh4W4ks7o,8704
|
||||
bidict-0.23.1.dist-info/RECORD,,
|
||||
bidict-0.23.1.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
||||
bidict-0.23.1.dist-info/top_level.txt,sha256=WuQO02jp0ODioS7sJoaHg3JJ5_3h6Sxo9RITvNGPYmc,7
|
||||
bidict/__init__.py,sha256=pL87KsrDpBsl3AG09LQk1t1TSFt0hIJVYa2POMdErN8,4398
|
||||
bidict/__pycache__/__init__.cpython-312.pyc,,
|
||||
bidict/__pycache__/_abc.cpython-312.pyc,,
|
||||
bidict/__pycache__/_base.cpython-312.pyc,,
|
||||
bidict/__pycache__/_bidict.cpython-312.pyc,,
|
||||
bidict/__pycache__/_dup.cpython-312.pyc,,
|
||||
bidict/__pycache__/_exc.cpython-312.pyc,,
|
||||
bidict/__pycache__/_frozen.cpython-312.pyc,,
|
||||
bidict/__pycache__/_iter.cpython-312.pyc,,
|
||||
bidict/__pycache__/_orderedbase.cpython-312.pyc,,
|
||||
bidict/__pycache__/_orderedbidict.cpython-312.pyc,,
|
||||
bidict/__pycache__/_typing.cpython-312.pyc,,
|
||||
bidict/__pycache__/metadata.cpython-312.pyc,,
|
||||
bidict/_abc.py,sha256=SMCNdCsmqSWg0OGnMZtnnXY8edjXcyZup5tva4HBm_c,3172
|
||||
bidict/_base.py,sha256=YiauA0aj52fNB6cfZ4gBt6OV-CRQoZm7WVhuw1nT-Cg,24439
|
||||
bidict/_bidict.py,sha256=Sr-RoEzWOaxpnDRbDJ7ngaGRIsyGnqZgzvR-NyT4jl4,6923
|
||||
bidict/_dup.py,sha256=YAn5gWA6lwMBA5A6ebVF19UTZyambGS8WxmbK4TN1Ww,2079
|
||||
bidict/_exc.py,sha256=HnD_WgteI5PrXa3zBx9RUiGlgnZTO6CF4nIU9p3-njk,1066
|
||||
bidict/_frozen.py,sha256=p4TaRHKeyTs0KmlpwSnZiTlN_CR4J97kAgBpNdZHQMs,1771
|
||||
bidict/_iter.py,sha256=zVUx-hJ1M4YuJROoFWRjPKlcaFnyo1AAuRpOaKAFhOQ,1530
|
||||
bidict/_orderedbase.py,sha256=M7v5rHa7vrym9Z3DxQBFQDxjnrr39Z8p26V0c1PggoE,8942
|
||||
bidict/_orderedbidict.py,sha256=pPnmC19mIISrj8_yjnb-4r_ti1B74tD5eTd08DETNuI,7080
|
||||
bidict/_typing.py,sha256=AylMZpBhEFTQegfziPSxfKkKLk7oUsH6o3awDIg2z_k,1289
|
||||
bidict/metadata.py,sha256=BMIKu6fBY_OKeV_q48EpumE7MdmFw8rFcdaUz8kcIYk,573
|
||||
bidict/py.typed,sha256=RJao5SVFYIp8IfbxhL_SpZkBQYe3XXzPlobSRdh4B_c,16
|
||||
@@ -0,0 +1,5 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: bdist_wheel (0.42.0)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
bidict
|
||||
@@ -0,0 +1,103 @@
|
||||
# Copyright 2009-2024 Joshua Bronson. All rights reserved.
|
||||
#
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# * Welcome to the bidict source code *
|
||||
# ============================================================================
|
||||
|
||||
# Reading through the code? You'll find a "Code review nav" comment like the one
|
||||
# below at the top and bottom of the key source files. Follow these cues to take
|
||||
# a path through the code that's optimized for familiarizing yourself with it.
|
||||
#
|
||||
# If you're not reading this on https://github.com/jab/bidict already, go there
|
||||
# to ensure you have the latest version of the code. While there, you can also
|
||||
# star the project, watch it for updates, fork the code, and submit an issue or
|
||||
# pull request with any proposed changes. More information can be found linked
|
||||
# from README.rst, which is also shown on https://github.com/jab/bidict.
|
||||
|
||||
# * Code review nav *
|
||||
# ============================================================================
|
||||
# Current: __init__.py Next: _abc.py →
|
||||
# ============================================================================
|
||||
|
||||
|
||||
"""The bidirectional mapping library for Python.
|
||||
|
||||
----
|
||||
|
||||
bidict by example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> from bidict import bidict
|
||||
>>> element_by_symbol = bidict({'H': 'hydrogen'})
|
||||
>>> element_by_symbol['H']
|
||||
'hydrogen'
|
||||
>>> element_by_symbol.inverse['hydrogen']
|
||||
'H'
|
||||
|
||||
|
||||
Please see https://github.com/jab/bidict for the most up-to-date code and
|
||||
https://bidict.readthedocs.io for the most up-to-date documentation
|
||||
if you are reading this elsewhere.
|
||||
|
||||
----
|
||||
|
||||
.. :copyright: (c) 2009-2024 Joshua Bronson.
|
||||
.. :license: MPLv2. See LICENSE for details.
|
||||
"""
|
||||
|
||||
# Use private aliases to not re-export these publicly (for Sphinx automodule with imported-members).
|
||||
from __future__ import annotations as _annotations
|
||||
|
||||
from contextlib import suppress as _suppress
|
||||
|
||||
from ._abc import BidirectionalMapping as BidirectionalMapping
|
||||
from ._abc import MutableBidirectionalMapping as MutableBidirectionalMapping
|
||||
from ._base import BidictBase as BidictBase
|
||||
from ._base import BidictKeysView as BidictKeysView
|
||||
from ._base import GeneratedBidictInverse as GeneratedBidictInverse
|
||||
from ._bidict import MutableBidict as MutableBidict
|
||||
from ._bidict import bidict as bidict
|
||||
from ._dup import DROP_NEW as DROP_NEW
|
||||
from ._dup import DROP_OLD as DROP_OLD
|
||||
from ._dup import ON_DUP_DEFAULT as ON_DUP_DEFAULT
|
||||
from ._dup import ON_DUP_DROP_OLD as ON_DUP_DROP_OLD
|
||||
from ._dup import ON_DUP_RAISE as ON_DUP_RAISE
|
||||
from ._dup import RAISE as RAISE
|
||||
from ._dup import OnDup as OnDup
|
||||
from ._dup import OnDupAction as OnDupAction
|
||||
from ._exc import BidictException as BidictException
|
||||
from ._exc import DuplicationError as DuplicationError
|
||||
from ._exc import KeyAndValueDuplicationError as KeyAndValueDuplicationError
|
||||
from ._exc import KeyDuplicationError as KeyDuplicationError
|
||||
from ._exc import ValueDuplicationError as ValueDuplicationError
|
||||
from ._frozen import frozenbidict as frozenbidict
|
||||
from ._iter import inverted as inverted
|
||||
from ._orderedbase import OrderedBidictBase as OrderedBidictBase
|
||||
from ._orderedbidict import OrderedBidict as OrderedBidict
|
||||
from .metadata import __author__ as __author__
|
||||
from .metadata import __copyright__ as __copyright__
|
||||
from .metadata import __description__ as __description__
|
||||
from .metadata import __license__ as __license__
|
||||
from .metadata import __url__ as __url__
|
||||
from .metadata import __version__ as __version__
|
||||
|
||||
|
||||
# Set __module__ of re-exported classes to the 'bidict' top-level module, so that e.g.
|
||||
# 'bidict.bidict' shows up as 'bidict.bidict` rather than 'bidict._bidict.bidict'.
|
||||
for _obj in tuple(locals().values()): # pragma: no cover
|
||||
if not getattr(_obj, '__module__', '').startswith('bidict.'):
|
||||
continue
|
||||
with _suppress(AttributeError):
|
||||
_obj.__module__ = 'bidict'
|
||||
|
||||
|
||||
# * Code review nav *
|
||||
# ============================================================================
|
||||
# Current: __init__.py Next: _abc.py →
|
||||
# ============================================================================
|
||||
@@ -0,0 +1,79 @@
|
||||
# Copyright 2009-2024 Joshua Bronson. All rights reserved.
|
||||
#
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
# * Code review nav *
|
||||
# (see comments in __init__.py)
|
||||
# ============================================================================
|
||||
# ← Prev: __init__.py Current: _abc.py Next: _base.py →
|
||||
# ============================================================================
|
||||
|
||||
|
||||
"""Provide the :class:`BidirectionalMapping` abstract base class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import typing as t
|
||||
from abc import abstractmethod
|
||||
|
||||
from ._typing import KT
|
||||
from ._typing import VT
|
||||
|
||||
|
||||
class BidirectionalMapping(t.Mapping[KT, VT]):
|
||||
"""Abstract base class for bidirectional mapping types.
|
||||
|
||||
Extends :class:`collections.abc.Mapping` primarily by adding the
|
||||
(abstract) :attr:`inverse` property,
|
||||
which implementers of :class:`BidirectionalMapping`
|
||||
should override to return a reference to the inverse
|
||||
:class:`BidirectionalMapping` instance.
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def inverse(self) -> BidirectionalMapping[VT, KT]:
|
||||
"""The inverse of this bidirectional mapping instance.
|
||||
|
||||
*See also* :attr:`bidict.BidictBase.inverse`, :attr:`bidict.BidictBase.inv`
|
||||
|
||||
:raises NotImplementedError: Meant to be overridden in subclasses.
|
||||
"""
|
||||
# The @abstractmethod decorator prevents subclasses from being instantiated unless they
|
||||
# override this method. But an overriding implementation may merely return super().inverse,
|
||||
# in which case this implementation is used. Raise NotImplementedError to indicate that
|
||||
# subclasses must actually provide their own implementation.
|
||||
raise NotImplementedError
|
||||
|
||||
def __inverted__(self) -> t.Iterator[tuple[VT, KT]]:
|
||||
"""Get an iterator over the items in :attr:`inverse`.
|
||||
|
||||
This is functionally equivalent to iterating over the items in the
|
||||
forward mapping and inverting each one on the fly, but this provides a
|
||||
more efficient implementation: Assuming the already-inverted items
|
||||
are stored in :attr:`inverse`, just return an iterator over them directly.
|
||||
|
||||
Providing this default implementation enables external functions,
|
||||
particularly :func:`~bidict.inverted`, to use this optimized
|
||||
implementation when available, instead of having to invert on the fly.
|
||||
|
||||
*See also* :func:`bidict.inverted`
|
||||
"""
|
||||
return iter(self.inverse.items())
|
||||
|
||||
|
||||
class MutableBidirectionalMapping(BidirectionalMapping[KT, VT], t.MutableMapping[KT, VT]):
|
||||
"""Abstract base class for mutable bidirectional mapping types."""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
|
||||
# * Code review nav *
|
||||
# ============================================================================
|
||||
# ← Prev: __init__.py Current: _abc.py Next: _base.py →
|
||||
# ============================================================================
|
||||
@@ -0,0 +1,556 @@
|
||||
# Copyright 2009-2024 Joshua Bronson. All rights reserved.
|
||||
#
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
# * Code review nav *
|
||||
# (see comments in __init__.py)
|
||||
# ============================================================================
|
||||
# ← Prev: _abc.py Current: _base.py Next: _frozen.py →
|
||||
# ============================================================================
|
||||
|
||||
|
||||
"""Provide :class:`BidictBase`."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import typing as t
|
||||
import weakref
|
||||
from itertools import starmap
|
||||
from operator import eq
|
||||
from types import MappingProxyType
|
||||
|
||||
from ._abc import BidirectionalMapping
|
||||
from ._dup import DROP_NEW
|
||||
from ._dup import DROP_OLD
|
||||
from ._dup import ON_DUP_DEFAULT
|
||||
from ._dup import RAISE
|
||||
from ._dup import OnDup
|
||||
from ._exc import DuplicationError
|
||||
from ._exc import KeyAndValueDuplicationError
|
||||
from ._exc import KeyDuplicationError
|
||||
from ._exc import ValueDuplicationError
|
||||
from ._iter import inverted
|
||||
from ._iter import iteritems
|
||||
from ._typing import KT
|
||||
from ._typing import MISSING
|
||||
from ._typing import OKT
|
||||
from ._typing import OVT
|
||||
from ._typing import VT
|
||||
from ._typing import Maplike
|
||||
from ._typing import MapOrItems
|
||||
|
||||
|
||||
OldKV = t.Tuple[OKT[KT], OVT[VT]]
|
||||
DedupResult = t.Optional[OldKV[KT, VT]]
|
||||
Unwrites = t.List[t.Tuple[t.Any, ...]]
|
||||
BT = t.TypeVar('BT', bound='BidictBase[t.Any, t.Any]')
|
||||
|
||||
|
||||
class BidictKeysView(t.KeysView[KT], t.ValuesView[KT]):
|
||||
"""Since the keys of a bidict are the values of its inverse (and vice versa),
|
||||
the :class:`~collections.abc.ValuesView` result of calling *bi.values()*
|
||||
is also a :class:`~collections.abc.KeysView` of *bi.inverse*.
|
||||
"""
|
||||
|
||||
|
||||
class BidictBase(BidirectionalMapping[KT, VT]):
|
||||
"""Base class implementing :class:`BidirectionalMapping`."""
|
||||
|
||||
#: The default :class:`~bidict.OnDup`
|
||||
#: that governs behavior when a provided item
|
||||
#: duplicates the key or value of other item(s).
|
||||
#:
|
||||
#: *See also*
|
||||
#: :ref:`basic-usage:Values Must Be Unique` (https://bidict.rtfd.io/basic-usage.html#values-must-be-unique),
|
||||
#: :doc:`extending` (https://bidict.rtfd.io/extending.html)
|
||||
on_dup = ON_DUP_DEFAULT
|
||||
|
||||
_fwdm: t.MutableMapping[KT, VT] #: the backing forward mapping (*key* → *val*)
|
||||
_invm: t.MutableMapping[VT, KT] #: the backing inverse mapping (*val* → *key*)
|
||||
|
||||
# Use Any rather than KT/VT in the following to avoid "ClassVar cannot contain type variables" errors:
|
||||
_fwdm_cls: t.ClassVar[type[t.MutableMapping[t.Any, t.Any]]] = dict #: class of the backing forward mapping
|
||||
_invm_cls: t.ClassVar[type[t.MutableMapping[t.Any, t.Any]]] = dict #: class of the backing inverse mapping
|
||||
|
||||
#: The class of the inverse bidict instance.
|
||||
_inv_cls: t.ClassVar[type[BidictBase[t.Any, t.Any]]]
|
||||
|
||||
def __init_subclass__(cls) -> None:
|
||||
super().__init_subclass__()
|
||||
cls._init_class()
|
||||
|
||||
@classmethod
|
||||
def _init_class(cls) -> None:
|
||||
cls._ensure_inv_cls()
|
||||
cls._set_reversed()
|
||||
|
||||
__reversed__: t.ClassVar[t.Any]
|
||||
|
||||
@classmethod
|
||||
def _set_reversed(cls) -> None:
|
||||
"""Set __reversed__ for subclasses that do not set it explicitly
|
||||
according to whether backing mappings are reversible.
|
||||
"""
|
||||
if cls is not BidictBase:
|
||||
resolved = cls.__reversed__
|
||||
overridden = resolved is not BidictBase.__reversed__
|
||||
if overridden: # E.g. OrderedBidictBase, OrderedBidict
|
||||
return
|
||||
backing_reversible = all(issubclass(i, t.Reversible) for i in (cls._fwdm_cls, cls._invm_cls))
|
||||
cls.__reversed__ = _fwdm_reversed if backing_reversible else None
|
||||
|
||||
@classmethod
|
||||
def _ensure_inv_cls(cls) -> None:
|
||||
"""Ensure :attr:`_inv_cls` is set, computing it dynamically if necessary.
|
||||
|
||||
All subclasses provided in :mod:`bidict` are their own inverse classes,
|
||||
i.e., their backing forward and inverse mappings are both the same type,
|
||||
but users may define subclasses where this is not the case.
|
||||
This method ensures that the inverse class is computed correctly regardless.
|
||||
|
||||
See: :ref:`extending:Dynamic Inverse Class Generation`
|
||||
(https://bidict.rtfd.io/extending.html#dynamic-inverse-class-generation)
|
||||
"""
|
||||
# This _ensure_inv_cls() method is (indirectly) corecursive with _make_inv_cls() below
|
||||
# in the case that we need to dynamically generate the inverse class:
|
||||
# 1. _ensure_inv_cls() calls cls._make_inv_cls()
|
||||
# 2. cls._make_inv_cls() calls type(..., (cls, ...), ...) to dynamically generate inv_cls
|
||||
# 3. Our __init_subclass__ hook (see above) is automatically called on inv_cls
|
||||
# 4. inv_cls.__init_subclass__() calls inv_cls._ensure_inv_cls()
|
||||
# 5. inv_cls._ensure_inv_cls() resolves to this implementation
|
||||
# (inv_cls deliberately does not override this), so we're back where we started.
|
||||
# But since the _make_inv_cls() call will have set inv_cls.__dict__._inv_cls,
|
||||
# just check if it's already set before calling _make_inv_cls() to prevent infinite recursion.
|
||||
if getattr(cls, '__dict__', {}).get('_inv_cls'): # Don't assume cls.__dict__ (e.g. mypyc native class)
|
||||
return
|
||||
cls._inv_cls = cls._make_inv_cls()
|
||||
|
||||
@classmethod
|
||||
def _make_inv_cls(cls: type[BT]) -> type[BT]:
|
||||
diff = cls._inv_cls_dict_diff()
|
||||
cls_is_own_inv = all(getattr(cls, k, MISSING) == v for (k, v) in diff.items())
|
||||
if cls_is_own_inv:
|
||||
return cls
|
||||
# Suppress auto-calculation of _inv_cls's _inv_cls since we know it already.
|
||||
# Works with the guard in BidictBase._ensure_inv_cls() to prevent infinite recursion.
|
||||
diff['_inv_cls'] = cls
|
||||
inv_cls = type(f'{cls.__name__}Inv', (cls, GeneratedBidictInverse), diff)
|
||||
inv_cls.__module__ = cls.__module__
|
||||
return t.cast(t.Type[BT], inv_cls)
|
||||
|
||||
@classmethod
|
||||
def _inv_cls_dict_diff(cls) -> dict[str, t.Any]:
|
||||
return {
|
||||
'_fwdm_cls': cls._invm_cls,
|
||||
'_invm_cls': cls._fwdm_cls,
|
||||
}
|
||||
|
||||
def __init__(self, arg: MapOrItems[KT, VT] = (), /, **kw: VT) -> None:
|
||||
"""Make a new bidirectional mapping.
|
||||
The signature behaves like that of :class:`dict`.
|
||||
ktems passed via positional arg are processed first,
|
||||
followed by any items passed via keyword argument.
|
||||
Any duplication encountered along the way
|
||||
is handled as per :attr:`on_dup`.
|
||||
"""
|
||||
self._fwdm = self._fwdm_cls()
|
||||
self._invm = self._invm_cls()
|
||||
self._update(arg, kw, rollback=False)
|
||||
|
||||
# If Python ever adds support for higher-kinded types, `inverse` could use them, e.g.
|
||||
# def inverse(self: BT[KT, VT]) -> BT[VT, KT]:
|
||||
# Ref: https://github.com/python/typing/issues/548#issuecomment-621571821
|
||||
@property
|
||||
def inverse(self) -> BidictBase[VT, KT]:
|
||||
"""The inverse of this bidirectional mapping instance."""
|
||||
# When `bi.inverse` is called for the first time, this method
|
||||
# computes the inverse instance, stores it for subsequent use, and then
|
||||
# returns it. It also stores a reference on `bi.inverse` back to `bi`,
|
||||
# but uses a weakref to avoid creating a reference cycle. Strong references
|
||||
# to inverse instances are stored in ._inv, and weak references are stored
|
||||
# in ._invweak.
|
||||
|
||||
# First check if a strong reference is already stored.
|
||||
inv: BidictBase[VT, KT] | None = getattr(self, '_inv', None)
|
||||
if inv is not None:
|
||||
return inv
|
||||
# Next check if a weak reference is already stored.
|
||||
invweak = getattr(self, '_invweak', None)
|
||||
if invweak is not None:
|
||||
inv = invweak() # Try to resolve a strong reference and return it.
|
||||
if inv is not None:
|
||||
return inv
|
||||
# No luck. Compute the inverse reference and store it for subsequent use.
|
||||
inv = self._make_inverse()
|
||||
self._inv: BidictBase[VT, KT] | None = inv
|
||||
self._invweak: weakref.ReferenceType[BidictBase[VT, KT]] | None = None
|
||||
# Also store a weak reference back to `instance` on its inverse instance, so that
|
||||
# the second `.inverse` access in `bi.inverse.inverse` hits the cached weakref.
|
||||
inv._inv = None
|
||||
inv._invweak = weakref.ref(self)
|
||||
# In e.g. `bidict().inverse.inverse`, this design ensures that a strong reference
|
||||
# back to the original instance is retained before its refcount drops to zero,
|
||||
# avoiding an unintended potential deallocation.
|
||||
return inv
|
||||
|
||||
def _make_inverse(self) -> BidictBase[VT, KT]:
|
||||
inv: BidictBase[VT, KT] = self._inv_cls()
|
||||
inv._fwdm = self._invm
|
||||
inv._invm = self._fwdm
|
||||
return inv
|
||||
|
||||
@property
|
||||
def inv(self) -> BidictBase[VT, KT]:
|
||||
"""Alias for :attr:`inverse`."""
|
||||
return self.inverse
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""See :func:`repr`."""
|
||||
clsname = self.__class__.__name__
|
||||
items = dict(self.items()) if self else ''
|
||||
return f'{clsname}({items})'
|
||||
|
||||
def values(self) -> BidictKeysView[VT]:
|
||||
"""A set-like object providing a view on the contained values.
|
||||
|
||||
Since the values of a bidict are equivalent to the keys of its inverse,
|
||||
this method returns a set-like object for this bidict's values
|
||||
rather than just a collections.abc.ValuesView.
|
||||
This object supports set operations like union and difference,
|
||||
and constant- rather than linear-time containment checks,
|
||||
and is no more expensive to provide than the less capable
|
||||
collections.abc.ValuesView would be.
|
||||
|
||||
See :meth:`keys` for more information.
|
||||
"""
|
||||
return t.cast(BidictKeysView[VT], self.inverse.keys())
|
||||
|
||||
def keys(self) -> t.KeysView[KT]:
|
||||
"""A set-like object providing a view on the contained keys.
|
||||
|
||||
When *b._fwdm* is a :class:`dict`, *b.keys()* returns a
|
||||
*dict_keys* object that behaves exactly the same as
|
||||
*collections.abc.KeysView(b)*, except for
|
||||
|
||||
- offering better performance
|
||||
|
||||
- being reversible on Python 3.8+
|
||||
|
||||
- having a .mapping attribute in Python 3.10+
|
||||
that exposes a mappingproxy to *b._fwdm*.
|
||||
"""
|
||||
fwdm, fwdm_cls = self._fwdm, self._fwdm_cls
|
||||
return fwdm.keys() if fwdm_cls is dict else BidictKeysView(self)
|
||||
|
||||
def items(self) -> t.ItemsView[KT, VT]:
|
||||
"""A set-like object providing a view on the contained items.
|
||||
|
||||
When *b._fwdm* is a :class:`dict`, *b.items()* returns a
|
||||
*dict_items* object that behaves exactly the same as
|
||||
*collections.abc.ItemsView(b)*, except for:
|
||||
|
||||
- offering better performance
|
||||
|
||||
- being reversible on Python 3.8+
|
||||
|
||||
- having a .mapping attribute in Python 3.10+
|
||||
that exposes a mappingproxy to *b._fwdm*.
|
||||
"""
|
||||
return self._fwdm.items() if self._fwdm_cls is dict else super().items()
|
||||
|
||||
# The inherited collections.abc.Mapping.__contains__() method is implemented by doing a `try`
|
||||
# `except KeyError` around `self[key]`. The following implementation is much faster,
|
||||
# especially in the missing case.
|
||||
def __contains__(self, key: t.Any) -> bool:
|
||||
"""True if the mapping contains the specified key, else False."""
|
||||
return key in self._fwdm
|
||||
|
||||
# The inherited collections.abc.Mapping.__eq__() method is implemented in terms of an inefficient
|
||||
# `dict(self.items()) == dict(other.items())` comparison, so override it with a
|
||||
# more efficient implementation.
|
||||
def __eq__(self, other: object) -> bool:
|
||||
"""*x.__eq__(other) ⟺ x == other*
|
||||
|
||||
Equivalent to *dict(x.items()) == dict(other.items())*
|
||||
but more efficient.
|
||||
|
||||
Note that :meth:`bidict's __eq__() <bidict.BidictBase.__eq__>` implementation
|
||||
is inherited by subclasses,
|
||||
in particular by the ordered bidict subclasses,
|
||||
so even with ordered bidicts,
|
||||
:ref:`== comparison is order-insensitive <eq-order-insensitive>`
|
||||
(https://bidict.rtfd.io/other-bidict-types.html#eq-is-order-insensitive).
|
||||
|
||||
*See also* :meth:`equals_order_sensitive`
|
||||
"""
|
||||
if isinstance(other, t.Mapping):
|
||||
return self._fwdm.items() == other.items()
|
||||
# Ref: https://docs.python.org/3/library/constants.html#NotImplemented
|
||||
return NotImplemented
|
||||
|
||||
def equals_order_sensitive(self, other: object) -> bool:
|
||||
"""Order-sensitive equality check.
|
||||
|
||||
*See also* :ref:`eq-order-insensitive`
|
||||
(https://bidict.rtfd.io/other-bidict-types.html#eq-is-order-insensitive)
|
||||
"""
|
||||
if not isinstance(other, t.Mapping) or len(self) != len(other):
|
||||
return False
|
||||
return all(starmap(eq, zip(self.items(), other.items())))
|
||||
|
||||
def _dedup(self, key: KT, val: VT, on_dup: OnDup) -> DedupResult[KT, VT]:
|
||||
"""Check *key* and *val* for any duplication in self.
|
||||
|
||||
Handle any duplication as per the passed in *on_dup*.
|
||||
|
||||
If (key, val) is already present, return None
|
||||
since writing (key, val) would be a no-op.
|
||||
|
||||
If duplication is found and the corresponding :class:`~bidict.OnDupAction` is
|
||||
:attr:`~bidict.DROP_NEW`, return None.
|
||||
|
||||
If duplication is found and the corresponding :class:`~bidict.OnDupAction` is
|
||||
:attr:`~bidict.RAISE`, raise the appropriate exception.
|
||||
|
||||
If duplication is found and the corresponding :class:`~bidict.OnDupAction` is
|
||||
:attr:`~bidict.DROP_OLD`, or if no duplication is found,
|
||||
return *(oldkey, oldval)*.
|
||||
"""
|
||||
fwdm, invm = self._fwdm, self._invm
|
||||
oldval: OVT[VT] = fwdm.get(key, MISSING)
|
||||
oldkey: OKT[KT] = invm.get(val, MISSING)
|
||||
isdupkey, isdupval = oldval is not MISSING, oldkey is not MISSING
|
||||
if isdupkey and isdupval:
|
||||
if key == oldkey:
|
||||
assert val == oldval
|
||||
# (key, val) duplicates an existing item -> no-op.
|
||||
return None
|
||||
# key and val each duplicate a different existing item.
|
||||
if on_dup.val is RAISE:
|
||||
raise KeyAndValueDuplicationError(key, val)
|
||||
if on_dup.val is DROP_NEW:
|
||||
return None
|
||||
assert on_dup.val is DROP_OLD
|
||||
# Fall through to the return statement on the last line.
|
||||
elif isdupkey:
|
||||
if on_dup.key is RAISE:
|
||||
raise KeyDuplicationError(key)
|
||||
if on_dup.key is DROP_NEW:
|
||||
return None
|
||||
assert on_dup.key is DROP_OLD
|
||||
# Fall through to the return statement on the last line.
|
||||
elif isdupval:
|
||||
if on_dup.val is RAISE:
|
||||
raise ValueDuplicationError(val)
|
||||
if on_dup.val is DROP_NEW:
|
||||
return None
|
||||
assert on_dup.val is DROP_OLD
|
||||
# Fall through to the return statement on the last line.
|
||||
# else neither isdupkey nor isdupval.
|
||||
return oldkey, oldval
|
||||
|
||||
def _write(self, newkey: KT, newval: VT, oldkey: OKT[KT], oldval: OVT[VT], unwrites: Unwrites | None) -> None:
|
||||
"""Insert (newkey, newval), extending *unwrites* with associated inverse operations if provided.
|
||||
|
||||
*oldkey* and *oldval* are as returned by :meth:`_dedup`.
|
||||
|
||||
If *unwrites* is not None, it is extended with the inverse operations necessary to undo the write.
|
||||
This design allows :meth:`_update` to roll back a partially applied update that fails part-way through
|
||||
when necessary.
|
||||
|
||||
This design also allows subclasses that require additional operations to easily extend this implementation.
|
||||
For example, :class:`bidict.OrderedBidictBase` calls this inherited implementation, and then extends *unwrites*
|
||||
with additional operations needed to keep its internal linked list nodes consistent with its items' order
|
||||
as changes are made.
|
||||
"""
|
||||
fwdm, invm = self._fwdm, self._invm
|
||||
fwdm_set, invm_set = fwdm.__setitem__, invm.__setitem__
|
||||
fwdm_del, invm_del = fwdm.__delitem__, invm.__delitem__
|
||||
# Always perform the following writes regardless of duplication.
|
||||
fwdm_set(newkey, newval)
|
||||
invm_set(newval, newkey)
|
||||
if oldval is MISSING and oldkey is MISSING: # no key or value duplication
|
||||
# {0: 1, 2: 3} | {4: 5} => {0: 1, 2: 3, 4: 5}
|
||||
if unwrites is not None:
|
||||
unwrites.extend((
|
||||
(fwdm_del, newkey),
|
||||
(invm_del, newval),
|
||||
))
|
||||
elif oldval is not MISSING and oldkey is not MISSING: # key and value duplication across two different items
|
||||
# {0: 1, 2: 3} | {0: 3} => {0: 3}
|
||||
fwdm_del(oldkey)
|
||||
invm_del(oldval)
|
||||
if unwrites is not None:
|
||||
unwrites.extend((
|
||||
(fwdm_set, newkey, oldval),
|
||||
(invm_set, oldval, newkey),
|
||||
(fwdm_set, oldkey, newval),
|
||||
(invm_set, newval, oldkey),
|
||||
))
|
||||
elif oldval is not MISSING: # just key duplication
|
||||
# {0: 1, 2: 3} | {2: 4} => {0: 1, 2: 4}
|
||||
invm_del(oldval)
|
||||
if unwrites is not None:
|
||||
unwrites.extend((
|
||||
(fwdm_set, newkey, oldval),
|
||||
(invm_set, oldval, newkey),
|
||||
(invm_del, newval),
|
||||
))
|
||||
else:
|
||||
assert oldkey is not MISSING # just value duplication
|
||||
# {0: 1, 2: 3} | {4: 3} => {0: 1, 4: 3}
|
||||
fwdm_del(oldkey)
|
||||
if unwrites is not None:
|
||||
unwrites.extend((
|
||||
(fwdm_set, oldkey, newval),
|
||||
(invm_set, newval, oldkey),
|
||||
(fwdm_del, newkey),
|
||||
))
|
||||
|
||||
def _update(
|
||||
self,
|
||||
arg: MapOrItems[KT, VT],
|
||||
kw: t.Mapping[str, VT] = MappingProxyType({}),
|
||||
*,
|
||||
rollback: bool | None = None,
|
||||
on_dup: OnDup | None = None,
|
||||
) -> None:
|
||||
"""Update with the items from *arg* and *kw*, maybe failing and rolling back as per *on_dup* and *rollback*."""
|
||||
# Note: We must process input in a single pass, since arg may be a generator.
|
||||
if not isinstance(arg, (t.Iterable, Maplike)):
|
||||
raise TypeError(f"'{arg.__class__.__name__}' object is not iterable")
|
||||
if not arg and not kw:
|
||||
return
|
||||
if on_dup is None:
|
||||
on_dup = self.on_dup
|
||||
if rollback is None:
|
||||
rollback = RAISE in on_dup
|
||||
|
||||
# Fast path when we're empty and updating only from another bidict (i.e. no dup vals in new items).
|
||||
if not self and not kw and isinstance(arg, BidictBase):
|
||||
self._init_from(arg)
|
||||
return
|
||||
|
||||
# Fast path when we're adding more items than we contain already and rollback is enabled:
|
||||
# Update a copy of self with rollback disabled. Fail if that fails, otherwise become the copy.
|
||||
if rollback and isinstance(arg, t.Sized) and len(arg) + len(kw) > len(self):
|
||||
tmp = self.copy()
|
||||
tmp._update(arg, kw, rollback=False, on_dup=on_dup)
|
||||
self._init_from(tmp)
|
||||
return
|
||||
|
||||
# In all other cases, benchmarking has indicated that the update is best implemented as follows:
|
||||
# For each new item, perform a dup check (raising if necessary), and apply the associated writes we need to
|
||||
# perform on our backing _fwdm and _invm mappings. If rollback is enabled, also compute the associated unwrites
|
||||
# as we go. If the update results in a DuplicationError and rollback is enabled, apply the accumulated unwrites
|
||||
# before raising, to ensure that we fail clean.
|
||||
write = self._write
|
||||
unwrites: Unwrites | None = [] if rollback else None
|
||||
for key, val in iteritems(arg, **kw):
|
||||
try:
|
||||
dedup_result = self._dedup(key, val, on_dup)
|
||||
except DuplicationError:
|
||||
if unwrites is not None:
|
||||
for fn, *args in reversed(unwrites):
|
||||
fn(*args)
|
||||
raise
|
||||
if dedup_result is not None:
|
||||
write(key, val, *dedup_result, unwrites=unwrites)
|
||||
|
||||
def __copy__(self: BT) -> BT:
|
||||
"""Used for the copy protocol. See the :mod:`copy` module."""
|
||||
return self.copy()
|
||||
|
||||
def copy(self: BT) -> BT:
|
||||
"""Make a (shallow) copy of this bidict."""
|
||||
# Could just `return self.__class__(self)` here, but the below is faster. The former
|
||||
# would copy this bidict's items into a new instance one at a time (checking for duplication
|
||||
# for each item), whereas the below copies from the backing mappings all at once, and foregoes
|
||||
# item-by-item duplication checking since the backing mappings have been checked already.
|
||||
return self._from_other(self.__class__, self)
|
||||
|
||||
@staticmethod
|
||||
def _from_other(bt: type[BT], other: MapOrItems[KT, VT], inv: bool = False) -> BT:
|
||||
"""Fast, private constructor based on :meth:`_init_from`.
|
||||
|
||||
If *inv* is true, return the inverse of the instance instead of the instance itself.
|
||||
(Useful for pickling with dynamically-generated inverse classes -- see :meth:`__reduce__`.)
|
||||
"""
|
||||
inst = bt()
|
||||
inst._init_from(other)
|
||||
return t.cast(BT, inst.inverse) if inv else inst
|
||||
|
||||
def _init_from(self, other: MapOrItems[KT, VT]) -> None:
|
||||
"""Fast init from *other*, bypassing item-by-item duplication checking."""
|
||||
self._fwdm.clear()
|
||||
self._invm.clear()
|
||||
self._fwdm.update(other)
|
||||
# If other is a bidict, use its existing backing inverse mapping, otherwise
|
||||
# other could be a generator that's now exhausted, so invert self._fwdm on the fly.
|
||||
inv = other.inverse if isinstance(other, BidictBase) else inverted(self._fwdm)
|
||||
self._invm.update(inv)
|
||||
|
||||
# other's type is Mapping rather than Maplike since bidict() | SupportsKeysAndGetItem({})
|
||||
# raises a TypeError, just like dict() | SupportsKeysAndGetItem({}) does.
|
||||
def __or__(self: BT, other: t.Mapping[KT, VT]) -> BT:
|
||||
"""Return self|other."""
|
||||
if not isinstance(other, t.Mapping):
|
||||
return NotImplemented
|
||||
new = self.copy()
|
||||
new._update(other, rollback=False)
|
||||
return new
|
||||
|
||||
def __ror__(self: BT, other: t.Mapping[KT, VT]) -> BT:
|
||||
"""Return other|self."""
|
||||
if not isinstance(other, t.Mapping):
|
||||
return NotImplemented
|
||||
new = self.__class__(other)
|
||||
new._update(self, rollback=False)
|
||||
return new
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""The number of contained items."""
|
||||
return len(self._fwdm)
|
||||
|
||||
def __iter__(self) -> t.Iterator[KT]:
|
||||
"""Iterator over the contained keys."""
|
||||
return iter(self._fwdm)
|
||||
|
||||
def __getitem__(self, key: KT) -> VT:
|
||||
"""*x.__getitem__(key) ⟺ x[key]*"""
|
||||
return self._fwdm[key]
|
||||
|
||||
def __reduce__(self) -> tuple[t.Any, ...]:
|
||||
"""Return state information for pickling."""
|
||||
cls = self.__class__
|
||||
inst: t.Mapping[t.Any, t.Any] = self
|
||||
# If this bidict's class is dynamically generated, pickle the inverse instead, whose (presumably not
|
||||
# dynamically generated) class the caller is more likely to have a reference to somewhere in sys.modules
|
||||
# that pickle can discover.
|
||||
if should_invert := isinstance(self, GeneratedBidictInverse):
|
||||
cls = self._inv_cls
|
||||
inst = self.inverse
|
||||
return self._from_other, (cls, dict(inst), should_invert)
|
||||
|
||||
|
||||
# See BidictBase._set_reversed() above.
|
||||
def _fwdm_reversed(self: BidictBase[KT, t.Any]) -> t.Iterator[KT]:
|
||||
"""Iterator over the contained keys in reverse order."""
|
||||
assert isinstance(self._fwdm, t.Reversible)
|
||||
return reversed(self._fwdm)
|
||||
|
||||
|
||||
BidictBase._init_class()
|
||||
|
||||
|
||||
class GeneratedBidictInverse:
|
||||
"""Base class for dynamically-generated inverse bidict classes."""
|
||||
|
||||
|
||||
# * Code review nav *
|
||||
# ============================================================================
|
||||
# ← Prev: _abc.py Current: _base.py Next: _frozen.py →
|
||||
# ============================================================================
|
||||
@@ -0,0 +1,194 @@
|
||||
# Copyright 2009-2024 Joshua Bronson. All rights reserved.
|
||||
#
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
# * Code review nav *
|
||||
# (see comments in __init__.py)
|
||||
# ============================================================================
|
||||
# ← Prev: _frozen.py Current: _bidict.py Next: _orderedbase.py →
|
||||
# ============================================================================
|
||||
|
||||
|
||||
"""Provide :class:`MutableBidict` and :class:`bidict`."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import typing as t
|
||||
|
||||
from ._abc import MutableBidirectionalMapping
|
||||
from ._base import BidictBase
|
||||
from ._dup import ON_DUP_DROP_OLD
|
||||
from ._dup import ON_DUP_RAISE
|
||||
from ._dup import OnDup
|
||||
from ._typing import DT
|
||||
from ._typing import KT
|
||||
from ._typing import MISSING
|
||||
from ._typing import ODT
|
||||
from ._typing import VT
|
||||
from ._typing import MapOrItems
|
||||
|
||||
|
||||
class MutableBidict(BidictBase[KT, VT], MutableBidirectionalMapping[KT, VT]):
|
||||
"""Base class for mutable bidirectional mappings."""
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
|
||||
@property
|
||||
def inverse(self) -> MutableBidict[VT, KT]: ...
|
||||
|
||||
@property
|
||||
def inv(self) -> MutableBidict[VT, KT]: ...
|
||||
|
||||
def _pop(self, key: KT) -> VT:
|
||||
val = self._fwdm.pop(key)
|
||||
del self._invm[val]
|
||||
return val
|
||||
|
||||
def __delitem__(self, key: KT) -> None:
|
||||
"""*x.__delitem__(y) ⟺ del x[y]*"""
|
||||
self._pop(key)
|
||||
|
||||
def __setitem__(self, key: KT, val: VT) -> None:
|
||||
"""Set the value for *key* to *val*.
|
||||
|
||||
If *key* is already associated with *val*, this is a no-op.
|
||||
|
||||
If *key* is already associated with a different value,
|
||||
the old value will be replaced with *val*,
|
||||
as with dict's :meth:`__setitem__`.
|
||||
|
||||
If *val* is already associated with a different key,
|
||||
an exception is raised
|
||||
to protect against accidental removal of the key
|
||||
that's currently associated with *val*.
|
||||
|
||||
Use :meth:`put` instead if you want to specify different behavior in
|
||||
the case that the provided key or value duplicates an existing one.
|
||||
Or use :meth:`forceput` to unconditionally associate *key* with *val*,
|
||||
replacing any existing items as necessary to preserve uniqueness.
|
||||
|
||||
:raises bidict.ValueDuplicationError: if *val* duplicates that of an
|
||||
existing item.
|
||||
|
||||
:raises bidict.KeyAndValueDuplicationError: if *key* duplicates the key of an
|
||||
existing item and *val* duplicates the value of a different
|
||||
existing item.
|
||||
"""
|
||||
self.put(key, val, on_dup=self.on_dup)
|
||||
|
||||
def put(self, key: KT, val: VT, on_dup: OnDup = ON_DUP_RAISE) -> None:
|
||||
"""Associate *key* with *val*, honoring the :class:`OnDup` given in *on_dup*.
|
||||
|
||||
For example, if *on_dup* is :attr:`~bidict.ON_DUP_RAISE`,
|
||||
then *key* will be associated with *val* if and only if
|
||||
*key* is not already associated with an existing value and
|
||||
*val* is not already associated with an existing key,
|
||||
otherwise an exception will be raised.
|
||||
|
||||
If *key* is already associated with *val*, this is a no-op.
|
||||
|
||||
:raises bidict.KeyDuplicationError: if attempting to insert an item
|
||||
whose key only duplicates an existing item's, and *on_dup.key* is
|
||||
:attr:`~bidict.RAISE`.
|
||||
|
||||
:raises bidict.ValueDuplicationError: if attempting to insert an item
|
||||
whose value only duplicates an existing item's, and *on_dup.val* is
|
||||
:attr:`~bidict.RAISE`.
|
||||
|
||||
:raises bidict.KeyAndValueDuplicationError: if attempting to insert an
|
||||
item whose key duplicates one existing item's, and whose value
|
||||
duplicates another existing item's, and *on_dup.val* is
|
||||
:attr:`~bidict.RAISE`.
|
||||
"""
|
||||
self._update(((key, val),), on_dup=on_dup)
|
||||
|
||||
def forceput(self, key: KT, val: VT) -> None:
|
||||
"""Associate *key* with *val* unconditionally.
|
||||
|
||||
Replace any existing mappings containing key *key* or value *val*
|
||||
as necessary to preserve uniqueness.
|
||||
"""
|
||||
self.put(key, val, on_dup=ON_DUP_DROP_OLD)
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Remove all items."""
|
||||
self._fwdm.clear()
|
||||
self._invm.clear()
|
||||
|
||||
@t.overload
|
||||
def pop(self, key: KT, /) -> VT: ...
|
||||
@t.overload
|
||||
def pop(self, key: KT, default: DT = ..., /) -> VT | DT: ...
|
||||
|
||||
def pop(self, key: KT, default: ODT[DT] = MISSING, /) -> VT | DT:
|
||||
"""*x.pop(k[, d]) → v*
|
||||
|
||||
Remove specified key and return the corresponding value.
|
||||
|
||||
:raises KeyError: if *key* is not found and no *default* is provided.
|
||||
"""
|
||||
try:
|
||||
return self._pop(key)
|
||||
except KeyError:
|
||||
if default is MISSING:
|
||||
raise
|
||||
return default
|
||||
|
||||
def popitem(self) -> tuple[KT, VT]:
|
||||
"""*x.popitem() → (k, v)*
|
||||
|
||||
Remove and return some item as a (key, value) pair.
|
||||
|
||||
:raises KeyError: if *x* is empty.
|
||||
"""
|
||||
key, val = self._fwdm.popitem()
|
||||
del self._invm[val]
|
||||
return key, val
|
||||
|
||||
def update(self, arg: MapOrItems[KT, VT] = (), /, **kw: VT) -> None:
|
||||
"""Like calling :meth:`putall` with *self.on_dup* passed for *on_dup*."""
|
||||
self._update(arg, kw=kw)
|
||||
|
||||
def forceupdate(self, arg: MapOrItems[KT, VT] = (), /, **kw: VT) -> None:
|
||||
"""Like a bulk :meth:`forceput`."""
|
||||
self._update(arg, kw=kw, on_dup=ON_DUP_DROP_OLD)
|
||||
|
||||
def putall(self, items: MapOrItems[KT, VT], on_dup: OnDup = ON_DUP_RAISE) -> None:
|
||||
"""Like a bulk :meth:`put`.
|
||||
|
||||
If one of the given items causes an exception to be raised,
|
||||
none of the items is inserted.
|
||||
"""
|
||||
self._update(items, on_dup=on_dup)
|
||||
|
||||
# other's type is Mapping rather than Maplike since bidict() |= SupportsKeysAndGetItem({})
|
||||
# raises a TypeError, just like dict() |= SupportsKeysAndGetItem({}) does.
|
||||
def __ior__(self, other: t.Mapping[KT, VT]) -> MutableBidict[KT, VT]:
|
||||
"""Return self|=other."""
|
||||
self.update(other)
|
||||
return self
|
||||
|
||||
|
||||
class bidict(MutableBidict[KT, VT]):
|
||||
"""The main bidirectional mapping type.
|
||||
|
||||
See :ref:`intro:Introduction` and :ref:`basic-usage:Basic Usage`
|
||||
to get started (also available at https://bidict.rtfd.io).
|
||||
"""
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
|
||||
@property
|
||||
def inverse(self) -> bidict[VT, KT]: ...
|
||||
|
||||
@property
|
||||
def inv(self) -> bidict[VT, KT]: ...
|
||||
|
||||
|
||||
# * Code review nav *
|
||||
# ============================================================================
|
||||
# ← Prev: _frozen.py Current: _bidict.py Next: _orderedbase.py →
|
||||
# ============================================================================
|
||||
@@ -0,0 +1,61 @@
|
||||
# Copyright 2009-2024 Joshua Bronson. All rights reserved.
|
||||
#
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
"""Provide :class:`OnDup` and related functionality."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import typing as t
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class OnDupAction(Enum):
|
||||
"""An action to take to prevent duplication from occurring."""
|
||||
|
||||
#: Raise a :class:`~bidict.DuplicationError`.
|
||||
RAISE = 'RAISE'
|
||||
#: Overwrite existing items with new items.
|
||||
DROP_OLD = 'DROP_OLD'
|
||||
#: Keep existing items and drop new items.
|
||||
DROP_NEW = 'DROP_NEW'
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'{self.__class__.__name__}.{self.name}'
|
||||
|
||||
|
||||
RAISE: t.Final[OnDupAction] = OnDupAction.RAISE
|
||||
DROP_OLD: t.Final[OnDupAction] = OnDupAction.DROP_OLD
|
||||
DROP_NEW: t.Final[OnDupAction] = OnDupAction.DROP_NEW
|
||||
|
||||
|
||||
class OnDup(t.NamedTuple):
|
||||
r"""A combination of :class:`~bidict.OnDupAction`\s specifying how to handle various types of duplication.
|
||||
|
||||
The :attr:`~OnDup.key` field specifies what action to take when a duplicate key is encountered.
|
||||
|
||||
The :attr:`~OnDup.val` field specifies what action to take when a duplicate value is encountered.
|
||||
|
||||
In the case of both key and value duplication across two different items,
|
||||
only :attr:`~OnDup.val` is used.
|
||||
|
||||
*See also* :ref:`basic-usage:Values Must Be Unique`
|
||||
(https://bidict.rtfd.io/basic-usage.html#values-must-be-unique)
|
||||
"""
|
||||
|
||||
key: OnDupAction = DROP_OLD
|
||||
val: OnDupAction = RAISE
|
||||
|
||||
|
||||
#: Default :class:`OnDup` used for the
|
||||
#: :meth:`~bidict.bidict.__init__`,
|
||||
#: :meth:`~bidict.bidict.__setitem__`, and
|
||||
#: :meth:`~bidict.bidict.update` methods.
|
||||
ON_DUP_DEFAULT: t.Final[OnDup] = OnDup(key=DROP_OLD, val=RAISE)
|
||||
#: An :class:`OnDup` whose members are all :obj:`RAISE`.
|
||||
ON_DUP_RAISE: t.Final[OnDup] = OnDup(key=RAISE, val=RAISE)
|
||||
#: An :class:`OnDup` whose members are all :obj:`DROP_OLD`.
|
||||
ON_DUP_DROP_OLD: t.Final[OnDup] = OnDup(key=DROP_OLD, val=DROP_OLD)
|
||||
@@ -0,0 +1,36 @@
|
||||
# Copyright 2009-2024 Joshua Bronson. All rights reserved.
|
||||
#
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
"""Provide all bidict exceptions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
class BidictException(Exception):
|
||||
"""Base class for bidict exceptions."""
|
||||
|
||||
|
||||
class DuplicationError(BidictException):
|
||||
"""Base class for exceptions raised when uniqueness is violated
|
||||
as per the :attr:`~bidict.RAISE` :class:`~bidict.OnDupAction`.
|
||||
"""
|
||||
|
||||
|
||||
class KeyDuplicationError(DuplicationError):
|
||||
"""Raised when a given key is not unique."""
|
||||
|
||||
|
||||
class ValueDuplicationError(DuplicationError):
|
||||
"""Raised when a given value is not unique."""
|
||||
|
||||
|
||||
class KeyAndValueDuplicationError(KeyDuplicationError, ValueDuplicationError):
|
||||
"""Raised when a given item's key and value are not unique.
|
||||
|
||||
That is, its key duplicates that of another item,
|
||||
and its value duplicates that of a different other item.
|
||||
"""
|
||||
@@ -0,0 +1,50 @@
|
||||
# Copyright 2009-2024 Joshua Bronson. All rights reserved.
|
||||
#
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
# * Code review nav *
|
||||
# (see comments in __init__.py)
|
||||
# ============================================================================
|
||||
# ← Prev: _base.py Current: _frozen.py Next: _bidict.py →
|
||||
# ============================================================================
|
||||
|
||||
"""Provide :class:`frozenbidict`, an immutable, hashable bidirectional mapping type."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import typing as t
|
||||
|
||||
from ._base import BidictBase
|
||||
from ._typing import KT
|
||||
from ._typing import VT
|
||||
|
||||
|
||||
class frozenbidict(BidictBase[KT, VT]):
|
||||
"""Immutable, hashable bidict type."""
|
||||
|
||||
_hash: int
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
|
||||
@property
|
||||
def inverse(self) -> frozenbidict[VT, KT]: ...
|
||||
|
||||
@property
|
||||
def inv(self) -> frozenbidict[VT, KT]: ...
|
||||
|
||||
def __hash__(self) -> int:
|
||||
"""The hash of this bidict as determined by its items."""
|
||||
if getattr(self, '_hash', None) is None:
|
||||
# The following is like hash(frozenset(self.items()))
|
||||
# but more memory efficient. See also: https://bugs.python.org/issue46684
|
||||
self._hash = t.ItemsView(self)._hash()
|
||||
return self._hash
|
||||
|
||||
|
||||
# * Code review nav *
|
||||
# ============================================================================
|
||||
# ← Prev: _base.py Current: _frozen.py Next: _bidict.py →
|
||||
# ============================================================================
|
||||
@@ -0,0 +1,51 @@
|
||||
# Copyright 2009-2024 Joshua Bronson. All rights reserved.
|
||||
#
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
"""Functions for iterating over items in a mapping."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import typing as t
|
||||
from operator import itemgetter
|
||||
|
||||
from ._typing import KT
|
||||
from ._typing import VT
|
||||
from ._typing import ItemsIter
|
||||
from ._typing import Maplike
|
||||
from ._typing import MapOrItems
|
||||
|
||||
|
||||
def iteritems(arg: MapOrItems[KT, VT] = (), /, **kw: VT) -> ItemsIter[KT, VT]:
|
||||
"""Yield the items from *arg* and *kw* in the order given."""
|
||||
if isinstance(arg, t.Mapping):
|
||||
yield from arg.items()
|
||||
elif isinstance(arg, Maplike):
|
||||
yield from ((k, arg[k]) for k in arg.keys())
|
||||
else:
|
||||
yield from arg
|
||||
yield from t.cast(ItemsIter[KT, VT], kw.items())
|
||||
|
||||
|
||||
swap: t.Final = itemgetter(1, 0)
|
||||
|
||||
|
||||
def inverted(arg: MapOrItems[KT, VT]) -> ItemsIter[VT, KT]:
|
||||
"""Yield the inverse items of the provided object.
|
||||
|
||||
If *arg* has a :func:`callable` ``__inverted__`` attribute,
|
||||
return the result of calling it.
|
||||
|
||||
Otherwise, return an iterator over the items in `arg`,
|
||||
inverting each item on the fly.
|
||||
|
||||
*See also* :attr:`bidict.BidirectionalMapping.__inverted__`
|
||||
"""
|
||||
invattr = getattr(arg, '__inverted__', None)
|
||||
if callable(invattr):
|
||||
inv: ItemsIter[VT, KT] = invattr()
|
||||
return inv
|
||||
return map(swap, iteritems(arg))
|
||||