Compare commits

...
2 Commits
16 changed files with 748 additions and 40 deletions

No files matched your search

+75 -5
View File
@@ -1,11 +1,13 @@
import base64
import socket import socket
import string import string
from collections import deque from collections import deque
from itertools import islice from itertools import islice
from random import choice from random import choice
from typing import Optional from typing import Iterator, Optional, cast
from urllib.parse import urlsplit, urlunsplit from urllib.parse import urlsplit, urlunsplit
import urlextract
from fastapi import FastAPI, Form, HTTPException, Request from fastapi import FastAPI, Form, HTTPException, Request
from fastapi.responses import HTMLResponse, PlainTextResponse, RedirectResponse from fastapi.responses import HTMLResponse, PlainTextResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
@@ -28,6 +30,10 @@ class Paste(BaseModel):
name: Optional[str] = None name: Optional[str] = None
class ExtractRequest(BaseModel):
text: str
# --- Helpers --- # --- Helpers ---
def get_local_ip() -> str: def get_local_ip() -> str:
"""Determines the local IP address of the machine.""" """Determines the local IP address of the machine."""
@@ -117,10 +123,35 @@ async def pop_and_redirect(request: Request):
) )
@app.get("/qrcode_queue", response_class=HTMLResponse) @app.get("/manage", response_class=HTMLResponse)
async def qrcode_queue_view(request: Request): async def manage_view(request: Request):
"""View entire queue as QR codes.""" """Management dashboard for the queue."""
return templates.TemplateResponse(request, "qq.html", {"queue_items": list(queue)}) return templates.TemplateResponse(
request, "manage.html", {"queue_items": list(queue)}
)
@app.post("/api/queue/dedup")
async def api_queue_dedup():
"""Deduplicate the queue."""
global queue
# Preserve order while removing duplicates
seen = set()
new_queue = deque()
for item in queue:
if item not in seen:
seen.add(item)
new_queue.append(item)
queue = new_queue
return {"count": len(queue)}
@app.post("/api/queue/sort")
async def api_queue_sort(reverse: bool = False):
"""Sort the queue."""
global queue
queue = deque(sorted(queue, reverse=reverse))
return {"count": len(queue)}
# --- Routes: Queue (API) --- # --- Routes: Queue (API) ---
@@ -192,3 +223,42 @@ async def api_paste_delete(name: str):
raise HTTPException(status_code=404, detail="Paste not found") raise HTTPException(status_code=404, detail="Paste not found")
content = texts.pop(name) content = texts.pop(name)
return {"name": name, "content": content} return {"name": name, "content": content}
# @app.get("/nsfw-shield", response_class=HTMLResponse)
# async def this(request: Request):
# return templates.TemplateResponse(request, "nsfw_shield.html")
# @app.get("/nsfw-shield/{link:path}")
# async def that(link: str):
# b64link = encode_safe(link)
# return RedirectResponse(f"/nsfw-shield?redirect={b64link}") # if i convert this thing to
@app.get("/extractor", response_class=HTMLResponse)
async def extractor_ui(request: Request):
"""Link Extractor UI."""
return templates.TemplateResponse(request, "extractor.html")
@app.post("/api/extract/jumblang")
async def api_extract_jumblang(request: ExtractRequest):
"""Extract URLs from messy text using urlextract."""
return list(extract_urls_from_jumblang(request.text))
def encode_safe(link: str) -> str:
return base64.urlsafe_b64encode(link.encode()).rstrip(b"=").decode()
def decode_safe(should_be_b64: str) -> str:
supposedlength = (len(should_be_b64) + 3) // 4 * 4
b64 = should_be_b64.ljust(supposedlength, "=")
return base64.urlsafe_b64decode(b64).decode()
def extract_urls_from_jumblang(txt: str) -> Iterator[str]:
return cast(
Iterator[str], urlextract.URLExtract().gen_urls(txt, with_schema_only=True)
) # i read the code. PLEASE have overloads mr coders
+30
View File
@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
console.log(window.location.search);
try {
console.log(
Array.from(new URLSearchParams(location.search).entries())
); // [["https://ldlda.com", ""]]
} catch (e) {
console.error(e);
}
const cuhwuhfduhs = window.location.search.replace(/^\?/g, "");
if (cuhwuhfduhs) {
const bbugsbg = new URL(cuhwuhfduhs);
const hweriwehr = new URL(location);
hweriwehr.search = "";
if (bbugsbg != hweriwehr) {
// window.location.href = cuhwuhfduhs;
alert(`redir to ${bbugsbg}`);
}
}
</script>
</body>
</html>
+1
View File
@@ -10,5 +10,6 @@ dependencies = [
"pydantic", "pydantic",
"python-multipart", "python-multipart",
"qrcode", "qrcode",
"urlextract>=1.9.0",
"uvicorn", "uvicorn",
] ]
+1
View File
@@ -22,6 +22,7 @@ a:hover {
.container { .container {
width: 100%; width: 100%;
max-width: 800px; /* Default width */
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
+8 -1
View File
@@ -56,7 +56,8 @@ textarea {
font-family: monospace; font-family: monospace;
min-width: 20%; min-width: 20%;
min-height: 20dvh; min-height: 20dvh;
box-sizing: border-box; /* Ensure padding doesn't break width */ box-sizing: border-box;
/* Ensure padding doesn't break width */
} }
form { form {
@@ -123,6 +124,8 @@ textarea.form-control {
/* Navbar */ /* Navbar */
.navbar { .navbar {
width: 100%; width: 100%;
max-width: 800px;
/* Default width */
background-color: var(--surface-color); background-color: var(--surface-color);
padding: 1rem 2rem; padding: 1rem 2rem;
display: flex; display: flex;
@@ -200,3 +203,7 @@ summary::-webkit-details-marker {
width: fit-content; width: fit-content;
align-self: center; align-self: center;
} }
.qrcode:empty {
display: none;
}
+186
View File
@@ -0,0 +1,186 @@
:root {
--bg-color: #121212;
--surface-color: #1e1e1e;
--primary-color: #bb86fc;
--text-color: #e0e0e0;
--error-color: #cf6679;
--success-color: #03dac6;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
font-family: system-ui, -apple-system, sans-serif;
display: flex;
justify-content: center;
align-items: center;
margin: 0;
padding: 20px;
box-sizing: border-box;
min-height: 100lvh;
}
.card {
background-color: var(--surface-color);
padding: 2rem;
border-radius: 12px;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.4);
max-width: 400px;
width: 100%;
text-align: center;
border: 1px solid #333;
}
h1 {
margin-top: 0;
color: var(--error-color);
font-size: 1.8rem;
}
p {
line-height: 1.5;
margin-bottom: 1.5rem;
}
input {
width: 100%;
padding: 12px;
margin: 10px 0;
background: #2d2d2d;
border: 1px solid #444;
color: white;
border-radius: 6px;
box-sizing: border-box;
font-size: 1rem;
}
input:focus {
outline: 2px solid var(--primary-color);
border-color: transparent;
}
.buttons {
display: flex;
gap: 10px;
justify-content: center;
margin-top: 20px;
}
button {
padding: 12px 24px;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: bold;
font-size: 1rem;
transition: transform 0.1s, opacity 0.2s;
}
button:active {
transform: scale(0.98);
}
button.primary {
background-color: var(--error-color);
color: #000;
}
button.create-btn {
background-color: var(--primary-color);
color: #000;
}
button.secondary {
background-color: #333;
color: #fff;
border: 1px solid #444;
}
button:hover {
opacity: 0.9;
}
.hidden {
display: none;
}
#result-area {
margin-top: 20px;
padding: 15px;
background: #252525;
border-radius: 6px;
border: 1px solid #333;
text-align: left;
}
.link-display {
word-break: break-all;
color: var(--primary-color);
margin-bottom: 10px;
display: block;
font-family: monospace;
}
.copy-feedback {
display: none;
color: var(--success-color);
font-size: 0.8rem;
margin-left: 10px;
opacity: 0;
transition: opacity 0.3s;
}
.copy-feedback.show {
opacity: 1;
display: unset;
}
footer {
margin-top: 30px;
font-size: 0.8rem;
color: #666;
}
footer a {
color: #888;
text-decoration: none;
}
footer a:hover {
text-decoration: underline;
}
/* Utility Classes */
.text-primary {
color: var(--primary-color);
}
.text-muted {
color: #888;
font-size: 0.9em;
}
.mb-small {
margin: 0 0 5px 0;
}
.btn-full {
width: 100%;
padding: 8px;
}
.warning-box {
background: #252525;
padding: 10px;
border-radius: 4px;
margin-bottom: 20px;
text-align: left;
}
.destination-url {
color: white;
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
+4 -2
View File
@@ -1,6 +1,6 @@
.pastepad-container { .pastepad-container {
width: 100%; width: 100%;
max-width: 1000px; /* Increased from 800px */ /* max-width handled by .layout-wide .container */
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 20px; gap: 20px;
@@ -51,6 +51,8 @@
box-sizing: border-box; box-sizing: border-box;
} }
.navbar:has( ~ .container .pastepad-container) { /* Wide Layout Override */
.layout-wide .navbar,
.layout-wide .container {
max-width: 1000px; max-width: 1000px;
} }
-4
View File
@@ -70,7 +70,3 @@ details:has(.route-list) {
.queue-form textarea { .queue-form textarea {
resize: vertical; resize: vertical;
} }
.navbar:has(~ .container .queue-title) {
max-width: 800px;
}
+1 -1
View File
@@ -10,7 +10,7 @@
/> />
{% block head %}{% endblock %} {% block head %}{% endblock %}
</head> </head>
<body> <body class="{% block body_class %}{% endblock %}">
<nav class="navbar"> <nav class="navbar">
<a href="/" class="nav-brand">QM</a> <a href="/" class="nav-brand">QM</a>
<div class="nav-links"> <div class="nav-links">
+260
View File
@@ -0,0 +1,260 @@
{% extends "base.html" %} {% block title %}Link Extractor{% endblock %}{% block
body_class %}layout-wide{% endblock %}{% block content %}
<div class="container">
<h1>Link Extractor</h1>
<p>Paste HTML or Markdown below to extract all links.</p>
<div class="card" style="width: 100%; max-width: 800px">
<div style="margin-bottom: 10px; text-align: left">
<label for="file-upload" class="btn btn-sm" style="cursor: pointer"
>📂 Upload File</label
>
<input
type="file"
id="file-upload"
style="display: none"
accept=".html,.htm,.md,.txt,.markdown"
multiple
/>
<span
id="file-name"
style="margin-left: 10px; color: #aaa; font-size: 0.9em"
></span>
</div>
<textarea
id="input-content"
class="form-control"
placeholder="Paste content here..."
style="min-height: 200px; font-family: monospace"
></textarea>
<div class="actions" style="margin-top: 15px">
<button onclick="extractLinks('html')" class="btn">
Extract from HTML
</button>
<button onclick="extractLinks('markdown')" class="btn">
Extract from Markdown
</button>
<button onclick="extractLinks('jumblang')" class="btn">
Extract from Messy Text
</button>
<button onclick="extractLinks('auto')" class="btn btn-primary">
Auto Detect & Extract
</button>
</div>
</div>
<div
id="results-container"
class="card hidden"
style="
width: 100%;
max-width: 800px;
margin-top: 20px;
text-align: left;
"
>
<div
style="
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
"
>
<h2 style="margin: 0">
Found <span id="link-count">0</span> Links
</h2>
<div>
<button onclick="copyAll()" class="btn-sm">Copy All</button>
<button onclick="sendToQueue()" class="btn-sm btn-primary">
Send to Queue
</button>
</div>
</div>
<textarea
id="output-links"
class="form-control"
readonly
style="min-height: 150px; white-space: pre"
></textarea>
</div>
</div>
<script>
// File Upload Handler
/** @type {HTMLInputElement} */
const fileInput = document.getElementById("file-upload");
fileInput.addEventListener("change", async function (e) {
/** @type {FileList} */
const files = e.target.files;
if (!files || files.length === 0) return;
document.getElementById(
"file-name"
).textContent = `${files.length} file(s) selected`;
/** @type {HTMLTextAreaElement} */
const textarea = document.getElementById("input-content");
textarea.value = ""; // Clear previous content
// Read all files
for (let i = 0; i < files.length; i++) {
const file = files[i];
const text = await readFileAsText(file);
textarea.value += `\n\n<!-- File: ${file.name} -->\n${text}`;
}
// Auto-detect mode based on the first file's extension (heuristic)
const firstFile = files[0];
if (
firstFile.name.endsWith(".md") ||
firstFile.name.endsWith(".markdown")
) {
extractLinks("markdown");
} else if (
firstFile.name.endsWith(".html") ||
firstFile.name.endsWith(".htm")
) {
extractLinks("html");
}
});
/**
* Reads a file as text using a Promise wrapper around FileReader.
* @param {File} file
* @returns {Promise<string>}
*/
function readFileAsText(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => resolve(e.target.result);
reader.onerror = (e) => reject(e);
reader.readAsText(file);
});
}
/**
* Extracts links from the input content based on the specified mode.
* @param {'html' | 'markdown' | 'auto' | 'jumblang'} mode
*/
async function extractLinks(mode) {
/** @type {string} */
const content = document.getElementById("input-content").value;
if (!content.trim()) return;
/** @type {string[]} */
let links = [];
if (mode === "jumblang") {
try {
const response = await fetch("/api/extract/jumblang", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text: content }),
});
if (!response.ok) throw new Error("Extraction failed");
links = await response.json();
} catch (e) {
alert("Extraction failed: " + e.message);
return;
}
} else {
if (mode === "auto") {
// Simple heuristic: if it has <a href, it's likely HTML.
mode =
content.includes("<a href") ||
content.includes("<!DOCTYPE html>")
? "html"
: "markdown";
}
if (mode === "html") {
const parser = new DOMParser();
const doc = parser.parseFromString(content, "text/html");
const anchors = doc.querySelectorAll("a[href]");
links = Array.from(anchors)
.map((a) => a.href)
.filter((h) => h.startsWith("http"));
} else {
// Markdown regex for [text](url) and <url>
const mdLinkRegex = /\[.*?\]\((https?:\/\/[^\s)]+)\)/g;
const rawLinkRegex = /<(https?:\/\/[^>]+)>/g;
let match;
while ((match = mdLinkRegex.exec(content)) !== null) {
links.push(match[1]);
}
while ((match = rawLinkRegex.exec(content)) !== null) {
links.push(match[1]);
}
// Also catch plain URLs if they are on their own line or space-separated
// This is a bit aggressive, so maybe optional? For now let's stick to explicit MD syntax
// or use a library if we want full robustness.
}
}
// Deduplicate
links = [...new Set(links)];
displayResults(links);
}
/**
* Displays the extracted links in the results container.
* @param {string[]} links
*/
function displayResults(links) {
const container = document.getElementById("results-container");
/** @type {HTMLTextAreaElement} */
const output = document.getElementById("output-links");
const count = document.getElementById("link-count");
output.value = links.join("\n");
count.textContent = links.length.toString();
container.classList.remove("hidden");
container.scrollIntoView({ behavior: "smooth" });
}
/**
* Copies all extracted links to the clipboard.
*/
function copyAll() {
/** @type {HTMLTextAreaElement} */
const output = document.getElementById("output-links");
output.select();
navigator.clipboard.writeText(output.value);
}
/**
* Sends the extracted links to the queue.
*/
async function sendToQueue() {
/** @type {HTMLTextAreaElement} */
const output = document.getElementById("output-links");
const links = output.value;
if (!links) return;
const formData = new FormData();
formData.append("lines", links);
try {
await fetch("/queuein", {
method: "POST",
body: formData,
});
alert("Links sent to queue!");
} catch (e) {
alert("Failed to send to queue");
}
}
</script>
<style>
.hidden {
display: none;
}
</style>
{% endblock %}
+8 -4
View File
@@ -56,10 +56,14 @@
</div> </div>
<div class="card"> <div class="card">
<h2>PastePad</h2> <h2>Tools</h2>
<p>Paste your clipboard or any text and manage them!</p> <div
<div class="actions"> class="actions"
<a href="/pastepad" class="btn">Visit</a> style="flex-direction: column; align-items: stretch"
>
<a href="/manage" class="btn">Queue Manager</a>
<a href="/pastepad" class="btn">PastePad</a>
<a href="/extractor" class="btn">Link Extractor</a>
</div> </div>
</div> </div>
</div> </div>
+36 -15
View File
@@ -31,28 +31,49 @@ queue_items %}
<a href="{{ item }}" target="_blank" rel="noreferrer noopener"> <a href="{{ item }}" target="_blank" rel="noreferrer noopener">
{{ item }} {{ item }}
</a> </a>
{% if not loop.index > qr_limit %}
<div class="qrcode"></div> <div class="qrcode"></div>
{% endif %}
</li> </li>
{% endfor %} {% endfor %}
</ol> </ol>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/qrcode.js"></script> <script src="https://cdn.jsdelivr.net/npm/[email protected]/qrcode.js"></script>
<script> <script>
window.addEventListener("load", (_) => { document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll(".qrcode").forEach((e) => { const observer = new IntersectionObserver(
// Find the sibling <a> tag within the same <li> (entries, obs) => {
const link = e.parentElement.querySelector("a"); entries.forEach((entry) => {
const a = link ? link.textContent.trim() : ""; if (entry.isIntersecting) {
if (a) { const li = entry.target;
new QRCode(e, { const el = li.querySelector(".qrcode");
text: a,
correctLevel: QRCode.CorrectLevel.L, // Avoid regenerating if already done
width: 256, if (!el || el.childElementCount > 0) {
height: 256, obs.unobserve(li);
return;
}
const link = li.querySelector("a");
const url = link ? link.textContent.trim() : "";
if (url) {
try {
new QRCode(el, {
text: url,
correctLevel: QRCode.CorrectLevel.L,
width: 256,
height: 256,
});
} catch (e) {
console.error("QR Gen failed", e);
}
}
obs.unobserve(li);
}
}); });
} },
}); { rootMargin: "200px" }
);
document.querySelectorAll(".queue-grid li").forEach((el) => observer.observe(el));
}); });
</script> </script>
{% endif %} {% endmacro %} {% endif %} {% endmacro %}
+88
View File
@@ -0,0 +1,88 @@
{% extends "base.html" %} {% block title %}Queue Manager{% endblock %} {% block
body_class %}layout-wide{% endblock %} {% block content %} {% from "macros.html"
import qrcode_queue %}
<div class="container">
<div
style="
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
margin-bottom: 20px;
"
>
<h1>Queue Manager</h1>
<div class="actions">
<button onclick="dedupQueue()" class="btn btn-sm">Dedup</button>
<button onclick="sortQueue(false)" class="btn btn-sm">
Sort A-Z
</button>
<button onclick="sortQueue(true)" class="btn btn-sm">
Sort Z-A
</button>
<button onclick="clearQueue()" class="btn btn-sm btn-danger">
Clear All
</button>
</div>
</div>
<input
type="text"
id="search-input"
class="form-control"
placeholder="Search links..."
style="margin-bottom: 20px"
onkeyup="filterQueue()"
/>
<div id="queue-container">
{% if queue_items %}
<p id="count-display">Total: {{ queue_items|length }}</p>
{{ qrcode_queue(queue_items) }} {% else %}
<p>Queue is empty.</p>
{% endif %}
</div>
</div>
<script>
async function dedupQueue() {
if (!confirm("Remove duplicates?")) return;
await fetch("/api/queue/dedup", { method: "POST" });
window.location.reload();
}
async function sortQueue(reverse) {
await fetch(`/api/queue/sort?reverse=${reverse}`, { method: "POST" });
window.location.reload();
}
async function clearQueue() {
if (!confirm("Clear entire queue?")) return;
await fetch("/api/queue/clear", { method: "POST" });
window.location.reload();
}
function filterQueue() {
const input = document.getElementById("search-input");
const filter = input.value.toLowerCase();
const listItems = document.querySelectorAll(".queue-grid li");
let visibleCount = 0;
listItems.forEach((li) => {
const link = li.querySelector("a");
const text = link.textContent || link.innerText;
if (text.toLowerCase().indexOf(filter) > -1) {
li.style.display = "";
visibleCount++;
} else {
li.style.display = "none";
}
});
document.getElementById(
"count-display"
).textContent = `Showing: ${visibleCount} / ${listItems.length}`;
}
</script>
{% endblock %}
+6 -2
View File
@@ -1,5 +1,9 @@
{% extends "base.html" %} {% block title %}PastePad - Queue Manager{% endblock {% extends "base.html" %}
%} {% block content %}
{% block title %}PastePad - Queue Manager{% endblock %}
{% block body_class %}layout-wide{% endblock %}
{% block content %}
<div class="pastepad-container"> <div class="pastepad-container">
<h1>PastePad</h1> <h1>PastePad</h1>
-6
View File
@@ -1,6 +0,0 @@
{% extends "base.html" %} {% block title %}QRCode queue{% endblock %} {% block
content %} {% from "macros.html" import qrcode_queue %} {% if queue_items %}
<p>total: {{ queue_items|length }}</p>
{{ qrcode_queue(queue_items) }} {% else %}
<p>empty</p>
{% endif %} {% endblock %}
Generated
+44
View File
@@ -77,6 +77,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/3e/57/aa70121b5008f44031be645a61a7c4abc24e0e888ad3fc8fda916f4d188e/fastapi-0.124.4-py3-none-any.whl", hash = "sha256:6d1e703698443ccb89e50abe4893f3c84d9d6689c0cf1ca4fad6d3c15cf69f15", size = 113281, upload-time = "2025-12-12T15:00:42.44Z" }, { url = "https://files.pythonhosted.org/packages/3e/57/aa70121b5008f44031be645a61a7c4abc24e0e888ad3fc8fda916f4d188e/fastapi-0.124.4-py3-none-any.whl", hash = "sha256:6d1e703698443ccb89e50abe4893f3c84d9d6689c0cf1ca4fad6d3c15cf69f15", size = 113281, upload-time = "2025-12-12T15:00:42.44Z" },
] ]
[[package]]
name = "filelock"
version = "3.20.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a7/23/ce7a1126827cedeb958fc043d61745754464eb56c5937c35bbf2b8e26f34/filelock-3.20.1.tar.gz", hash = "sha256:b8360948b351b80f420878d8516519a2204b07aefcdcfd24912a5d33127f188c", size = 19476, upload-time = "2025-12-15T23:54:28.027Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e3/7f/a1a97644e39e7316d850784c642093c99df1290a460df4ede27659056834/filelock-3.20.1-py3-none-any.whl", hash = "sha256:15d9e9a67306188a44baa72f569d2bfd803076269365fdea0934385da4dc361a", size = 16666, upload-time = "2025-12-15T23:54:26.874Z" },
]
[[package]] [[package]]
name = "flask" name = "flask"
version = "3.1.2" version = "3.1.2"
@@ -185,6 +194,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
] ]
[[package]]
name = "platformdirs"
version = "4.5.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" },
]
[[package]] [[package]]
name = "pydantic" name = "pydantic"
version = "2.12.5" version = "2.12.5"
@@ -284,6 +302,7 @@ dependencies = [
{ name = "pydantic" }, { name = "pydantic" },
{ name = "python-multipart" }, { name = "python-multipart" },
{ name = "qrcode" }, { name = "qrcode" },
{ name = "urlextract" },
{ name = "uvicorn" }, { name = "uvicorn" },
] ]
@@ -294,6 +313,7 @@ requires-dist = [
{ name = "pydantic" }, { name = "pydantic" },
{ name = "python-multipart" }, { name = "python-multipart" },
{ name = "qrcode" }, { name = "qrcode" },
{ name = "urlextract", specifier = ">=1.9.0" },
{ name = "uvicorn" }, { name = "uvicorn" },
] ]
@@ -330,6 +350,30 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
] ]
[[package]]
name = "uritools"
version = "6.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/dd/f7/6651d145bedd535a5bdd6dad108329ec1fec89d38ec611f8d98834eb5378/uritools-6.0.1.tar.gz", hash = "sha256:2f9e9cb954e7877232b2c863f724a44a06eb98d9c7ebdd69914876e9487b94f8", size = 22857, upload-time = "2025-12-21T18:58:54.446Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/d7/e1542857c3f7615a1a9afa6b602b87cb5a33885db41c686aa7bf5092d4f0/uritools-6.0.1-py3-none-any.whl", hash = "sha256:d9507b82206c857d2f93d8fcc84f3b05ae4174096761102be690aa76a360cc1b", size = 10466, upload-time = "2025-12-21T18:58:52.903Z" },
]
[[package]]
name = "urlextract"
version = "1.9.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "filelock" },
{ name = "idna" },
{ name = "platformdirs" },
{ name = "uritools" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e1/1d/57a552551f851c6db5e5d66f505548d47dd3f734a8e648f9d4f05e4e5ac9/urlextract-1.9.0.tar.gz", hash = "sha256:70508e02ba9df372e25cf0642db367cece273e8712cd0ce78178fc5dd7ea00db", size = 35369, upload-time = "2024-02-29T09:14:24.221Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/cb/82ef2994dec66351aaffa45061f30051b6b557bf592952f14657f38b6e43/urlextract-1.9.0-py3-none-any.whl", hash = "sha256:f88963532488b1c7c405e21bd162ae97871754ea04b60e18d33ee075b19b82fd", size = 21460, upload-time = "2024-02-29T09:14:22.297Z" },
]
[[package]] [[package]]
name = "uvicorn" name = "uvicorn"
version = "0.38.0" version = "0.38.0"