From e41e49f7ea477a31daf136a6c124baa832d57b94 Mon Sep 17 00:00:00 2001 From: despiegk Date: Sun, 26 Oct 2025 07:17:49 +0400 Subject: [PATCH] ... --- README.md | 206 ++++++ collections/documents/images/welcome2.md | 58 ++ collections/documents/welcome2.md | 58 ++ collections/notes/ttt/test.md | 4 + config.yaml | 27 + pyproject.toml | 18 + server_webdav.py | 197 ++++++ start.sh | 20 + static/app-tree.js | 866 +++++++++++++++++++++++ static/app.js | 527 ++++++++++++++ static/css/components.css | 160 +++++ static/css/editor.css | 75 ++ static/css/file-tree.css | 88 +++ static/css/layout.css | 69 ++ static/css/variables.css | 31 + static/js/app.js | 302 ++++++++ static/js/editor.js | 273 +++++++ static/js/file-tree.js | 290 ++++++++ static/js/ui-utils.js | 256 +++++++ static/js/webdav-client.js | 239 +++++++ static/style.css | 594 ++++++++++++++++ templates/index.html | 164 +++++ uv.lock | 548 ++++++++++++++ 23 files changed, 5070 insertions(+) create mode 100644 README.md create mode 100644 collections/documents/images/welcome2.md create mode 100644 collections/documents/welcome2.md create mode 100644 collections/notes/ttt/test.md create mode 100644 config.yaml create mode 100644 pyproject.toml create mode 100755 server_webdav.py create mode 100755 start.sh create mode 100644 static/app-tree.js create mode 100644 static/app.js create mode 100644 static/css/components.css create mode 100644 static/css/editor.css create mode 100644 static/css/file-tree.css create mode 100644 static/css/layout.css create mode 100644 static/css/variables.css create mode 100644 static/js/app.js create mode 100644 static/js/editor.js create mode 100644 static/js/file-tree.js create mode 100644 static/js/ui-utils.js create mode 100644 static/js/webdav-client.js create mode 100644 static/style.css create mode 100644 templates/index.html create mode 100644 uv.lock diff --git a/README.md b/README.md new file mode 100644 index 0000000..a419f5a --- /dev/null +++ b/README.md @@ -0,0 +1,206 @@ +# Markdown Editor + +A full-featured markdown editor with live preview, built with modern web technologies. + +## Features + +- **Real-time Preview**: See your markdown rendered as you type +- **Syntax Highlighting**: Powered by CodeMirror with support for multiple languages +- **File Management**: Create, save, load, and delete markdown files +- **Mermaid Diagrams**: Render beautiful diagrams directly in your markdown +- **HTML Support**: Embed HTML snippets in your markdown +- **Synchronized Scrolling**: Editor and preview scroll together +- **Responsive Design**: Works on desktop and mobile devices + +## Supported Languages + +The editor provides syntax highlighting for: + +- Markdown +- JavaScript +- Python +- HTML/CSS +- Go +- Rust +- JSON +- TOML +- YAML +- And more! + +## Technology Stack + +### Frontend +- **Bootstrap 5**: Modern, responsive UI framework +- **CodeMirror 5**: Powerful code editor component +- **Marked.js**: Fast markdown parser +- **Mermaid**: Diagram and flowchart rendering +- **Unpoly**: Progressive enhancement library + +### Backend +- **FastAPI**: Modern Python web framework +- **Uvicorn**: ASGI server +- **uv (Astral)**: Fast Python package installer and environment manager + +## Installation & Usage + +### Quick Start + +1. Navigate to the project directory: +```bash +cd markdown-editor +``` + +2. Run the startup script: +```bash +./start.sh +``` + +The script will: +- Install uv if not present +- Create a virtual environment +- Install all Python dependencies +- Create a sample markdown file +- Start the development server + +3. Open your browser and navigate to: +``` +http://localhost:8000 +``` + +### Manual Installation + +If you prefer to install manually: + +```bash +# Install dependencies with uv +uv venv +uv pip install -e . + +# Start the server +uv run uvicorn server:app --host 0.0.0.0 --port 8000 --reload +``` + +## Project Structure + +``` +markdown-editor/ +├── data/ # Markdown files storage +├── static/ # Static assets +│ ├── app.js # Main JavaScript application +│ └── style.css # Custom styles +├── templates/ # HTML templates +│ └── index.html # Main editor page +├── server.py # FastAPI backend server +├── pyproject.toml # Python project configuration +├── start.sh # Installation and startup script +└── README.md # This file +``` + +## API Endpoints + +The backend provides the following REST API endpoints: + +- `GET /` - Serve the main editor page +- `GET /api/files` - List all markdown files +- `GET /api/files/{filename}` - Get content of a specific file +- `POST /api/files` - Create or update a file +- `DELETE /api/files/{filename}` - Delete a file + +## Usage Guide + +### Creating a New File + +1. Click the **New** button in the navbar +2. Enter a filename in the input field (e.g., `my-document.md`) +3. Start typing your markdown content +4. Click **Save** to save the file + +### Loading an Existing File + +- Click on any file in the sidebar to load it into the editor + +### Saving Changes + +- Click the **Save** button or press `Ctrl+S` (Windows/Linux) or `Cmd+S` (Mac) + +### Deleting a File + +1. Load the file you want to delete +2. Click the **Delete** button +3. Confirm the deletion + +### Using Mermaid Diagrams + +Create a code block with the `mermaid` language identifier: + +````markdown +```mermaid +graph TD + A[Start] --> B[Process] + B --> C[End] +``` +```` + +### Embedding HTML + +You can embed HTML directly in your markdown: + +```markdown +
+ This is red text! +
+``` + +## Keyboard Shortcuts + +- `Ctrl+S` / `Cmd+S` - Save current file + +## Development + +### Requirements + +- Python 3.11+ +- uv (Astral) - Will be installed automatically by start.sh + +### Running in Development Mode + +The server runs with auto-reload enabled by default, so any changes to the Python code will automatically restart the server. + +### Adding New Features + +- Frontend code: Edit `static/app.js` and `static/style.css` +- Backend API: Edit `server.py` +- UI layout: Edit `templates/index.html` + +## Troubleshooting + +### Port Already in Use + +If port 8000 is already in use, you can change it in `start.sh` or run manually: + +```bash +uv run uvicorn server:app --host 0.0.0.0 --port 8080 --reload +``` + +### Dependencies Not Installing + +Make sure uv is properly installed: + +```bash +curl -LsSf https://astral.sh/uv/install.sh | sh +``` + +Then try running `./start.sh` again. + +## License + +MIT License - Feel free to use this project for any purpose. + +## Contributing + +Contributions are welcome! Feel free to submit issues or pull requests. + +--- + +**Happy writing!** 📝✨ + diff --git a/collections/documents/images/welcome2.md b/collections/documents/images/welcome2.md new file mode 100644 index 0000000..fed0c5f --- /dev/null +++ b/collections/documents/images/welcome2.md @@ -0,0 +1,58 @@ +# Welcome to Markdown Editor + +This is a **WebDAV-based** markdown editor with modular architecture. + +```mermaid +%%{init: {'theme':'dark'}}%% +graph TD + + %% User side + H1[Human A] --> PA1[Personal Agent A] + H2[Human B] --> PA2[Personal Agent B] + + %% Local mail nodes + PA1 --> M1[MyMail Node A] + PA2 --> M2[MyMail Node B] + + %% Proxy coordination layer + M1 --> Proxy1A[Proxy Agent L1] + Proxy1A --> Proxy2A[Proxy Agent L2] + Proxy2A --> Proxy2B[Proxy Agent L2] + Proxy2B --> Proxy1B[Proxy Agent L1] + Proxy1B --> M2 + + %% Blockchain anchoring + M1 --> Chain[Dynamic Blockchain] + M2 --> Chain +``` + +## Features + +- ✅ Standards-compliant WebDAV backend +- ✅ Multiple document collections +- ✅ Modular JavaScript/CSS +- ✅ Live preview +- ✅ Syntax highlighting +- ✅ Mermaid diagrams +- ✅ Dark mode + +## WebDAV Methods + +This editor uses standard WebDAV methods: + +- `PROPFIND` - List files +- `GET` - Read files +- `PUT` - Create/update files +- `DELETE` - Delete files +- `COPY` - Copy files +- `MOVE` - Move/rename files +- `MKCOL` - Create directories + +## Try It Out + +1. Create a new file +2. Edit markdown +3. See live preview +4. Save with WebDAV PUT + +Enjoy! diff --git a/collections/documents/welcome2.md b/collections/documents/welcome2.md new file mode 100644 index 0000000..fed0c5f --- /dev/null +++ b/collections/documents/welcome2.md @@ -0,0 +1,58 @@ +# Welcome to Markdown Editor + +This is a **WebDAV-based** markdown editor with modular architecture. + +```mermaid +%%{init: {'theme':'dark'}}%% +graph TD + + %% User side + H1[Human A] --> PA1[Personal Agent A] + H2[Human B] --> PA2[Personal Agent B] + + %% Local mail nodes + PA1 --> M1[MyMail Node A] + PA2 --> M2[MyMail Node B] + + %% Proxy coordination layer + M1 --> Proxy1A[Proxy Agent L1] + Proxy1A --> Proxy2A[Proxy Agent L2] + Proxy2A --> Proxy2B[Proxy Agent L2] + Proxy2B --> Proxy1B[Proxy Agent L1] + Proxy1B --> M2 + + %% Blockchain anchoring + M1 --> Chain[Dynamic Blockchain] + M2 --> Chain +``` + +## Features + +- ✅ Standards-compliant WebDAV backend +- ✅ Multiple document collections +- ✅ Modular JavaScript/CSS +- ✅ Live preview +- ✅ Syntax highlighting +- ✅ Mermaid diagrams +- ✅ Dark mode + +## WebDAV Methods + +This editor uses standard WebDAV methods: + +- `PROPFIND` - List files +- `GET` - Read files +- `PUT` - Create/update files +- `DELETE` - Delete files +- `COPY` - Copy files +- `MOVE` - Move/rename files +- `MKCOL` - Create directories + +## Try It Out + +1. Create a new file +2. Edit markdown +3. See live preview +4. Save with WebDAV PUT + +Enjoy! diff --git a/collections/notes/ttt/test.md b/collections/notes/ttt/test.md new file mode 100644 index 0000000..c1f1849 --- /dev/null +++ b/collections/notes/ttt/test.md @@ -0,0 +1,4 @@ + +test + + diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..522c792 --- /dev/null +++ b/config.yaml @@ -0,0 +1,27 @@ +# WsgiDAV Configuration +# Collections define WebDAV-accessible directories + +collections: + documents: + path: "./collections/documents" + description: "General documents and notes" + + notes: + path: "./collections/notes" + description: "Personal notes and drafts" + + projects: + path: "./collections/projects" + description: "Project documentation" + +# Server settings +server: + host: "0.0.0.0" + port: 8004 + +# WebDAV settings +webdav: + verbose: 1 + enable_loggers: [] + property_manager: true + lock_manager: true diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..356d667 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,18 @@ +[project] +name = "markdown-editor" +version = "3.0.0" +description = "WebDAV-based Markdown Editor with modular architecture" +requires-python = ">=3.11" +dependencies = [ + "wsgidav>=4.3.0", + "cheroot>=10.0.0", + "pyyaml>=6.0", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = [] + diff --git a/server_webdav.py b/server_webdav.py new file mode 100755 index 0000000..10307e7 --- /dev/null +++ b/server_webdav.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +""" +WebDAV-based Markdown Editor Server +Uses WsgiDAV for standards-compliant file operations +""" + +import os +import sys +import yaml +import json +from pathlib import Path +from wsgidav.wsgidav_app import WsgiDAVApp +from wsgidav.fs_dav_provider import FilesystemProvider +from cheroot import wsgi +from cheroot.ssl.builtin import BuiltinSSLAdapter + + +class MarkdownEditorApp: + """Main application that wraps WsgiDAV and adds custom endpoints""" + + def __init__(self, config_path="config.yaml"): + self.config = self.load_config(config_path) + self.collections = self.config.get('collections', {}) + self.setup_collections() + self.webdav_app = self.create_webdav_app() + + def load_config(self, config_path): + """Load configuration from YAML file""" + with open(config_path, 'r') as f: + return yaml.safe_load(f) + + def setup_collections(self): + """Create collection directories if they don't exist""" + for name, config in self.collections.items(): + path = Path(config['path']) + path.mkdir(parents=True, exist_ok=True) + + # Create images subdirectory + images_path = path / 'images' + images_path.mkdir(exist_ok=True) + + print(f"Collection '{name}' -> {path.absolute()}") + + def create_webdav_app(self): + """Create WsgiDAV application with configured collections""" + provider_mapping = {} + + for name, config in self.collections.items(): + path = os.path.abspath(config['path']) + provider_mapping[f'/fs/{name}'] = FilesystemProvider(path) + + config = { + 'host': self.config['server']['host'], + 'port': self.config['server']['port'], + 'provider_mapping': provider_mapping, + 'verbose': self.config['webdav'].get('verbose', 1), + 'logging': { + 'enable_loggers': [] + }, + 'property_manager': True, + 'lock_storage': True, + 'simple_dc': { + 'user_mapping': { + '*': True # Allow anonymous access for development + } + } + } + + return WsgiDAVApp(config) + + def __call__(self, environ, start_response): + """WSGI application entry point""" + path = environ.get('PATH_INFO', '') + method = environ.get('REQUEST_METHOD', '') + + # Handle collection list endpoint + if path == '/fs/' and method == 'GET': + return self.handle_collections_list(environ, start_response) + + # Handle static files + if path.startswith('/static/'): + return self.handle_static(environ, start_response) + + # Handle root - serve index.html + if path == '/' or path == '/index.html': + return self.handle_index(environ, start_response) + + # All other requests go to WebDAV + return self.webdav_app(environ, start_response) + + def handle_collections_list(self, environ, start_response): + """Return list of available collections""" + collections = list(self.collections.keys()) + response_body = json.dumps(collections).encode('utf-8') + + start_response('200 OK', [ + ('Content-Type', 'application/json'), + ('Content-Length', str(len(response_body))), + ('Access-Control-Allow-Origin', '*') + ]) + + return [response_body] + + def handle_static(self, environ, start_response): + """Serve static files""" + path = environ.get('PATH_INFO', '')[1:] # Remove leading / + file_path = Path(path) + + if not file_path.exists() or not file_path.is_file(): + start_response('404 Not Found', [('Content-Type', 'text/plain')]) + return [b'File not found'] + + # Determine content type + content_types = { + '.html': 'text/html', + '.css': 'text/css', + '.js': 'application/javascript', + '.json': 'application/json', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.gif': 'image/gif', + '.svg': 'image/svg+xml', + '.ico': 'image/x-icon' + } + + ext = file_path.suffix.lower() + content_type = content_types.get(ext, 'application/octet-stream') + + with open(file_path, 'rb') as f: + content = f.read() + + start_response('200 OK', [ + ('Content-Type', content_type), + ('Content-Length', str(len(content))) + ]) + + return [content] + + def handle_index(self, environ, start_response): + """Serve index.html""" + index_path = Path('templates/index.html') + + if not index_path.exists(): + start_response('404 Not Found', [('Content-Type', 'text/plain')]) + return [b'index.html not found'] + + with open(index_path, 'r', encoding='utf-8') as f: + content = f.read().encode('utf-8') + + start_response('200 OK', [ + ('Content-Type', 'text/html; charset=utf-8'), + ('Content-Length', str(len(content))) + ]) + + return [content] + + +def main(): + """Start the server""" + print("=" * 60) + print("Markdown Editor with WebDAV Backend") + print("=" * 60) + + # Create application + app = MarkdownEditorApp() + + # Get server config + host = app.config['server']['host'] + port = app.config['server']['port'] + + print(f"\nServer starting on http://{host}:{port}") + print(f"\nAvailable collections:") + for name, config in app.collections.items(): + print(f" - {name}: {config['description']}") + print(f" WebDAV: http://{host}:{port}/fs/{name}/") + + print(f"\nWeb UI: http://{host}:{port}/") + print("\nPress Ctrl+C to stop the server") + print("=" * 60) + + # Create and start server + server = wsgi.Server( + bind_addr=(host, port), + wsgi_app=app + ) + + try: + server.start() + except KeyboardInterrupt: + print("\n\nShutting down...") + server.stop() + + +if __name__ == '__main__': + main() + diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..b4705fd --- /dev/null +++ b/start.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -e +echo "==============================================" +echo "Markdown Editor v3.0 - WebDAV Server" +echo "==============================================" +if ! command -v uv &> /dev/null; then + echo "Installing uv..." + curl -LsSf https://astral.sh/uv/install.sh | sh + export PATH="$HOME/.cargo/bin:$PATH" +fi +if [ ! -d ".venv" ]; then + echo "Creating virtual environment..." + uv venv +fi +echo "Activating virtual environment..." +source .venv/bin/activate +echo "Installing dependencies..." +uv pip install wsgidav cheroot pyyaml +echo "Starting WebDAV server..." +python server_webdav.py diff --git a/static/app-tree.js b/static/app-tree.js new file mode 100644 index 0000000..0fdea6d --- /dev/null +++ b/static/app-tree.js @@ -0,0 +1,866 @@ +// Markdown Editor Application with File Tree +(function() { + 'use strict'; + + // State management + let currentFile = null; + let currentFilePath = null; + let editor = null; + let isScrollingSynced = true; + let scrollTimeout = null; + let isDarkMode = false; + let fileTree = []; + let contextMenuTarget = null; + let clipboard = null; // For copy/move operations + + // Dark mode management + function initDarkMode() { + const savedMode = localStorage.getItem('darkMode'); + if (savedMode === 'true') { + enableDarkMode(); + } + } + + function enableDarkMode() { + isDarkMode = true; + document.body.classList.add('dark-mode'); + document.getElementById('darkModeIcon').textContent = '☀️'; + localStorage.setItem('darkMode', 'true'); + + mermaid.initialize({ + startOnLoad: false, + theme: 'dark', + securityLevel: 'loose' + }); + + if (editor && editor.getValue()) { + updatePreview(); + } + } + + function disableDarkMode() { + isDarkMode = false; + document.body.classList.remove('dark-mode'); + document.getElementById('darkModeIcon').textContent = '🌙'; + localStorage.setItem('darkMode', 'false'); + + mermaid.initialize({ + startOnLoad: false, + theme: 'default', + securityLevel: 'loose' + }); + + if (editor && editor.getValue()) { + updatePreview(); + } + } + + function toggleDarkMode() { + if (isDarkMode) { + disableDarkMode(); + } else { + enableDarkMode(); + } + } + + // Initialize Mermaid + mermaid.initialize({ + startOnLoad: false, + theme: 'default', + securityLevel: 'loose' + }); + + // Configure marked.js for markdown parsing + marked.setOptions({ + breaks: true, + gfm: true, + headerIds: true, + mangle: false, + sanitize: false, + smartLists: true, + smartypants: true, + xhtml: false + }); + + // Handle image upload + async function uploadImage(file) { + const formData = new FormData(); + formData.append('file', file); + + try { + const response = await fetch('/api/upload-image', { + method: 'POST', + body: formData + }); + + if (!response.ok) throw new Error('Upload failed'); + + const result = await response.json(); + return result.url; + } catch (error) { + console.error('Error uploading image:', error); + showNotification('Error uploading image', 'danger'); + return null; + } + } + + // Handle drag and drop for images + function setupDragAndDrop() { + const editorElement = document.querySelector('.CodeMirror'); + + ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { + editorElement.addEventListener(eventName, preventDefaults, false); + }); + + function preventDefaults(e) { + e.preventDefault(); + e.stopPropagation(); + } + + ['dragenter', 'dragover'].forEach(eventName => { + editorElement.addEventListener(eventName, () => { + editorElement.classList.add('drag-over'); + }, false); + }); + + ['dragleave', 'drop'].forEach(eventName => { + editorElement.addEventListener(eventName, () => { + editorElement.classList.remove('drag-over'); + }, false); + }); + + editorElement.addEventListener('drop', async (e) => { + const files = e.dataTransfer.files; + + if (files.length === 0) return; + + const imageFiles = Array.from(files).filter(file => + file.type.startsWith('image/') + ); + + if (imageFiles.length === 0) { + showNotification('Please drop image files only', 'warning'); + return; + } + + showNotification(`Uploading ${imageFiles.length} image(s)...`, 'info'); + + for (const file of imageFiles) { + const url = await uploadImage(file); + if (url) { + const cursor = editor.getCursor(); + const imageMarkdown = `![${file.name}](${url})`; + editor.replaceRange(imageMarkdown, cursor); + editor.setCursor(cursor.line, cursor.ch + imageMarkdown.length); + showNotification(`Image uploaded: ${file.name}`, 'success'); + } + } + }, false); + + editorElement.addEventListener('paste', async (e) => { + const items = e.clipboardData?.items; + if (!items) return; + + for (const item of items) { + if (item.type.startsWith('image/')) { + e.preventDefault(); + const file = item.getAsFile(); + if (file) { + showNotification('Uploading pasted image...', 'info'); + const url = await uploadImage(file); + if (url) { + const cursor = editor.getCursor(); + const imageMarkdown = `![pasted-image](${url})`; + editor.replaceRange(imageMarkdown, cursor); + showNotification('Image uploaded successfully', 'success'); + } + } + } + } + }); + } + + // Initialize CodeMirror editor + function initEditor() { + editor = CodeMirror.fromTextArea(document.getElementById('editor'), { + mode: 'markdown', + theme: 'monokai', + lineNumbers: true, + lineWrapping: true, + autofocus: true, + extraKeys: { + 'Ctrl-S': function() { saveFile(); }, + 'Cmd-S': function() { saveFile(); } + } + }); + + editor.on('change', debounce(updatePreview, 300)); + + setTimeout(setupDragAndDrop, 100); + + setupScrollSync(); + } + + // Debounce function + function debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; + } + + // Setup synchronized scrolling + function setupScrollSync() { + const previewDiv = document.getElementById('preview'); + + editor.on('scroll', () => { + if (!isScrollingSynced) return; + + const scrollInfo = editor.getScrollInfo(); + const scrollPercentage = scrollInfo.top / (scrollInfo.height - scrollInfo.clientHeight); + + const previewScrollHeight = previewDiv.scrollHeight - previewDiv.clientHeight; + previewDiv.scrollTop = previewScrollHeight * scrollPercentage; + }); + } + + // Update preview + async function updatePreview() { + const markdown = editor.getValue(); + const previewDiv = document.getElementById('preview'); + + if (!markdown.trim()) { + previewDiv.innerHTML = ` +
+

Preview

+

Start typing in the editor to see the preview

+
+ `; + return; + } + + try { + let html = marked.parse(markdown); + + html = html.replace( + /
([\s\S]*?)<\/code><\/pre>/g,
+                '
$1
' + ); + + previewDiv.innerHTML = html; + + const codeBlocks = previewDiv.querySelectorAll('pre code'); + codeBlocks.forEach(block => { + const languageClass = Array.from(block.classList).find(cls => cls.startsWith('language-')); + if (languageClass && languageClass !== 'language-mermaid') { + Prism.highlightElement(block); + } + }); + + const mermaidElements = previewDiv.querySelectorAll('.mermaid'); + if (mermaidElements.length > 0) { + try { + await mermaid.run({ + nodes: mermaidElements, + suppressErrors: false + }); + } catch (error) { + console.error('Mermaid rendering error:', error); + } + } + } catch (error) { + console.error('Preview rendering error:', error); + previewDiv.innerHTML = ` + + `; + } + } + + // ======================================================================== + // File Tree Management + // ======================================================================== + + async function loadFileTree() { + try { + const response = await fetch('/api/tree'); + if (!response.ok) throw new Error('Failed to load file tree'); + + fileTree = await response.json(); + renderFileTree(); + } catch (error) { + console.error('Error loading file tree:', error); + showNotification('Error loading files', 'danger'); + } + } + + function renderFileTree() { + const container = document.getElementById('fileTree'); + container.innerHTML = ''; + + if (fileTree.length === 0) { + container.innerHTML = '
No files yet
'; + return; + } + + fileTree.forEach(node => { + container.appendChild(createTreeNode(node)); + }); + } + + function createTreeNode(node, level = 0) { + const nodeDiv = document.createElement('div'); + nodeDiv.className = 'tree-node-wrapper'; + + const nodeContent = document.createElement('div'); + nodeContent.className = 'tree-node'; + nodeContent.dataset.path = node.path; + nodeContent.dataset.type = node.type; + nodeContent.dataset.name = node.name; + + // Make draggable + nodeContent.draggable = true; + nodeContent.addEventListener('dragstart', handleDragStart); + nodeContent.addEventListener('dragend', handleDragEnd); + nodeContent.addEventListener('dragover', handleDragOver); + nodeContent.addEventListener('dragleave', handleDragLeave); + nodeContent.addEventListener('drop', handleDrop); + + const contentWrapper = document.createElement('div'); + contentWrapper.className = 'tree-node-content'; + + if (node.type === 'directory') { + const toggle = document.createElement('span'); + toggle.className = 'tree-node-toggle'; + toggle.innerHTML = '▶'; + toggle.addEventListener('click', (e) => { + e.stopPropagation(); + toggleNode(nodeDiv); + }); + contentWrapper.appendChild(toggle); + } else { + const spacer = document.createElement('span'); + spacer.style.width = '16px'; + contentWrapper.appendChild(spacer); + } + + const icon = document.createElement('i'); + icon.className = node.type === 'directory' ? 'bi bi-folder tree-node-icon' : 'bi bi-file-earmark-text tree-node-icon'; + contentWrapper.appendChild(icon); + + const name = document.createElement('span'); + name.className = 'tree-node-name'; + name.textContent = node.name; + contentWrapper.appendChild(name); + + if (node.type === 'file' && node.size) { + const size = document.createElement('span'); + size.className = 'file-size-badge'; + size.textContent = formatFileSize(node.size); + contentWrapper.appendChild(size); + } + + nodeContent.appendChild(contentWrapper); + + nodeContent.addEventListener('click', (e) => { + if (node.type === 'file') { + loadFile(node.path); + } + }); + + nodeContent.addEventListener('contextmenu', (e) => { + e.preventDefault(); + showContextMenu(e, node); + }); + + nodeDiv.appendChild(nodeContent); + + if (node.children && node.children.length > 0) { + const childrenDiv = document.createElement('div'); + childrenDiv.className = 'tree-children collapsed'; + + node.children.forEach(child => { + childrenDiv.appendChild(createTreeNode(child, level + 1)); + }); + + nodeDiv.appendChild(childrenDiv); + } + + return nodeDiv; + } + + function toggleNode(nodeWrapper) { + const toggle = nodeWrapper.querySelector('.tree-node-toggle'); + const children = nodeWrapper.querySelector('.tree-children'); + + if (children) { + children.classList.toggle('collapsed'); + toggle.classList.toggle('expanded'); + } + } + + function formatFileSize(bytes) { + if (bytes < 1024) return bytes + ' B'; + if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; + return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; + } + + // ======================================================================== + // Drag and Drop for Files + // ======================================================================== + + let draggedNode = null; + + function handleDragStart(e) { + draggedNode = { + path: e.currentTarget.dataset.path, + type: e.currentTarget.dataset.type, + name: e.currentTarget.dataset.name + }; + e.currentTarget.classList.add('dragging'); + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData('text/plain', draggedNode.path); + } + + function handleDragEnd(e) { + e.currentTarget.classList.remove('dragging'); + document.querySelectorAll('.drag-over').forEach(el => { + el.classList.remove('drag-over'); + }); + } + + function handleDragOver(e) { + if (!draggedNode) return; + + e.preventDefault(); + e.dataTransfer.dropEffect = 'move'; + + const targetType = e.currentTarget.dataset.type; + if (targetType === 'directory') { + e.currentTarget.classList.add('drag-over'); + } + } + + function handleDragLeave(e) { + e.currentTarget.classList.remove('drag-over'); + } + + async function handleDrop(e) { + e.preventDefault(); + e.currentTarget.classList.remove('drag-over'); + + if (!draggedNode) return; + + const targetPath = e.currentTarget.dataset.path; + const targetType = e.currentTarget.dataset.type; + + if (targetType !== 'directory') return; + if (draggedNode.path === targetPath) return; + + const sourcePath = draggedNode.path; + const destPath = targetPath + '/' + draggedNode.name; + + try { + const response = await fetch('/api/file/move', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + source: sourcePath, + destination: destPath + }) + }); + + if (!response.ok) throw new Error('Move failed'); + + showNotification(`Moved ${draggedNode.name}`, 'success'); + loadFileTree(); + } catch (error) { + console.error('Error moving file:', error); + showNotification('Error moving file', 'danger'); + } + + draggedNode = null; + } + + // ======================================================================== + // Context Menu + // ======================================================================== + + function showContextMenu(e, node) { + contextMenuTarget = node; + const menu = document.getElementById('contextMenu'); + const pasteItem = document.getElementById('pasteMenuItem'); + + // Show paste option only if clipboard has something and target is a directory + if (clipboard && node.type === 'directory') { + pasteItem.style.display = 'flex'; + } else { + pasteItem.style.display = 'none'; + } + + menu.style.display = 'block'; + menu.style.left = e.pageX + 'px'; + menu.style.top = e.pageY + 'px'; + + document.addEventListener('click', hideContextMenu); + } + + function hideContextMenu() { + const menu = document.getElementById('contextMenu'); + menu.style.display = 'none'; + document.removeEventListener('click', hideContextMenu); + } + + // ======================================================================== + // File Operations + // ======================================================================== + + async function loadFile(path) { + try { + const response = await fetch(`/api/file?path=${encodeURIComponent(path)}`); + if (!response.ok) throw new Error('Failed to load file'); + + const data = await response.json(); + currentFile = data.filename; + currentFilePath = path; + + document.getElementById('filenameInput').value = path; + editor.setValue(data.content); + updatePreview(); + + document.querySelectorAll('.tree-node').forEach(node => { + node.classList.remove('active'); + }); + document.querySelector(`[data-path="${path}"]`)?.classList.add('active'); + + showNotification(`Loaded ${data.filename}`, 'info'); + } catch (error) { + console.error('Error loading file:', error); + showNotification('Error loading file', 'danger'); + } + } + + async function saveFile() { + const path = document.getElementById('filenameInput').value.trim(); + + if (!path) { + showNotification('Please enter a filename', 'warning'); + return; + } + + const content = editor.getValue(); + + try { + const response = await fetch('/api/file', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ path, content }) + }); + + if (!response.ok) throw new Error('Failed to save file'); + + const result = await response.json(); + currentFile = path.split('/').pop(); + currentFilePath = result.path; + + showNotification(`Saved ${currentFile}`, 'success'); + loadFileTree(); + } catch (error) { + console.error('Error saving file:', error); + showNotification('Error saving file', 'danger'); + } + } + + async function deleteFile() { + if (!currentFilePath) { + showNotification('No file selected', 'warning'); + return; + } + + if (!confirm(`Are you sure you want to delete ${currentFile}?`)) { + return; + } + + try { + const response = await fetch(`/api/file?path=${encodeURIComponent(currentFilePath)}`, { + method: 'DELETE' + }); + + if (!response.ok) throw new Error('Failed to delete file'); + + showNotification(`Deleted ${currentFile}`, 'success'); + + currentFile = null; + currentFilePath = null; + document.getElementById('filenameInput').value = ''; + editor.setValue(''); + updatePreview(); + + loadFileTree(); + } catch (error) { + console.error('Error deleting file:', error); + showNotification('Error deleting file', 'danger'); + } + } + + function newFile() { + // Clear editor for new file + currentFile = null; + currentFilePath = null; + document.getElementById('filenameInput').value = ''; + document.getElementById('filenameInput').focus(); + editor.setValue(''); + updatePreview(); + + document.querySelectorAll('.tree-node').forEach(node => { + node.classList.remove('active'); + }); + + showNotification('Enter filename and start typing', 'info'); + } + + async function createFolder() { + const folderName = prompt('Enter folder name:'); + if (!folderName) return; + + try { + const response = await fetch('/api/directory', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ path: folderName }) + }); + + if (!response.ok) throw new Error('Failed to create folder'); + + showNotification(`Created folder ${folderName}`, 'success'); + loadFileTree(); + } catch (error) { + console.error('Error creating folder:', error); + showNotification('Error creating folder', 'danger'); + } + } + + // ======================================================================== + // Context Menu Actions + // ======================================================================== + + async function handleContextMenuAction(action) { + if (!contextMenuTarget) return; + + switch (action) { + case 'open': + if (contextMenuTarget.type === 'file') { + loadFile(contextMenuTarget.path); + } + break; + + case 'rename': + await renameItem(); + break; + + case 'copy': + clipboard = { ...contextMenuTarget, operation: 'copy' }; + showNotification(`Copied ${contextMenuTarget.name}`, 'info'); + break; + + case 'move': + clipboard = { ...contextMenuTarget, operation: 'move' }; + showNotification(`Cut ${contextMenuTarget.name}`, 'info'); + break; + + case 'paste': + await pasteItem(); + break; + + case 'delete': + await deleteItem(); + break; + } + } + + async function renameItem() { + const newName = prompt(`Rename ${contextMenuTarget.name}:`, contextMenuTarget.name); + if (!newName || newName === contextMenuTarget.name) return; + + const oldPath = contextMenuTarget.path; + const newPath = oldPath.substring(0, oldPath.lastIndexOf('/') + 1) + newName; + + try { + const endpoint = contextMenuTarget.type === 'directory' ? '/api/directory/rename' : '/api/file/rename'; + const response = await fetch(endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + old_path: oldPath, + new_path: newPath + }) + }); + + if (!response.ok) throw new Error('Rename failed'); + + showNotification(`Renamed to ${newName}`, 'success'); + loadFileTree(); + } catch (error) { + console.error('Error renaming:', error); + showNotification('Error renaming', 'danger'); + } + } + + async function pasteItem() { + if (!clipboard) return; + + const destDir = contextMenuTarget.path; + const sourcePath = clipboard.path; + const fileName = clipboard.name; + const destPath = destDir + '/' + fileName; + + try { + if (clipboard.operation === 'copy') { + // Copy operation + const response = await fetch('/api/file/copy', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + source: sourcePath, + destination: destPath + }) + }); + + if (!response.ok) throw new Error('Copy failed'); + showNotification(`Copied ${fileName} to ${contextMenuTarget.name}`, 'success'); + } else if (clipboard.operation === 'move') { + // Move operation + const response = await fetch('/api/file/move', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + source: sourcePath, + destination: destPath + }) + }); + + if (!response.ok) throw new Error('Move failed'); + showNotification(`Moved ${fileName} to ${contextMenuTarget.name}`, 'success'); + clipboard = null; // Clear clipboard after move + } + + loadFileTree(); + } catch (error) { + console.error('Error pasting:', error); + showNotification('Error pasting file', 'danger'); + } + } + + async function deleteItem() { + if (!confirm(`Are you sure you want to delete ${contextMenuTarget.name}?`)) { + return; + } + + try { + let response; + if (contextMenuTarget.type === 'directory') { + response = await fetch(`/api/directory?path=${encodeURIComponent(contextMenuTarget.path)}&recursive=true`, { + method: 'DELETE' + }); + } else { + response = await fetch(`/api/file?path=${encodeURIComponent(contextMenuTarget.path)}`, { + method: 'DELETE' + }); + } + + if (!response.ok) throw new Error('Delete failed'); + + showNotification(`Deleted ${contextMenuTarget.name}`, 'success'); + loadFileTree(); + } catch (error) { + console.error('Error deleting:', error); + showNotification('Error deleting', 'danger'); + } + } + + // ======================================================================== + // Notifications + // ======================================================================== + + function showNotification(message, type = 'info') { + let toastContainer = document.getElementById('toastContainer'); + if (!toastContainer) { + toastContainer = createToastContainer(); + } + + const toast = document.createElement('div'); + toast.className = `toast align-items-center text-white bg-${type} border-0`; + toast.setAttribute('role', 'alert'); + toast.innerHTML = ` +
+
${message}
+ +
+ `; + + toastContainer.appendChild(toast); + + const bsToast = new bootstrap.Toast(toast, { delay: 3000 }); + bsToast.show(); + + toast.addEventListener('hidden.bs.toast', () => { + toast.remove(); + }); + } + + function createToastContainer() { + const container = document.createElement('div'); + container.id = 'toastContainer'; + container.className = 'toast-container position-fixed top-0 end-0 p-3'; + container.style.zIndex = '9999'; + document.body.appendChild(container); + return container; + } + + // ======================================================================== + // Initialization + // ======================================================================== + + function init() { + initDarkMode(); + initEditor(); + loadFileTree(); + + document.getElementById('saveBtn').addEventListener('click', saveFile); + document.getElementById('deleteBtn').addEventListener('click', deleteFile); + document.getElementById('newFileBtn').addEventListener('click', newFile); + document.getElementById('newFolderBtn').addEventListener('click', createFolder); + document.getElementById('darkModeToggle').addEventListener('click', toggleDarkMode); + + // Context menu actions + document.querySelectorAll('.context-menu-item').forEach(item => { + item.addEventListener('click', () => { + const action = item.dataset.action; + handleContextMenuAction(action); + hideContextMenu(); + }); + }); + + document.addEventListener('keydown', (e) => { + if ((e.ctrlKey || e.metaKey) && e.key === 's') { + e.preventDefault(); + saveFile(); + } + }); + + console.log('Markdown Editor with File Tree initialized'); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})(); + diff --git a/static/app.js b/static/app.js new file mode 100644 index 0000000..7f18ca4 --- /dev/null +++ b/static/app.js @@ -0,0 +1,527 @@ +// Markdown Editor Application +(function() { + 'use strict'; + + // State management + let currentFile = null; + let editor = null; + let isScrollingSynced = true; + let scrollTimeout = null; + let isDarkMode = false; + + // Dark mode management + function initDarkMode() { + // Check for saved preference + const savedMode = localStorage.getItem('darkMode'); + if (savedMode === 'true') { + enableDarkMode(); + } + } + + function enableDarkMode() { + isDarkMode = true; + document.body.classList.add('dark-mode'); + document.getElementById('darkModeIcon').textContent = '☀️'; + localStorage.setItem('darkMode', 'true'); + + // Update mermaid theme + mermaid.initialize({ + startOnLoad: false, + theme: 'dark', + securityLevel: 'loose' + }); + + // Re-render preview if there's content + if (editor && editor.getValue()) { + updatePreview(); + } + } + + function disableDarkMode() { + isDarkMode = false; + document.body.classList.remove('dark-mode'); + document.getElementById('darkModeIcon').textContent = '🌙'; + localStorage.setItem('darkMode', 'false'); + + // Update mermaid theme + mermaid.initialize({ + startOnLoad: false, + theme: 'default', + securityLevel: 'loose' + }); + + // Re-render preview if there's content + if (editor && editor.getValue()) { + updatePreview(); + } + } + + function toggleDarkMode() { + if (isDarkMode) { + disableDarkMode(); + } else { + enableDarkMode(); + } + } + + // Initialize Mermaid + mermaid.initialize({ + startOnLoad: false, + theme: 'default', + securityLevel: 'loose' + }); + + // Configure marked.js for markdown parsing + marked.setOptions({ + breaks: true, + gfm: true, + headerIds: true, + mangle: false, + sanitize: false, // Allow HTML in markdown + smartLists: true, + smartypants: true, + xhtml: false + }); + + // Handle image upload + async function uploadImage(file) { + const formData = new FormData(); + formData.append('file', file); + + try { + const response = await fetch('/api/upload-image', { + method: 'POST', + body: formData + }); + + if (!response.ok) throw new Error('Upload failed'); + + const result = await response.json(); + return result.url; + } catch (error) { + console.error('Error uploading image:', error); + showNotification('Error uploading image', 'danger'); + return null; + } + } + + // Handle drag and drop + function setupDragAndDrop() { + const editorElement = document.querySelector('.CodeMirror'); + + // Prevent default drag behavior + ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { + editorElement.addEventListener(eventName, preventDefaults, false); + }); + + function preventDefaults(e) { + e.preventDefault(); + e.stopPropagation(); + } + + // Highlight drop zone + ['dragenter', 'dragover'].forEach(eventName => { + editorElement.addEventListener(eventName, () => { + editorElement.classList.add('drag-over'); + }, false); + }); + + ['dragleave', 'drop'].forEach(eventName => { + editorElement.addEventListener(eventName, () => { + editorElement.classList.remove('drag-over'); + }, false); + }); + + // Handle drop + editorElement.addEventListener('drop', async (e) => { + const files = e.dataTransfer.files; + + if (files.length === 0) return; + + // Filter for images only + const imageFiles = Array.from(files).filter(file => + file.type.startsWith('image/') + ); + + if (imageFiles.length === 0) { + showNotification('Please drop image files only', 'warning'); + return; + } + + showNotification(`Uploading ${imageFiles.length} image(s)...`, 'info'); + + // Upload images + for (const file of imageFiles) { + const url = await uploadImage(file); + if (url) { + // Insert markdown image syntax at cursor + const cursor = editor.getCursor(); + const imageMarkdown = `![${file.name}](${url})`; + editor.replaceRange(imageMarkdown, cursor); + editor.setCursor(cursor.line, cursor.ch + imageMarkdown.length); + showNotification(`Image uploaded: ${file.name}`, 'success'); + } + } + }, false); + + // Also handle paste events for images + editorElement.addEventListener('paste', async (e) => { + const items = e.clipboardData?.items; + if (!items) return; + + for (const item of items) { + if (item.type.startsWith('image/')) { + e.preventDefault(); + const file = item.getAsFile(); + if (file) { + showNotification('Uploading pasted image...', 'info'); + const url = await uploadImage(file); + if (url) { + const cursor = editor.getCursor(); + const imageMarkdown = `![pasted-image](${url})`; + editor.replaceRange(imageMarkdown, cursor); + showNotification('Image uploaded successfully', 'success'); + } + } + } + } + }); + } + + // Initialize CodeMirror editor + function initEditor() { + const textarea = document.getElementById('editor'); + editor = CodeMirror.fromTextArea(textarea, { + mode: 'markdown', + theme: 'monokai', + lineNumbers: true, + lineWrapping: true, + autofocus: true, + extraKeys: { + 'Ctrl-S': function() { saveFile(); }, + 'Cmd-S': function() { saveFile(); } + } + }); + + // Update preview on change + editor.on('change', debounce(updatePreview, 300)); + + // Setup drag and drop after editor is ready + setTimeout(setupDragAndDrop, 100); + + // Sync scroll + editor.on('scroll', handleEditorScroll); + } + + // Debounce function to limit update frequency + function debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; + } + + // Update preview with markdown content + async function updatePreview() { + const content = editor.getValue(); + const previewDiv = document.getElementById('preview'); + + if (!content.trim()) { + previewDiv.innerHTML = ` +
+

Preview

+

Start typing in the editor to see the preview

+
+ `; + return; + } + + try { + // Parse markdown to HTML + let html = marked.parse(content); + + // Replace mermaid code blocks with div containers + html = html.replace( + /
([\s\S]*?)<\/code><\/pre>/g,
+                '
$1
' + ); + + previewDiv.innerHTML = html; + + // Apply syntax highlighting to code blocks + const codeBlocks = previewDiv.querySelectorAll('pre code'); + codeBlocks.forEach(block => { + // Detect language from class name + const languageClass = Array.from(block.classList).find(cls => cls.startsWith('language-')); + if (languageClass && languageClass !== 'language-mermaid') { + Prism.highlightElement(block); + } + }); + + // Render mermaid diagrams + const mermaidElements = previewDiv.querySelectorAll('.mermaid'); + if (mermaidElements.length > 0) { + try { + await mermaid.run({ + nodes: mermaidElements, + suppressErrors: false + }); + } catch (error) { + console.error('Mermaid rendering error:', error); + } + } + } catch (error) { + console.error('Preview rendering error:', error); + previewDiv.innerHTML = ` + + `; + } + } + + // Handle editor scroll for synchronized scrolling + function handleEditorScroll() { + if (!isScrollingSynced) return; + + clearTimeout(scrollTimeout); + scrollTimeout = setTimeout(() => { + const editorScrollInfo = editor.getScrollInfo(); + const editorScrollPercentage = editorScrollInfo.top / (editorScrollInfo.height - editorScrollInfo.clientHeight); + + const previewPane = document.querySelector('.preview-pane'); + const previewScrollHeight = previewPane.scrollHeight - previewPane.clientHeight; + + if (previewScrollHeight > 0) { + previewPane.scrollTop = editorScrollPercentage * previewScrollHeight; + } + }, 10); + } + + // Load file list from server + async function loadFileList() { + try { + const response = await fetch('/api/files'); + if (!response.ok) throw new Error('Failed to load file list'); + + const files = await response.json(); + const fileListDiv = document.getElementById('fileList'); + + if (files.length === 0) { + fileListDiv.innerHTML = '
No files yet
'; + return; + } + + fileListDiv.innerHTML = files.map(file => ` + + ${file.filename} + ${formatFileSize(file.size)} + + `).join(''); + + // Add click handlers + document.querySelectorAll('.file-item').forEach(item => { + item.addEventListener('click', (e) => { + e.preventDefault(); + const filename = item.dataset.filename; + loadFile(filename); + }); + }); + } catch (error) { + console.error('Error loading file list:', error); + showNotification('Error loading file list', 'danger'); + } + } + + // Load a specific file + async function loadFile(filename) { + try { + const response = await fetch(`/api/files/${filename}`); + if (!response.ok) throw new Error('Failed to load file'); + + const data = await response.json(); + currentFile = data.filename; + + // Update UI + document.getElementById('filenameInput').value = data.filename; + editor.setValue(data.content); + + // Update active state in file list + document.querySelectorAll('.file-item').forEach(item => { + item.classList.toggle('active', item.dataset.filename === filename); + }); + + updatePreview(); + showNotification(`Loaded ${filename}`, 'success'); + } catch (error) { + console.error('Error loading file:', error); + showNotification('Error loading file', 'danger'); + } + } + + // Save current file + async function saveFile() { + const filename = document.getElementById('filenameInput').value.trim(); + + if (!filename) { + showNotification('Please enter a filename', 'warning'); + return; + } + + const content = editor.getValue(); + + try { + const response = await fetch('/api/files', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ filename, content }) + }); + + if (!response.ok) throw new Error('Failed to save file'); + + const result = await response.json(); + currentFile = result.filename; + + showNotification(`Saved ${result.filename}`, 'success'); + loadFileList(); + } catch (error) { + console.error('Error saving file:', error); + showNotification('Error saving file', 'danger'); + } + } + + // Delete current file + async function deleteFile() { + const filename = document.getElementById('filenameInput').value.trim(); + + if (!filename) { + showNotification('No file selected', 'warning'); + return; + } + + if (!confirm(`Are you sure you want to delete ${filename}?`)) { + return; + } + + try { + const response = await fetch(`/api/files/${filename}`, { + method: 'DELETE' + }); + + if (!response.ok) throw new Error('Failed to delete file'); + + showNotification(`Deleted ${filename}`, 'success'); + + // Clear editor + currentFile = null; + document.getElementById('filenameInput').value = ''; + editor.setValue(''); + updatePreview(); + + loadFileList(); + } catch (error) { + console.error('Error deleting file:', error); + showNotification('Error deleting file', 'danger'); + } + } + + // Create new file + function newFile() { + currentFile = null; + document.getElementById('filenameInput').value = ''; + editor.setValue(''); + updatePreview(); + + // Remove active state from all file items + document.querySelectorAll('.file-item').forEach(item => { + item.classList.remove('active'); + }); + + showNotification('New file created', 'info'); + } + + // Format file size for display + function formatFileSize(bytes) { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; + } + + // Show notification + function showNotification(message, type = 'info') { + // Create toast notification + const toastContainer = document.getElementById('toastContainer') || createToastContainer(); + + const toast = document.createElement('div'); + toast.className = `toast align-items-center text-white bg-${type} border-0`; + toast.setAttribute('role', 'alert'); + toast.setAttribute('aria-live', 'assertive'); + toast.setAttribute('aria-atomic', 'true'); + + toast.innerHTML = ` +
+
${message}
+ +
+ `; + + toastContainer.appendChild(toast); + + const bsToast = new bootstrap.Toast(toast, { delay: 3000 }); + bsToast.show(); + + toast.addEventListener('hidden.bs.toast', () => { + toast.remove(); + }); + } + + // Create toast container if it doesn't exist + function createToastContainer() { + const container = document.createElement('div'); + container.id = 'toastContainer'; + container.className = 'toast-container position-fixed top-0 end-0 p-3'; + container.style.zIndex = '9999'; + document.body.appendChild(container); + return container; + } + + // Initialize application + function init() { + initDarkMode(); + initEditor(); + loadFileList(); + + // Set up event listeners + document.getElementById('saveBtn').addEventListener('click', saveFile); + document.getElementById('deleteBtn').addEventListener('click', deleteFile); + document.getElementById('newFileBtn').addEventListener('click', newFile); + document.getElementById('darkModeToggle').addEventListener('click', toggleDarkMode); + + // Keyboard shortcuts + document.addEventListener('keydown', (e) => { + if ((e.ctrlKey || e.metaKey) && e.key === 's') { + e.preventDefault(); + saveFile(); + } + }); + + console.log('Markdown Editor initialized'); + } + + // Start application when DOM is ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})(); + diff --git a/static/css/components.css b/static/css/components.css new file mode 100644 index 0000000..545770d --- /dev/null +++ b/static/css/components.css @@ -0,0 +1,160 @@ +/* Preview pane styles */ +.preview-pane { + font-size: 16px; + line-height: 1.6; +} + +.preview-pane h1, .preview-pane h2, .preview-pane h3, +.preview-pane h4, .preview-pane h5, .preview-pane h6 { + margin-top: 24px; + margin-bottom: 16px; + font-weight: 600; + line-height: 1.25; + color: var(--text-primary); +} + +.preview-pane a { + color: var(--link-color); + text-decoration: none; +} + +.preview-pane a:hover { + text-decoration: underline; +} + +.preview-pane code { + background-color: var(--bg-tertiary); + padding: 2px 6px; + border-radius: 3px; + font-size: 85%; +} + +.preview-pane pre { + background-color: var(--bg-tertiary); + padding: 16px; + border-radius: 6px; + overflow-x: auto; +} + +.preview-pane pre code { + background-color: transparent; + padding: 0; +} + +.preview-pane table { + border-collapse: collapse; + width: 100%; + margin: 16px 0; +} + +.preview-pane table th, +.preview-pane table td { + border: 1px solid var(--border-color); + padding: 8px 12px; +} + +.preview-pane table th { + background-color: var(--bg-secondary); + font-weight: 600; +} + +.preview-pane blockquote { + border-left: 4px solid var(--border-color); + padding-left: 16px; + margin-left: 0; + color: var(--text-secondary); +} + +.preview-pane img { + max-width: 100%; + height: auto; +} + +/* Context Menu */ +.context-menu { + position: fixed; + background-color: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: 6px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + z-index: 10000; + min-width: 180px; + max-width: 200px; + width: auto; + padding: 4px 0; + display: none; +} + +body.dark-mode .context-menu { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); +} + +.context-menu-item { + padding: 8px 16px; + cursor: pointer; + display: flex; + align-items: center; + gap: 10px; + color: var(--text-primary); + transition: background-color 0.15s ease; +} + +.context-menu-item:hover { + background-color: var(--bg-tertiary); +} + +.context-menu-item i { + width: 16px; + text-align: center; +} + +.context-menu-divider { + height: 1px; + background-color: var(--border-color); + margin: 4px 0; +} + +/* Toast Notifications */ +.toast-container { + position: fixed; + top: 70px; + right: 20px; + z-index: 9999; +} + +.toast { + min-width: 250px; + margin-bottom: 10px; + background-color: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: 6px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + animation: slideIn 0.3s ease; +} + +@keyframes slideIn { + from { + transform: translateX(400px); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +.toast.hiding { + animation: slideOut 0.3s ease; +} + +@keyframes slideOut { + from { + transform: translateX(0); + opacity: 1; + } + to { + transform: translateX(400px); + opacity: 0; + } +} + diff --git a/static/css/editor.css b/static/css/editor.css new file mode 100644 index 0000000..6ee84a6 --- /dev/null +++ b/static/css/editor.css @@ -0,0 +1,75 @@ +/* Editor styles */ +.editor-header { + padding: 10px; + background-color: var(--bg-secondary); + border-bottom: 1px solid var(--border-color); + display: flex; + gap: 10px; + align-items: center; +} + +.editor-header input { + flex: 1; + padding: 6px 12px; + background-color: var(--bg-primary); + color: var(--text-primary); + border: 1px solid var(--border-color); + border-radius: 4px; +} + +.editor-container { + flex: 1; + overflow: hidden; +} + +/* CodeMirror customization */ +.CodeMirror { + height: 100%; + font-family: 'Consolas', 'Monaco', 'Courier New', monospace; + font-size: 14px; + background-color: var(--bg-primary); + color: var(--text-primary); +} + +body.dark-mode .CodeMirror { + background-color: #1c2128; + color: #e6edf3; +} + +.CodeMirror-gutters { + background-color: var(--bg-secondary); + border-right: 1px solid var(--border-color); +} + +body.dark-mode .CodeMirror-gutters { + background-color: #161b22; + border-right-color: #30363d; +} + +.CodeMirror-linenumber { + color: var(--text-secondary); +} + +.CodeMirror-cursor { + border-left-color: var(--text-primary); +} + +/* Drag and drop overlay */ +.editor-container.drag-over::after { + content: 'Drop images here'; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(9, 105, 218, 0.1); + border: 2px dashed var(--info-color); + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + color: var(--info-color); + pointer-events: none; + z-index: 1000; +} + diff --git a/static/css/file-tree.css b/static/css/file-tree.css new file mode 100644 index 0000000..7a5206d --- /dev/null +++ b/static/css/file-tree.css @@ -0,0 +1,88 @@ +/* File tree styles */ +.file-tree { + font-size: 14px; + user-select: none; +} + +.tree-node { + padding: 6px 8px; + cursor: pointer; + display: flex; + align-items: center; + gap: 8px; + border-radius: 4px; + margin: 2px 0; + color: var(--text-primary); + transition: background-color 0.15s ease; +} + +.tree-node:hover { + background-color: var(--bg-tertiary); +} + +.tree-node.active { + background-color: #0969da; + color: white; +} + +body.dark-mode .tree-node.active { + background-color: #1f6feb; +} + +.tree-node-icon { + width: 16px; + text-align: center; + flex-shrink: 0; +} + +.tree-node-content { + display: flex; + align-items: center; + gap: 6px; + flex: 1; + min-width: 0; +} + +.tree-node-name { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.tree-node-size { + font-size: 11px; + color: var(--text-secondary); + flex-shrink: 0; +} + +.tree-children { + margin-left: 16px; +} + +.tree-node.dragging { + opacity: 0.5; +} + +.tree-node.drag-over { + background-color: var(--info-color); + color: white; +} + +/* Collection selector */ +.collection-selector { + margin-bottom: 10px; + padding: 8px; + background-color: var(--bg-tertiary); + border-radius: 4px; +} + +.collection-selector select { + width: 100%; + padding: 6px; + background-color: var(--bg-primary); + color: var(--text-primary); + border: 1px solid var(--border-color); + border-radius: 4px; +} + diff --git a/static/css/layout.css b/static/css/layout.css new file mode 100644 index 0000000..e0a0248 --- /dev/null +++ b/static/css/layout.css @@ -0,0 +1,69 @@ +/* Base layout styles */ +html, body { + height: 100%; + margin: 0; + padding: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif; + background-color: var(--bg-primary); + color: var(--text-primary); + transition: background-color 0.3s ease, color 0.3s ease; +} + +.container-fluid { + height: calc(100% - 56px); +} + +.sidebar { + background-color: var(--bg-secondary); + border-right: 1px solid var(--border-color); + overflow-y: auto; + height: 100%; + transition: background-color 0.3s ease; +} + +.editor-pane { + background-color: var(--bg-primary); + height: 100%; + display: flex; + flex-direction: column; + border-right: 1px solid var(--border-color); +} + +.preview-pane { + background-color: var(--bg-primary); + height: 100%; + overflow-y: auto; + padding: 20px; +} + +/* Navbar */ +.navbar { + background-color: var(--bg-secondary); + border-bottom: 1px solid var(--border-color); + transition: background-color 0.3s ease; +} + +.navbar-brand { + color: var(--text-primary) !important; + font-weight: 600; +} + +/* Scrollbar styling */ +::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +::-webkit-scrollbar-track { + background: var(--bg-secondary); +} + +::-webkit-scrollbar-thumb { + background: var(--border-color); + border-radius: 5px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--text-secondary); +} + diff --git a/static/css/variables.css b/static/css/variables.css new file mode 100644 index 0000000..aa4e5ee --- /dev/null +++ b/static/css/variables.css @@ -0,0 +1,31 @@ +/* CSS Variables for theming */ +:root { + /* Light mode colors */ + --bg-primary: #ffffff; + --bg-secondary: #f6f8fa; + --bg-tertiary: #f0f0f0; + --text-primary: #24292f; + --text-secondary: #57606a; + --border-color: #d0d7de; + --link-color: #0969da; + --success-color: #2da44e; + --danger-color: #cf222e; + --warning-color: #bf8700; + --info-color: #0969da; +} + +body.dark-mode { + /* Dark mode colors - GitHub style */ + --bg-primary: #0d1117; + --bg-secondary: #161b22; + --bg-tertiary: #1c2128; + --text-primary: #e6edf3; + --text-secondary: #8d96a0; + --border-color: #30363d; + --link-color: #4fc3f7; + --success-color: #3fb950; + --danger-color: #f85149; + --warning-color: #d29922; + --info-color: #58a6ff; +} + diff --git a/static/js/app.js b/static/js/app.js new file mode 100644 index 0000000..8859f6b --- /dev/null +++ b/static/js/app.js @@ -0,0 +1,302 @@ +/** + * Main Application + * Coordinates all modules and handles user interactions + */ + +// Global state +let webdavClient; +let fileTree; +let editor; +let darkMode; +let collectionSelector; +let clipboard = null; +let currentFilePath = null; + +// Initialize application +document.addEventListener('DOMContentLoaded', async () => { + // Initialize WebDAV client + webdavClient = new WebDAVClient('/fs/'); + + // Initialize dark mode + darkMode = new DarkMode(); + document.getElementById('darkModeBtn').addEventListener('click', () => { + darkMode.toggle(); + }); + + // Initialize collection selector + collectionSelector = new CollectionSelector('collectionSelect', webdavClient); + collectionSelector.onChange = async (collection) => { + await fileTree.load(); + }; + await collectionSelector.load(); + + // Initialize file tree + fileTree = new FileTree('fileTree', webdavClient); + fileTree.onFileSelect = async (item) => { + await loadFile(item.path); + }; + await fileTree.load(); + + // Initialize editor + editor = new MarkdownEditor('editor', 'preview'); + + // Setup editor drop handler + const editorDropHandler = new EditorDropHandler( + document.querySelector('.editor-container'), + async (file) => { + await handleEditorFileDrop(file); + } + ); + + // Setup button handlers + document.getElementById('newBtn').addEventListener('click', () => { + newFile(); + }); + + document.getElementById('saveBtn').addEventListener('click', async () => { + await saveFile(); + }); + + document.getElementById('deleteBtn').addEventListener('click', async () => { + await deleteCurrentFile(); + }); + + // Setup context menu handlers + setupContextMenuHandlers(); + + // Initialize mermaid + mermaid.initialize({ startOnLoad: true, theme: darkMode.isDark ? 'dark' : 'default' }); +}); + +/** + * File Operations + */ +async function loadFile(path) { + try { + const content = await webdavClient.get(path); + editor.setValue(content); + document.getElementById('filenameInput').value = path; + currentFilePath = path; + showNotification('File loaded', 'success'); + } catch (error) { + console.error('Failed to load file:', error); + showNotification('Failed to load file', 'error'); + } +} + +function newFile() { + editor.setValue('# New File\n\nStart typing...\n'); + document.getElementById('filenameInput').value = ''; + document.getElementById('filenameInput').focus(); + currentFilePath = null; + showNotification('New file', 'info'); +} + +async function saveFile() { + const filename = document.getElementById('filenameInput').value.trim(); + if (!filename) { + showNotification('Please enter a filename', 'warning'); + return; + } + + try { + const content = editor.getValue(); + await webdavClient.put(filename, content); + currentFilePath = filename; + await fileTree.load(); + showNotification('Saved', 'success'); + } catch (error) { + console.error('Failed to save file:', error); + showNotification('Failed to save file', 'error'); + } +} + +async function deleteCurrentFile() { + if (!currentFilePath) { + showNotification('No file selected', 'warning'); + return; + } + + if (!confirm(`Delete ${currentFilePath}?`)) { + return; + } + + try { + await webdavClient.delete(currentFilePath); + await fileTree.load(); + newFile(); + showNotification('Deleted', 'success'); + } catch (error) { + console.error('Failed to delete file:', error); + showNotification('Failed to delete file', 'error'); + } +} + +/** + * Context Menu Handlers + */ +function setupContextMenuHandlers() { + const menu = document.getElementById('contextMenu'); + + menu.addEventListener('click', async (e) => { + const item = e.target.closest('.context-menu-item'); + if (!item) return; + + const action = item.dataset.action; + const targetPath = menu.dataset.targetPath; + const isDir = menu.dataset.targetIsDir === 'true'; + + hideContextMenu(); + + await handleContextAction(action, targetPath, isDir); + }); +} + +async function handleContextAction(action, targetPath, isDir) { + switch (action) { + case 'open': + if (!isDir) { + await loadFile(targetPath); + } + break; + + case 'new-file': + if (isDir) { + const filename = prompt('Enter filename:'); + if (filename) { + await fileTree.createFile(targetPath, filename); + } + } + break; + + case 'new-folder': + if (isDir) { + const foldername = prompt('Enter folder name:'); + if (foldername) { + await fileTree.createFolder(targetPath, foldername); + } + } + break; + + case 'upload': + if (isDir) { + showFileUploadDialog(targetPath, async (path, file) => { + await fileTree.uploadFile(path, file); + }); + } + break; + + case 'download': + if (isDir) { + await fileTree.downloadFolder(targetPath); + } else { + await fileTree.downloadFile(targetPath); + } + break; + + case 'rename': + const newName = prompt('Enter new name:', targetPath.split('/').pop()); + if (newName) { + const parentPath = targetPath.split('/').slice(0, -1).join('/'); + const newPath = parentPath ? `${parentPath}/${newName}` : newName; + try { + await webdavClient.move(targetPath, newPath); + await fileTree.load(); + showNotification('Renamed', 'success'); + } catch (error) { + console.error('Failed to rename:', error); + showNotification('Failed to rename', 'error'); + } + } + break; + + case 'copy': + clipboard = { path: targetPath, operation: 'copy' }; + showNotification('Copied to clipboard', 'info'); + updatePasteVisibility(); + break; + + case 'cut': + clipboard = { path: targetPath, operation: 'cut' }; + showNotification('Cut to clipboard', 'info'); + updatePasteVisibility(); + break; + + case 'paste': + if (clipboard && isDir) { + const filename = clipboard.path.split('/').pop(); + const destPath = `${targetPath}/${filename}`; + + try { + if (clipboard.operation === 'copy') { + await webdavClient.copy(clipboard.path, destPath); + showNotification('Copied', 'success'); + } else { + await webdavClient.move(clipboard.path, destPath); + showNotification('Moved', 'success'); + clipboard = null; + updatePasteVisibility(); + } + await fileTree.load(); + } catch (error) { + console.error('Failed to paste:', error); + showNotification('Failed to paste', 'error'); + } + } + break; + + case 'delete': + if (confirm(`Delete ${targetPath}?`)) { + try { + await webdavClient.delete(targetPath); + await fileTree.load(); + showNotification('Deleted', 'success'); + } catch (error) { + console.error('Failed to delete:', error); + showNotification('Failed to delete', 'error'); + } + } + break; + } +} + +function updatePasteVisibility() { + const pasteItem = document.getElementById('pasteMenuItem'); + if (pasteItem) { + pasteItem.style.display = clipboard ? 'block' : 'none'; + } +} + +/** + * Editor File Drop Handler + */ +async function handleEditorFileDrop(file) { + try { + // Get current file's directory + let targetDir = ''; + if (currentFilePath) { + const parts = currentFilePath.split('/'); + parts.pop(); // Remove filename + targetDir = parts.join('/'); + } + + // Upload file + const uploadedPath = await fileTree.uploadFile(targetDir, file); + + // Insert markdown link at cursor + const isImage = file.type.startsWith('image/'); + const link = isImage + ? `![${file.name}](/${webdavClient.currentCollection}/${uploadedPath})` + : `[${file.name}](/${webdavClient.currentCollection}/${uploadedPath})`; + + editor.insertAtCursor(link); + showNotification(`Uploaded and inserted link`, 'success'); + } catch (error) { + console.error('Failed to handle file drop:', error); + showNotification('Failed to upload file', 'error'); + } +} + +// Make showContextMenu global +window.showContextMenu = showContextMenu; + diff --git a/static/js/editor.js b/static/js/editor.js new file mode 100644 index 0000000..4ab9fbd --- /dev/null +++ b/static/js/editor.js @@ -0,0 +1,273 @@ +/** + * Editor Module + * Handles CodeMirror editor and markdown preview + */ + +class MarkdownEditor { + constructor(editorId, previewId, filenameInputId) { + this.editorElement = document.getElementById(editorId); + this.previewElement = document.getElementById(previewId); + this.filenameInput = document.getElementById(filenameInputId); + this.currentFile = null; + this.webdavClient = null; + + this.initCodeMirror(); + this.initMarkdown(); + this.initMermaid(); + } + + /** + * Initialize CodeMirror + */ + initCodeMirror() { + this.editor = CodeMirror(this.editorElement, { + mode: 'markdown', + theme: 'monokai', + lineNumbers: true, + lineWrapping: true, + autofocus: true, + extraKeys: { + 'Ctrl-S': () => this.save(), + 'Cmd-S': () => this.save() + } + }); + + // Update preview on change + this.editor.on('change', () => { + this.updatePreview(); + }); + + // Sync scroll + this.editor.on('scroll', () => { + this.syncScroll(); + }); + } + + /** + * Initialize markdown parser + */ + initMarkdown() { + this.marked = window.marked; + this.marked.setOptions({ + breaks: true, + gfm: true, + highlight: (code, lang) => { + if (lang && window.Prism.languages[lang]) { + return window.Prism.highlight(code, window.Prism.languages[lang], lang); + } + return code; + } + }); + } + + /** + * Initialize Mermaid + */ + initMermaid() { + if (window.mermaid) { + window.mermaid.initialize({ + startOnLoad: false, + theme: document.body.classList.contains('dark-mode') ? 'dark' : 'default' + }); + } + } + + /** + * Set WebDAV client + */ + setWebDAVClient(client) { + this.webdavClient = client; + } + + /** + * Load file + */ + async loadFile(path) { + try { + const content = await this.webdavClient.get(path); + this.currentFile = path; + this.filenameInput.value = path; + this.editor.setValue(content); + this.updatePreview(); + + if (window.showNotification) { + window.showNotification(`Loaded ${path}`, 'info'); + } + } catch (error) { + console.error('Failed to load file:', error); + if (window.showNotification) { + window.showNotification('Failed to load file', 'danger'); + } + } + } + + /** + * Save file + */ + async save() { + const path = this.filenameInput.value.trim(); + if (!path) { + if (window.showNotification) { + window.showNotification('Please enter a filename', 'warning'); + } + return; + } + + const content = this.editor.getValue(); + + try { + await this.webdavClient.put(path, content); + this.currentFile = path; + + if (window.showNotification) { + window.showNotification('✅ Saved', 'success'); + } + + // Trigger file tree reload + if (window.fileTree) { + await window.fileTree.load(); + window.fileTree.selectNode(path); + } + } catch (error) { + console.error('Failed to save file:', error); + if (window.showNotification) { + window.showNotification('Failed to save file', 'danger'); + } + } + } + + /** + * Create new file + */ + newFile() { + this.currentFile = null; + this.filenameInput.value = ''; + this.filenameInput.focus(); + this.editor.setValue(''); + this.updatePreview(); + + if (window.showNotification) { + window.showNotification('Enter filename and start typing', 'info'); + } + } + + /** + * Delete current file + */ + async deleteFile() { + if (!this.currentFile) { + if (window.showNotification) { + window.showNotification('No file selected', 'warning'); + } + return; + } + + if (!confirm(`Delete ${this.currentFile}?`)) { + return; + } + + try { + await this.webdavClient.delete(this.currentFile); + + if (window.showNotification) { + window.showNotification(`Deleted ${this.currentFile}`, 'success'); + } + + this.newFile(); + + // Trigger file tree reload + if (window.fileTree) { + await window.fileTree.load(); + } + } catch (error) { + console.error('Failed to delete file:', error); + if (window.showNotification) { + window.showNotification('Failed to delete file', 'danger'); + } + } + } + + /** + * Update preview + */ + updatePreview() { + const markdown = this.editor.getValue(); + let html = this.marked.parse(markdown); + + // Process mermaid diagrams + html = html.replace(/
([\s\S]*?)<\/code><\/pre>/g, (match, code) => {
+            const id = 'mermaid-' + Math.random().toString(36).substr(2, 9);
+            return `
${code}
`; + }); + + this.previewElement.innerHTML = html; + + // Render mermaid diagrams + if (window.mermaid) { + window.mermaid.init(undefined, this.previewElement.querySelectorAll('.mermaid')); + } + + // Highlight code blocks + if (window.Prism) { + window.Prism.highlightAllUnder(this.previewElement); + } + } + + /** + * Sync scroll between editor and preview + */ + syncScroll() { + const scrollInfo = this.editor.getScrollInfo(); + const scrollPercent = scrollInfo.top / (scrollInfo.height - scrollInfo.clientHeight); + + const previewHeight = this.previewElement.scrollHeight - this.previewElement.clientHeight; + this.previewElement.scrollTop = previewHeight * scrollPercent; + } + + /** + * Handle image upload + */ + async uploadImage(file) { + try { + const filename = await this.webdavClient.uploadImage(file); + const imageUrl = `/fs/${this.webdavClient.currentCollection}/images/${filename}`; + const markdown = `![${file.name}](${imageUrl})`; + + // Insert at cursor + this.editor.replaceSelection(markdown); + + if (window.showNotification) { + window.showNotification('Image uploaded', 'success'); + } + } catch (error) { + console.error('Failed to upload image:', error); + if (window.showNotification) { + window.showNotification('Failed to upload image', 'danger'); + } + } + } + + /** + * Get editor content + */ + getValue() { + return this.editor.getValue(); + } + + insertAtCursor(text) { + const doc = this.editor.getDoc(); + const cursor = doc.getCursor(); + doc.replaceRange(text, cursor); + } + + /** + * Set editor content + */ + setValue(content) { + this.editor.setValue(content); + } +} + +// Export for use in other modules +window.MarkdownEditor = MarkdownEditor; + diff --git a/static/js/file-tree.js b/static/js/file-tree.js new file mode 100644 index 0000000..10707a4 --- /dev/null +++ b/static/js/file-tree.js @@ -0,0 +1,290 @@ +/** + * File Tree Component + * Manages the hierarchical file tree display and interactions + */ + +class FileTree { + constructor(containerId, webdavClient) { + this.container = document.getElementById(containerId); + this.webdavClient = webdavClient; + this.tree = []; + this.selectedPath = null; + this.onFileSelect = null; + this.onFolderSelect = null; + + this.setupEventListeners(); + } + + setupEventListeners() { + // Click handler for tree nodes + this.container.addEventListener('click', (e) => { + const node = e.target.closest('.tree-node'); + if (!node) return; + + const path = node.dataset.path; + const isDir = node.dataset.isdir === 'true'; + + // Toggle folder + if (e.target.closest('.tree-toggle')) { + this.toggleFolder(node); + return; + } + + // Select node + if (isDir) { + this.selectFolder(path); + } else { + this.selectFile(path); + } + }); + + // Context menu + this.container.addEventListener('contextmenu', (e) => { + const node = e.target.closest('.tree-node'); + if (!node) return; + + e.preventDefault(); + const path = node.dataset.path; + const isDir = node.dataset.isdir === 'true'; + + window.showContextMenu(e.clientX, e.clientY, { path, isDir }); + }); + } + + async load() { + try { + const items = await this.webdavClient.propfind('', 'infinity'); + this.tree = this.webdavClient.buildTree(items); + this.render(); + } catch (error) { + console.error('Failed to load file tree:', error); + showNotification('Failed to load files', 'error'); + } + } + + render() { + this.container.innerHTML = ''; + this.renderNodes(this.tree, this.container, 0); + } + + renderNodes(nodes, parentElement, level) { + nodes.forEach(node => { + const nodeElement = this.createNodeElement(node, level); + parentElement.appendChild(nodeElement); + + if (node.children && node.children.length > 0) { + const childContainer = document.createElement('div'); + childContainer.className = 'tree-children'; + childContainer.style.display = 'none'; + nodeElement.appendChild(childContainer); + this.renderNodes(node.children, childContainer, level + 1); + } + }); + } + + createNodeElement(node, level) { + const div = document.createElement('div'); + div.className = 'tree-node'; + div.dataset.path = node.path; + div.dataset.isdir = node.isDirectory; + div.style.paddingLeft = `${level * 20 + 10}px`; + + // Toggle arrow for folders + if (node.isDirectory) { + const toggle = document.createElement('span'); + toggle.className = 'tree-toggle'; + toggle.innerHTML = ''; + div.appendChild(toggle); + } else { + const spacer = document.createElement('span'); + spacer.className = 'tree-spacer'; + spacer.style.width = '16px'; + spacer.style.display = 'inline-block'; + div.appendChild(spacer); + } + + // Icon + const icon = document.createElement('i'); + if (node.isDirectory) { + icon.className = 'bi bi-folder-fill'; + icon.style.color = '#dcb67a'; + } else { + icon.className = 'bi bi-file-earmark-text'; + icon.style.color = '#6a9fb5'; + } + div.appendChild(icon); + + // Name + const name = document.createElement('span'); + name.className = 'tree-name'; + name.textContent = node.name; + div.appendChild(name); + + // Size for files + if (!node.isDirectory && node.size) { + const size = document.createElement('span'); + size.className = 'tree-size'; + size.textContent = this.formatSize(node.size); + div.appendChild(size); + } + + return div; + } + + toggleFolder(nodeElement) { + const childContainer = nodeElement.querySelector('.tree-children'); + if (!childContainer) return; + + const toggle = nodeElement.querySelector('.tree-toggle i'); + const isExpanded = childContainer.style.display !== 'none'; + + if (isExpanded) { + childContainer.style.display = 'none'; + toggle.className = 'bi bi-chevron-right'; + } else { + childContainer.style.display = 'block'; + toggle.className = 'bi bi-chevron-down'; + } + } + + selectFile(path) { + this.selectedPath = path; + this.updateSelection(); + if (this.onFileSelect) { + this.onFileSelect({ path, isDirectory: false }); + } + } + + selectFolder(path) { + this.selectedPath = path; + this.updateSelection(); + if (this.onFolderSelect) { + this.onFolderSelect({ path, isDirectory: true }); + } + } + + updateSelection() { + // Remove previous selection + this.container.querySelectorAll('.tree-node').forEach(node => { + node.classList.remove('selected'); + }); + + // Add selection to current + if (this.selectedPath) { + const node = this.container.querySelector(`[data-path="${this.selectedPath}"]`); + if (node) { + node.classList.add('selected'); + } + } + } + + formatSize(bytes) { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return Math.round(bytes / Math.pow(k, i) * 10) / 10 + ' ' + sizes[i]; + } + + async createFile(parentPath, filename) { + try { + const fullPath = parentPath ? `${parentPath}/${filename}` : filename; + await this.webdavClient.put(fullPath, '# New File\n\nStart typing...\n'); + await this.load(); + showNotification('File created', 'success'); + return fullPath; + } catch (error) { + console.error('Failed to create file:', error); + showNotification('Failed to create file', 'error'); + throw error; + } + } + + async createFolder(parentPath, foldername) { + try { + const fullPath = parentPath ? `${parentPath}/${foldername}` : foldername; + await this.webdavClient.mkcol(fullPath); + await this.load(); + showNotification('Folder created', 'success'); + return fullPath; + } catch (error) { + console.error('Failed to create folder:', error); + showNotification('Failed to create folder', 'error'); + throw error; + } + } + + async uploadFile(parentPath, file) { + try { + const fullPath = parentPath ? `${parentPath}/${file.name}` : file.name; + const content = await file.arrayBuffer(); + await this.webdavClient.putBinary(fullPath, content); + await this.load(); + showNotification(`Uploaded ${file.name}`, 'success'); + return fullPath; + } catch (error) { + console.error('Failed to upload file:', error); + showNotification('Failed to upload file', 'error'); + throw error; + } + } + + async downloadFile(path) { + try { + const content = await this.webdavClient.get(path); + const filename = path.split('/').pop(); + this.triggerDownload(content, filename); + showNotification('Downloaded', 'success'); + } catch (error) { + console.error('Failed to download file:', error); + showNotification('Failed to download file', 'error'); + } + } + + async downloadFolder(path) { + try { + showNotification('Creating zip...', 'info'); + // Get all files in folder + const items = await this.webdavClient.propfind(path, 'infinity'); + const files = items.filter(item => !item.isDirectory); + + // Use JSZip to create zip file + const JSZip = window.JSZip; + if (!JSZip) { + throw new Error('JSZip not loaded'); + } + + const zip = new JSZip(); + const folder = zip.folder(path.split('/').pop() || 'download'); + + // Add all files to zip + for (const file of files) { + const content = await this.webdavClient.get(file.path); + const relativePath = file.path.replace(path + '/', ''); + folder.file(relativePath, content); + } + + // Generate zip + const zipBlob = await zip.generateAsync({ type: 'blob' }); + const zipFilename = `${path.split('/').pop() || 'download'}.zip`; + this.triggerDownload(zipBlob, zipFilename); + showNotification('Downloaded', 'success'); + } catch (error) { + console.error('Failed to download folder:', error); + showNotification('Failed to download folder', 'error'); + } + } + + triggerDownload(content, filename) { + const blob = content instanceof Blob ? content : new Blob([content]); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } +} + diff --git a/static/js/ui-utils.js b/static/js/ui-utils.js new file mode 100644 index 0000000..b076afa --- /dev/null +++ b/static/js/ui-utils.js @@ -0,0 +1,256 @@ +/** + * UI Utilities Module + * Toast notifications, context menu, dark mode, file upload dialog + */ + +/** + * Show toast notification + */ +function showNotification(message, type = 'info') { + const container = document.getElementById('toastContainer') || createToastContainer(); + + const toast = document.createElement('div'); + const bgClass = type === 'error' ? 'danger' : type === 'success' ? 'success' : type === 'warning' ? 'warning' : 'primary'; + toast.className = `toast align-items-center text-white bg-${bgClass} border-0`; + toast.setAttribute('role', 'alert'); + + toast.innerHTML = ` +
+
${message}
+ +
+ `; + + container.appendChild(toast); + + const bsToast = new bootstrap.Toast(toast, { delay: 3000 }); + bsToast.show(); + + toast.addEventListener('hidden.bs.toast', () => { + toast.remove(); + }); +} + +function createToastContainer() { + const container = document.createElement('div'); + container.id = 'toastContainer'; + container.className = 'toast-container position-fixed top-0 end-0 p-3'; + container.style.zIndex = '9999'; + document.body.appendChild(container); + return container; +} + +/** + * Enhanced Context Menu + */ +function showContextMenu(x, y, target) { + const menu = document.getElementById('contextMenu'); + if (!menu) return; + + // Store target + menu.dataset.targetPath = target.path; + menu.dataset.targetIsDir = target.isDir; + + // Show/hide menu items based on target type + const newFileItem = menu.querySelector('[data-action="new-file"]'); + const newFolderItem = menu.querySelector('[data-action="new-folder"]'); + const uploadItem = menu.querySelector('[data-action="upload"]'); + const downloadItem = menu.querySelector('[data-action="download"]'); + + if (target.isDir) { + // Folder context menu + if (newFileItem) newFileItem.style.display = 'block'; + if (newFolderItem) newFolderItem.style.display = 'block'; + if (uploadItem) uploadItem.style.display = 'block'; + if (downloadItem) downloadItem.style.display = 'block'; + } else { + // File context menu + if (newFileItem) newFileItem.style.display = 'none'; + if (newFolderItem) newFolderItem.style.display = 'none'; + if (uploadItem) uploadItem.style.display = 'none'; + if (downloadItem) downloadItem.style.display = 'block'; + } + + // Position menu + menu.style.display = 'block'; + menu.style.left = x + 'px'; + menu.style.top = y + 'px'; + + // Adjust if off-screen + const rect = menu.getBoundingClientRect(); + if (rect.right > window.innerWidth) { + menu.style.left = (window.innerWidth - rect.width - 10) + 'px'; + } + if (rect.bottom > window.innerHeight) { + menu.style.top = (window.innerHeight - rect.height - 10) + 'px'; + } +} + +function hideContextMenu() { + const menu = document.getElementById('contextMenu'); + if (menu) { + menu.style.display = 'none'; + } +} + +// Hide context menu on click outside +document.addEventListener('click', (e) => { + if (!e.target.closest('#contextMenu')) { + hideContextMenu(); + } +}); + +/** + * File Upload Dialog + */ +function showFileUploadDialog(targetPath, onUpload) { + const input = document.createElement('input'); + input.type = 'file'; + input.multiple = true; + + input.addEventListener('change', async (e) => { + const files = Array.from(e.target.files); + if (files.length === 0) return; + + for (const file of files) { + try { + await onUpload(targetPath, file); + } catch (error) { + console.error('Upload failed:', error); + } + } + }); + + input.click(); +} + +/** + * Dark Mode Manager + */ +class DarkMode { + constructor() { + this.isDark = localStorage.getItem('darkMode') === 'true'; + this.apply(); + } + + toggle() { + this.isDark = !this.isDark; + localStorage.setItem('darkMode', this.isDark); + this.apply(); + } + + apply() { + if (this.isDark) { + document.body.classList.add('dark-mode'); + const btn = document.getElementById('darkModeBtn'); + if (btn) btn.textContent = '☀️'; + + // Update mermaid theme + if (window.mermaid) { + mermaid.initialize({ theme: 'dark' }); + } + } else { + document.body.classList.remove('dark-mode'); + const btn = document.getElementById('darkModeBtn'); + if (btn) btn.textContent = '🌙'; + + // Update mermaid theme + if (window.mermaid) { + mermaid.initialize({ theme: 'default' }); + } + } + } +} + +/** + * Collection Selector + */ +class CollectionSelector { + constructor(selectId, webdavClient) { + this.select = document.getElementById(selectId); + this.webdavClient = webdavClient; + this.onChange = null; + } + + async load() { + try { + const collections = await this.webdavClient.getCollections(); + this.select.innerHTML = ''; + + collections.forEach(collection => { + const option = document.createElement('option'); + option.value = collection; + option.textContent = collection; + this.select.appendChild(option); + }); + + // Select first collection + if (collections.length > 0) { + this.select.value = collections[0]; + this.webdavClient.setCollection(collections[0]); + if (this.onChange) { + this.onChange(collections[0]); + } + } + + // Add change listener + this.select.addEventListener('change', () => { + const collection = this.select.value; + this.webdavClient.setCollection(collection); + if (this.onChange) { + this.onChange(collection); + } + }); + } catch (error) { + console.error('Failed to load collections:', error); + showNotification('Failed to load collections', 'error'); + } + } +} + +/** + * Editor Drop Handler + * Handles file drops into the editor + */ +class EditorDropHandler { + constructor(editorElement, onFileDrop) { + this.editorElement = editorElement; + this.onFileDrop = onFileDrop; + this.setupHandlers(); + } + + setupHandlers() { + this.editorElement.addEventListener('dragover', (e) => { + e.preventDefault(); + e.stopPropagation(); + this.editorElement.classList.add('drag-over'); + }); + + this.editorElement.addEventListener('dragleave', (e) => { + e.preventDefault(); + e.stopPropagation(); + this.editorElement.classList.remove('drag-over'); + }); + + this.editorElement.addEventListener('drop', async (e) => { + e.preventDefault(); + e.stopPropagation(); + this.editorElement.classList.remove('drag-over'); + + const files = Array.from(e.dataTransfer.files); + if (files.length === 0) return; + + for (const file of files) { + try { + if (this.onFileDrop) { + await this.onFileDrop(file); + } + } catch (error) { + console.error('Drop failed:', error); + showNotification(`Failed to upload ${file.name}`, 'error'); + } + } + }); + } +} + diff --git a/static/js/webdav-client.js b/static/js/webdav-client.js new file mode 100644 index 0000000..2b93cb5 --- /dev/null +++ b/static/js/webdav-client.js @@ -0,0 +1,239 @@ +/** + * WebDAV Client + * Handles all WebDAV protocol operations + */ + +class WebDAVClient { + constructor(baseUrl) { + this.baseUrl = baseUrl; + this.currentCollection = null; + } + + setCollection(collection) { + this.currentCollection = collection; + } + + getFullUrl(path) { + if (!this.currentCollection) { + throw new Error('No collection selected'); + } + const cleanPath = path.startsWith('/') ? path.slice(1) : path; + return `${this.baseUrl}${this.currentCollection}/${cleanPath}`; + } + + async getCollections() { + const response = await fetch(this.baseUrl); + if (!response.ok) { + throw new Error('Failed to get collections'); + } + return await response.json(); + } + + async propfind(path = '', depth = '1') { + const url = this.getFullUrl(path); + const response = await fetch(url, { + method: 'PROPFIND', + headers: { + 'Depth': depth, + 'Content-Type': 'application/xml' + } + }); + + if (!response.ok) { + throw new Error(`PROPFIND failed: ${response.statusText}`); + } + + const xml = await response.text(); + return this.parseMultiStatus(xml); + } + + async get(path) { + const url = this.getFullUrl(path); + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`GET failed: ${response.statusText}`); + } + + return await response.text(); + } + + async getBinary(path) { + const url = this.getFullUrl(path); + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`GET failed: ${response.statusText}`); + } + + return await response.blob(); + } + + async put(path, content) { + const url = this.getFullUrl(path); + const response = await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'text/plain' + }, + body: content + }); + + if (!response.ok) { + throw new Error(`PUT failed: ${response.statusText}`); + } + + return true; + } + + async putBinary(path, content) { + const url = this.getFullUrl(path); + const response = await fetch(url, { + method: 'PUT', + body: content + }); + + if (!response.ok) { + throw new Error(`PUT failed: ${response.statusText}`); + } + + return true; + } + + async delete(path) { + const url = this.getFullUrl(path); + const response = await fetch(url, { + method: 'DELETE' + }); + + if (!response.ok) { + throw new Error(`DELETE failed: ${response.statusText}`); + } + + return true; + } + + async copy(sourcePath, destPath) { + const sourceUrl = this.getFullUrl(sourcePath); + const destUrl = this.getFullUrl(destPath); + + const response = await fetch(sourceUrl, { + method: 'COPY', + headers: { + 'Destination': destUrl + } + }); + + if (!response.ok) { + throw new Error(`COPY failed: ${response.statusText}`); + } + + return true; + } + + async move(sourcePath, destPath) { + const sourceUrl = this.getFullUrl(sourcePath); + const destUrl = this.getFullUrl(destPath); + + const response = await fetch(sourceUrl, { + method: 'MOVE', + headers: { + 'Destination': destUrl + } + }); + + if (!response.ok) { + throw new Error(`MOVE failed: ${response.statusText}`); + } + + return true; + } + + async mkcol(path) { + const url = this.getFullUrl(path); + const response = await fetch(url, { + method: 'MKCOL' + }); + + if (!response.ok && response.status !== 405) { // 405 means already exists + throw new Error(`MKCOL failed: ${response.statusText}`); + } + + return true; + } + + parseMultiStatus(xml) { + const parser = new DOMParser(); + const doc = parser.parseFromString(xml, 'text/xml'); + const responses = doc.getElementsByTagNameNS('DAV:', 'response'); + + const items = []; + for (let i = 0; i < responses.length; i++) { + const response = responses[i]; + const href = response.getElementsByTagNameNS('DAV:', 'href')[0].textContent; + const propstat = response.getElementsByTagNameNS('DAV:', 'propstat')[0]; + const prop = propstat.getElementsByTagNameNS('DAV:', 'prop')[0]; + + // Check if it's a collection (directory) + const resourcetype = prop.getElementsByTagNameNS('DAV:', 'resourcetype')[0]; + const isDirectory = resourcetype.getElementsByTagNameNS('DAV:', 'collection').length > 0; + + // Get size + const contentlengthEl = prop.getElementsByTagNameNS('DAV:', 'getcontentlength')[0]; + const size = contentlengthEl ? parseInt(contentlengthEl.textContent) : 0; + + // Extract path relative to collection + const pathParts = href.split(`/${this.currentCollection}/`); + const relativePath = pathParts.length > 1 ? pathParts[1] : ''; + + // Skip the collection root itself + if (!relativePath) continue; + + // Remove trailing slash from directories + const cleanPath = relativePath.endsWith('/') ? relativePath.slice(0, -1) : relativePath; + + items.push({ + path: cleanPath, + name: cleanPath.split('/').pop(), + isDirectory, + size + }); + } + + return items; + } + + buildTree(items) { + const root = []; + const map = {}; + + // Sort items by path depth and name + items.sort((a, b) => { + const depthA = a.path.split('/').length; + const depthB = b.path.split('/').length; + if (depthA !== depthB) return depthA - depthB; + return a.path.localeCompare(b.path); + }); + + items.forEach(item => { + const parts = item.path.split('/'); + const parentPath = parts.slice(0, -1).join('/'); + + const node = { + ...item, + children: [] + }; + + map[item.path] = node; + + if (parentPath && map[parentPath]) { + map[parentPath].children.push(node); + } else { + root.push(node); + } + }); + + return root; + } +} + diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..ed66b87 --- /dev/null +++ b/static/style.css @@ -0,0 +1,594 @@ +/* CSS Variables for theming */ +:root { + --bg-primary: #ffffff; + --bg-secondary: #f8f9fa; + --bg-tertiary: #f6f8fa; + --text-primary: #24292e; + --text-secondary: #6a737d; + --border-color: #dee2e6; + --border-light: #eaecef; + --link-color: #0366d6; + --accent-color: #0d6efd; + --code-bg: rgba(27, 31, 35, 0.05); + --scrollbar-track: #f1f1f1; + --scrollbar-thumb: #888; + --scrollbar-thumb-hover: #555; +} + +/* Dark mode variables */ +body.dark-mode { + --bg-primary: #0d1117; + --bg-secondary: #161b22; + --bg-tertiary: #1c2128; + --text-primary: #e6edf3; + --text-secondary: #8b949e; + --border-color: #30363d; + --border-light: #21262d; + --link-color: #58a6ff; + --accent-color: #1f6feb; + --code-bg: rgba(110, 118, 129, 0.15); + --scrollbar-track: #161b22; + --scrollbar-thumb: #484f58; + --scrollbar-thumb-hover: #6e7681; +} + +/* Global styles */ +html, body { + height: 100%; + margin: 0; + padding: 0; + overflow: hidden; +} + +body { + display: flex; + flex-direction: column; + background-color: var(--bg-primary); + color: var(--text-primary); + transition: background-color 0.3s ease, color 0.3s ease; +} + +.container-fluid { + flex: 1; + padding: 0; + overflow: hidden; +} + +.row { + margin: 0; +} + +/* Navbar */ +.navbar { + z-index: 1000; + background-color: var(--bg-secondary) !important; + border-bottom: 1px solid var(--border-color); + transition: background-color 0.3s ease; +} + +.navbar-brand { + color: var(--text-primary) !important; +} + +.btn { + transition: all 0.2s ease; +} + +/* Dark mode toggle button */ +.dark-mode-toggle { + background: none; + border: 1px solid var(--border-color); + color: var(--text-primary); + padding: 0.375rem 0.75rem; + border-radius: 0.25rem; + cursor: pointer; + font-size: 1.2rem; + transition: all 0.2s ease; +} + +.dark-mode-toggle:hover { + background-color: var(--bg-tertiary); +} + +/* Sidebar */ +.sidebar { + height: calc(100vh - 56px); + overflow-y: auto; + padding: 0; + background-color: var(--bg-secondary); + border-right: 1px solid var(--border-color); + transition: background-color 0.3s ease; +} + +.sidebar h6 { + color: var(--text-primary); + background-color: var(--bg-tertiary); + transition: background-color 0.3s ease; +} + +.list-group-item { + cursor: pointer; + border-radius: 0; + border-left: 0; + border-right: 0; + background-color: var(--bg-secondary); + color: var(--text-primary); + border-color: var(--border-color); + transition: background-color 0.2s ease, color 0.2s ease; +} + +.list-group-item:first-child { + border-top: 0; +} + +.list-group-item.active { + background-color: var(--accent-color); + border-color: var(--accent-color); + color: #ffffff; +} + +.list-group-item:hover:not(.active) { + background-color: var(--bg-tertiary); +} + +/* Editor pane */ +.editor-pane { + height: calc(100vh - 56px); + display: flex; + flex-direction: column; + padding: 0; + border-right: 1px solid var(--border-color); + background-color: var(--bg-primary); + transition: background-color 0.3s ease; +} + +.editor-pane input[type="text"] { + background-color: var(--bg-primary); + color: var(--text-primary); + border-color: var(--border-color); + transition: background-color 0.3s ease, color 0.3s ease; +} + +.editor-pane input[type="text"]:focus { + background-color: var(--bg-primary); + color: var(--text-primary); + border-color: var(--accent-color); +} + +#editorContainer { + flex: 1; + overflow: hidden; +} + +.CodeMirror { + height: 100%; + font-family: 'Consolas', 'Monaco', 'Courier New', monospace; + font-size: 14px; + line-height: 1.6; + transition: all 0.2s ease; +} + +.CodeMirror.drag-over { + border: 3px dashed var(--accent-color); + background-color: rgba(13, 110, 253, 0.05); +} + +/* Dark mode CodeMirror adjustments */ +body.dark-mode .CodeMirror { + background-color: #1c2128; + color: #e6edf3; +} + +body.dark-mode .CodeMirror-gutters { + background-color: #161b22; + border-right: 1px solid #30363d; +} + +body.dark-mode .CodeMirror-linenumber { + color: #8b949e; +} + +/* Preview pane */ +.preview-pane { + height: calc(100vh - 56px); + overflow-y: auto; + background-color: var(--bg-primary); + padding: 0; + transition: background-color 0.3s ease; +} + +#preview { + padding: 20px; + max-width: 100%; + word-wrap: break-word; + color: var(--text-primary); + transition: color 0.3s ease; +} + +/* Markdown preview styles */ +#preview h1, #preview h2, #preview h3, #preview h4, #preview h5, #preview h6 { + margin-top: 24px; + margin-bottom: 16px; + font-weight: 600; + line-height: 1.25; + color: var(--text-primary); +} + +#preview h1 { + font-size: 2em; + border-bottom: 1px solid var(--border-light); + padding-bottom: 0.3em; +} + +#preview h2 { + font-size: 1.5em; + border-bottom: 1px solid var(--border-light); + padding-bottom: 0.3em; +} + +#preview h3 { + font-size: 1.25em; +} + +#preview p { + margin-bottom: 16px; + line-height: 1.6; + color: var(--text-primary); +} + +#preview code { + background-color: var(--code-bg); + border-radius: 3px; + font-size: 85%; + margin: 0; + padding: 0.2em 0.4em; + font-family: 'Consolas', 'Monaco', 'Courier New', monospace; + color: var(--text-primary); +} + +#preview pre { + background-color: #2d2d2d !important; + border-radius: 6px; + padding: 16px; + overflow: auto; + line-height: 1.45; + margin-bottom: 16px; +} + +#preview pre code { + background-color: transparent !important; + border: 0; + display: block; + line-height: inherit; + margin: 0; + overflow: visible; + padding: 0 !important; + word-wrap: normal; + font-family: 'Consolas', 'Monaco', 'Courier New', monospace; + font-size: 14px; +} + +/* Prism theme override for better visibility */ +#preview pre[class*="language-"] { + background-color: #2d2d2d !important; + margin: 0; +} + +#preview code[class*="language-"] { + background-color: transparent !important; +} + +#preview blockquote { + border-left: 4px solid var(--border-light); + color: var(--text-secondary); + padding-left: 16px; + margin-left: 0; + margin-bottom: 16px; +} + +#preview ul, #preview ol { + margin-bottom: 16px; + padding-left: 2em; +} + +#preview li { + margin-bottom: 4px; +} + +#preview table { + border-collapse: collapse; + width: 100%; + margin-bottom: 16px; +} + +#preview table th, +#preview table td { + border: 1px solid var(--border-light); + padding: 6px 13px; +} + +#preview table th { + background-color: var(--bg-tertiary); + font-weight: 600; +} + +#preview table tr:nth-child(2n) { + background-color: var(--bg-tertiary); +} + +#preview img { + max-width: 100%; + height: auto; + margin: 16px 0; +} + +#preview hr { + height: 4px; + padding: 0; + margin: 24px 0; + background-color: var(--border-light); + border: 0; +} + +#preview a { + color: var(--link-color); + text-decoration: none; +} + +#preview a:hover { + text-decoration: underline; +} + +/* Mermaid diagrams */ +.mermaid { + text-align: center; + margin: 20px 0; +} + +/* Dark mode mermaid adjustments */ +body.dark-mode .mermaid { + filter: invert(0.9) hue-rotate(180deg); +} + +body.dark-mode .mermaid svg { + background-color: transparent !important; +} + +/* Scrollbar styling */ +::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +::-webkit-scrollbar-track { + background: var(--scrollbar-track); +} + +::-webkit-scrollbar-thumb { + background: var(--scrollbar-thumb); + border-radius: 5px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--scrollbar-thumb-hover); +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .sidebar { + display: none; + } + + .editor-pane, + .preview-pane { + height: 50vh; + } +} + +/* File list item styling */ +.file-item { + display: flex; + justify-content: space-between; + align-items: center; +} + +.file-name { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.file-size { + font-size: 0.75rem; + color: var(--text-secondary); + margin-left: 8px; +} + +/* Toast notifications dark mode */ +body.dark-mode .toast { + background-color: var(--bg-secondary); + color: var(--text-primary); + border: 1px solid var(--border-color); +} + +body.dark-mode .toast-header { + background-color: var(--bg-tertiary); + color: var(--text-primary); + border-bottom: 1px solid var(--border-color); +} + + + +/* File Tree Styles */ +.file-tree { + user-select: none; + font-size: 14px; +} + +.tree-node { + padding: 4px 8px; + cursor: pointer; + border-radius: 4px; + transition: background-color 0.15s ease; + display: flex; + align-items: center; + gap: 6px; +} + +.tree-node:hover { + background-color: var(--bg-tertiary); +} + +.tree-node.active { + background-color: var(--accent-color); + color: white; +} + +.tree-node.drag-over { + background-color: rgba(13, 110, 253, 0.2); + border: 2px dashed var(--accent-color); +} + +.tree-node-content { + display: flex; + align-items: center; + gap: 6px; + flex: 1; +} + +.tree-node-icon { + font-size: 16px; + width: 16px; + text-align: center; +} + +.tree-node-name { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.tree-node-toggle { + width: 16px; + height: 16px; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + transition: transform 0.2s ease; +} + +.tree-node-toggle.expanded { + transform: rotate(90deg); +} + +.tree-children { + margin-left: 16px; + border-left: 1px solid var(--border-color); + padding-left: 8px; +} + +.tree-children.collapsed { + display: none; +} + +/* Context Menu */ +.context-menu { + position: fixed; + background-color: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: 6px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + z-index: 10000; + min-width: 180px; + max-width: 200px; + width: auto; + padding: 4px 0; +} + +body.dark-mode .context-menu { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); +} + +.context-menu-item { + padding: 8px 16px; + cursor: pointer; + display: flex; + align-items: center; + gap: 10px; + color: var(--text-primary); + transition: background-color 0.15s ease; +} + +.context-menu-item:hover { + background-color: var(--bg-tertiary); +} + +.context-menu-item i { + width: 16px; + text-align: center; +} + +.context-menu-divider { + height: 1px; + background-color: var(--border-color); + margin: 4px 0; +} + +/* Drag and Drop */ +.dragging { + opacity: 0.5; +} + +.drop-indicator { + height: 2px; + background-color: var(--accent-color); + margin: 2px 0; +} + +/* Modal for rename/new folder */ +.modal-backdrop.show { + opacity: 0.5; +} + +/* File size badge */ +.file-size-badge { + font-size: 10px; + color: var(--text-secondary); + margin-left: auto; +} + + + +/* Dark mode tree and sidebar fixes */ +body.dark-mode .sidebar { + background-color: var(--bg-secondary); +} + +body.dark-mode .tree-node { + color: var(--text-primary); +} + +body.dark-mode .tree-node:hover { + background-color: var(--bg-tertiary); +} + +body.dark-mode .tree-node-name { + color: var(--text-primary); +} + +body.dark-mode .tree-node-size { + color: var(--text-secondary); +} + +body.dark-mode .sidebar h6 { + color: var(--text-primary); + background-color: var(--bg-tertiary); +} + +body.dark-mode .tree-children { + border-left-color: var(--border-color); +} + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..ba0d5f3 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,164 @@ + + + + + + + Markdown Editor + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + +
+
+ +
+
+
+
+
+ + +
+

Preview

+
+

Start typing in the editor to see the preview

+
+
+
+
+ + +
+
+ Open +
+
+
+ New File +
+
+ New Folder +
+
+ Upload File +
+
+
+ Download +
+
+ Rename +
+
+ Copy +
+
+ Cut +
+ +
+
+ Delete +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..c570fbc --- /dev/null +++ b/uv.lock @@ -0,0 +1,548 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[[package]] +name = "annotated-doc" +version = "0.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/a6/dc46877b911e40c00d395771ea710d5e77b6de7bacd5fdcd78d70cc5a48f/annotated_doc-0.0.3.tar.gz", hash = "sha256:e18370014c70187422c33e945053ff4c286f453a984eba84d0dbfa0c935adeda", size = 5535, upload-time = "2025-10-24T14:57:10.718Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/b7/cf592cb5de5cb3bade3357f8d2cf42bf103bbe39f459824b4939fd212911/annotated_doc-0.0.3-py3-none-any.whl", hash = "sha256:348ec6664a76f1fd3be81f43dffbee4c7e8ce931ba71ec67cc7f4ade7fbbb580", size = 5488, upload-time = "2025-10-24T14:57:09.462Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, +] + +[[package]] +name = "click" +version = "8.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "fastapi" +version = "0.120.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/0e/7f29e8f7219e4526747db182e1afb5a4b6abc3201768fb38d81fa2536241/fastapi-0.120.0.tar.gz", hash = "sha256:6ce2c1cfb7000ac14ffd8ddb2bc12e62d023a36c20ec3710d09d8e36fab177a0", size = 337603, upload-time = "2025-10-23T20:56:34.743Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/60/7a639ceaba54aec4e1d5676498c568abc654b95762d456095b6cb529b1ca/fastapi-0.120.0-py3-none-any.whl", hash = "sha256:84009182e530c47648da2f07eb380b44b69889a4acfd9e9035ee4605c5cfc469", size = 108243, upload-time = "2025-10-23T20:56:33.281Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httptools" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/08/17e07e8d89ab8f343c134616d72eebfe03798835058e2ab579dcc8353c06/httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657", size = 206521, upload-time = "2025-10-10T03:54:31.002Z" }, + { url = "https://files.pythonhosted.org/packages/aa/06/c9c1b41ff52f16aee526fd10fbda99fa4787938aa776858ddc4a1ea825ec/httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70", size = 110375, upload-time = "2025-10-10T03:54:31.941Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cc/10935db22fda0ee34c76f047590ca0a8bd9de531406a3ccb10a90e12ea21/httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df", size = 456621, upload-time = "2025-10-10T03:54:33.176Z" }, + { url = "https://files.pythonhosted.org/packages/0e/84/875382b10d271b0c11aa5d414b44f92f8dd53e9b658aec338a79164fa548/httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e", size = 454954, upload-time = "2025-10-10T03:54:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/30/e1/44f89b280f7e46c0b1b2ccee5737d46b3bb13136383958f20b580a821ca0/httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274", size = 440175, upload-time = "2025-10-10T03:54:35.942Z" }, + { url = "https://files.pythonhosted.org/packages/6f/7e/b9287763159e700e335028bc1824359dc736fa9b829dacedace91a39b37e/httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec", size = 440310, upload-time = "2025-10-10T03:54:37.1Z" }, + { url = "https://files.pythonhosted.org/packages/b3/07/5b614f592868e07f5c94b1f301b5e14a21df4e8076215a3bccb830a687d8/httptools-0.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb", size = 86875, upload-time = "2025-10-10T03:54:38.421Z" }, + { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, + { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, + { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" }, + { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" }, + { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" }, + { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, + { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" }, + { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" }, + { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" }, + { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" }, + { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" }, + { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" }, + { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" }, + { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" }, + { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "markdown-editor" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "fastapi" }, + { name = "pydantic" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[package.metadata] +requires-dist = [ + { name = "fastapi", specifier = ">=0.104.0" }, + { name = "pydantic", specifier = ">=2.5.0" }, + { name = "uvicorn", extras = ["standard"], specifier = ">=0.24.0" }, +] + +[[package]] +name = "pydantic" +version = "2.12.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/1e/4f0a3233767010308f2fd6bd0814597e3f63f1dc98304a9112b8759df4ff/pydantic-2.12.3.tar.gz", hash = "sha256:1da1c82b0fc140bb0103bc1441ffe062154c8d38491189751ee00fd8ca65ce74", size = 819383, upload-time = "2025-10-17T15:04:21.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/6b/83661fa77dcefa195ad5f8cd9af3d1a7450fd57cc883ad04d65446ac2029/pydantic-2.12.3-py3-none-any.whl", hash = "sha256:6986454a854bc3bc6e5443e1369e06a3a456af9d339eda45510f517d9ea5c6bf", size = 462431, upload-time = "2025-10-17T15:04:19.346Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/18/d0944e8eaaa3efd0a91b0f1fc537d3be55ad35091b6a87638211ba691964/pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5", size = 457557, upload-time = "2025-10-14T10:23:47.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/4c/f6cbfa1e8efacd00b846764e8484fe173d25b8dab881e277a619177f3384/pydantic_core-2.41.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:28ff11666443a1a8cf2a044d6a545ebffa8382b5f7973f22c36109205e65dc80", size = 2109062, upload-time = "2025-10-14T10:20:04.486Z" }, + { url = "https://files.pythonhosted.org/packages/21/f8/40b72d3868896bfcd410e1bd7e516e762d326201c48e5b4a06446f6cf9e8/pydantic_core-2.41.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61760c3925d4633290292bad462e0f737b840508b4f722247d8729684f6539ae", size = 1916301, upload-time = "2025-10-14T10:20:06.857Z" }, + { url = "https://files.pythonhosted.org/packages/94/4d/d203dce8bee7faeca791671c88519969d98d3b4e8f225da5b96dad226fc8/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eae547b7315d055b0de2ec3965643b0ab82ad0106a7ffd29615ee9f266a02827", size = 1968728, upload-time = "2025-10-14T10:20:08.353Z" }, + { url = "https://files.pythonhosted.org/packages/65/f5/6a66187775df87c24d526985b3a5d78d861580ca466fbd9d4d0e792fcf6c/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef9ee5471edd58d1fcce1c80ffc8783a650e3e3a193fe90d52e43bb4d87bff1f", size = 2050238, upload-time = "2025-10-14T10:20:09.766Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b9/78336345de97298cf53236b2f271912ce11f32c1e59de25a374ce12f9cce/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15dd504af121caaf2c95cb90c0ebf71603c53de98305621b94da0f967e572def", size = 2249424, upload-time = "2025-10-14T10:20:11.732Z" }, + { url = "https://files.pythonhosted.org/packages/99/bb/a4584888b70ee594c3d374a71af5075a68654d6c780369df269118af7402/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a926768ea49a8af4d36abd6a8968b8790f7f76dd7cbd5a4c180db2b4ac9a3a2", size = 2366047, upload-time = "2025-10-14T10:20:13.647Z" }, + { url = "https://files.pythonhosted.org/packages/5f/8d/17fc5de9d6418e4d2ae8c675f905cdafdc59d3bf3bf9c946b7ab796a992a/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6916b9b7d134bff5440098a4deb80e4cb623e68974a87883299de9124126c2a8", size = 2071163, upload-time = "2025-10-14T10:20:15.307Z" }, + { url = "https://files.pythonhosted.org/packages/54/e7/03d2c5c0b8ed37a4617430db68ec5e7dbba66358b629cd69e11b4d564367/pydantic_core-2.41.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5cf90535979089df02e6f17ffd076f07237efa55b7343d98760bde8743c4b265", size = 2190585, upload-time = "2025-10-14T10:20:17.3Z" }, + { url = "https://files.pythonhosted.org/packages/be/fc/15d1c9fe5ad9266a5897d9b932b7f53d7e5cfc800573917a2c5d6eea56ec/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7533c76fa647fade2d7ec75ac5cc079ab3f34879626dae5689b27790a6cf5a5c", size = 2150109, upload-time = "2025-10-14T10:20:19.143Z" }, + { url = "https://files.pythonhosted.org/packages/26/ef/e735dd008808226c83ba56972566138665b71477ad580fa5a21f0851df48/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:37e516bca9264cbf29612539801ca3cd5d1be465f940417b002905e6ed79d38a", size = 2315078, upload-time = "2025-10-14T10:20:20.742Z" }, + { url = "https://files.pythonhosted.org/packages/90/00/806efdcf35ff2ac0f938362350cd9827b8afb116cc814b6b75cf23738c7c/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0c19cb355224037c83642429b8ce261ae108e1c5fbf5c028bac63c77b0f8646e", size = 2318737, upload-time = "2025-10-14T10:20:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/41/7e/6ac90673fe6cb36621a2283552897838c020db343fa86e513d3f563b196f/pydantic_core-2.41.4-cp311-cp311-win32.whl", hash = "sha256:09c2a60e55b357284b5f31f5ab275ba9f7f70b7525e18a132ec1f9160b4f1f03", size = 1974160, upload-time = "2025-10-14T10:20:23.817Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9d/7c5e24ee585c1f8b6356e1d11d40ab807ffde44d2db3b7dfd6d20b09720e/pydantic_core-2.41.4-cp311-cp311-win_amd64.whl", hash = "sha256:711156b6afb5cb1cb7c14a2cc2c4a8b4c717b69046f13c6b332d8a0a8f41ca3e", size = 2021883, upload-time = "2025-10-14T10:20:25.48Z" }, + { url = "https://files.pythonhosted.org/packages/33/90/5c172357460fc28b2871eb4a0fb3843b136b429c6fa827e4b588877bf115/pydantic_core-2.41.4-cp311-cp311-win_arm64.whl", hash = "sha256:6cb9cf7e761f4f8a8589a45e49ed3c0d92d1d696a45a6feaee8c904b26efc2db", size = 1968026, upload-time = "2025-10-14T10:20:27.039Z" }, + { url = "https://files.pythonhosted.org/packages/e9/81/d3b3e95929c4369d30b2a66a91db63c8ed0a98381ae55a45da2cd1cc1288/pydantic_core-2.41.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ab06d77e053d660a6faaf04894446df7b0a7e7aba70c2797465a0a1af00fc887", size = 2099043, upload-time = "2025-10-14T10:20:28.561Z" }, + { url = "https://files.pythonhosted.org/packages/58/da/46fdac49e6717e3a94fc9201403e08d9d61aa7a770fab6190b8740749047/pydantic_core-2.41.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c53ff33e603a9c1179a9364b0a24694f183717b2e0da2b5ad43c316c956901b2", size = 1910699, upload-time = "2025-10-14T10:20:30.217Z" }, + { url = "https://files.pythonhosted.org/packages/1e/63/4d948f1b9dd8e991a5a98b77dd66c74641f5f2e5225fee37994b2e07d391/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:304c54176af2c143bd181d82e77c15c41cbacea8872a2225dd37e6544dce9999", size = 1952121, upload-time = "2025-10-14T10:20:32.246Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a7/e5fc60a6f781fc634ecaa9ecc3c20171d238794cef69ae0af79ac11b89d7/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:025ba34a4cf4fb32f917d5d188ab5e702223d3ba603be4d8aca2f82bede432a4", size = 2041590, upload-time = "2025-10-14T10:20:34.332Z" }, + { url = "https://files.pythonhosted.org/packages/70/69/dce747b1d21d59e85af433428978a1893c6f8a7068fa2bb4a927fba7a5ff/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9f5f30c402ed58f90c70e12eff65547d3ab74685ffe8283c719e6bead8ef53f", size = 2219869, upload-time = "2025-10-14T10:20:35.965Z" }, + { url = "https://files.pythonhosted.org/packages/83/6a/c070e30e295403bf29c4df1cb781317b6a9bac7cd07b8d3acc94d501a63c/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd96e5d15385d301733113bcaa324c8bcf111275b7675a9c6e88bfb19fc05e3b", size = 2345169, upload-time = "2025-10-14T10:20:37.627Z" }, + { url = "https://files.pythonhosted.org/packages/f0/83/06d001f8043c336baea7fd202a9ac7ad71f87e1c55d8112c50b745c40324/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98f348cbb44fae6e9653c1055db7e29de67ea6a9ca03a5fa2c2e11a47cff0e47", size = 2070165, upload-time = "2025-10-14T10:20:39.246Z" }, + { url = "https://files.pythonhosted.org/packages/14/0a/e567c2883588dd12bcbc110232d892cf385356f7c8a9910311ac997ab715/pydantic_core-2.41.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec22626a2d14620a83ca583c6f5a4080fa3155282718b6055c2ea48d3ef35970", size = 2189067, upload-time = "2025-10-14T10:20:41.015Z" }, + { url = "https://files.pythonhosted.org/packages/f4/1d/3d9fca34273ba03c9b1c5289f7618bc4bd09c3ad2289b5420481aa051a99/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a95d4590b1f1a43bf33ca6d647b990a88f4a3824a8c4572c708f0b45a5290ed", size = 2132997, upload-time = "2025-10-14T10:20:43.106Z" }, + { url = "https://files.pythonhosted.org/packages/52/70/d702ef7a6cd41a8afc61f3554922b3ed8d19dd54c3bd4bdbfe332e610827/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:f9672ab4d398e1b602feadcffcdd3af44d5f5e6ddc15bc7d15d376d47e8e19f8", size = 2307187, upload-time = "2025-10-14T10:20:44.849Z" }, + { url = "https://files.pythonhosted.org/packages/68/4c/c06be6e27545d08b802127914156f38d10ca287a9e8489342793de8aae3c/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:84d8854db5f55fead3b579f04bda9a36461dab0730c5d570e1526483e7bb8431", size = 2305204, upload-time = "2025-10-14T10:20:46.781Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e5/35ae4919bcd9f18603419e23c5eaf32750224a89d41a8df1a3704b69f77e/pydantic_core-2.41.4-cp312-cp312-win32.whl", hash = "sha256:9be1c01adb2ecc4e464392c36d17f97e9110fbbc906bcbe1c943b5b87a74aabd", size = 1972536, upload-time = "2025-10-14T10:20:48.39Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c2/49c5bb6d2a49eb2ee3647a93e3dae7080c6409a8a7558b075027644e879c/pydantic_core-2.41.4-cp312-cp312-win_amd64.whl", hash = "sha256:d682cf1d22bab22a5be08539dca3d1593488a99998f9f412137bc323179067ff", size = 2031132, upload-time = "2025-10-14T10:20:50.421Z" }, + { url = "https://files.pythonhosted.org/packages/06/23/936343dbcba6eec93f73e95eb346810fc732f71ba27967b287b66f7b7097/pydantic_core-2.41.4-cp312-cp312-win_arm64.whl", hash = "sha256:833eebfd75a26d17470b58768c1834dfc90141b7afc6eb0429c21fc5a21dcfb8", size = 1969483, upload-time = "2025-10-14T10:20:52.35Z" }, + { url = "https://files.pythonhosted.org/packages/13/d0/c20adabd181a029a970738dfe23710b52a31f1258f591874fcdec7359845/pydantic_core-2.41.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:85e050ad9e5f6fe1004eec65c914332e52f429bc0ae12d6fa2092407a462c746", size = 2105688, upload-time = "2025-10-14T10:20:54.448Z" }, + { url = "https://files.pythonhosted.org/packages/00/b6/0ce5c03cec5ae94cca220dfecddc453c077d71363b98a4bbdb3c0b22c783/pydantic_core-2.41.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7393f1d64792763a48924ba31d1e44c2cfbc05e3b1c2c9abb4ceeadd912cced", size = 1910807, upload-time = "2025-10-14T10:20:56.115Z" }, + { url = "https://files.pythonhosted.org/packages/68/3e/800d3d02c8beb0b5c069c870cbb83799d085debf43499c897bb4b4aaff0d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94dab0940b0d1fb28bcab847adf887c66a27a40291eedf0b473be58761c9799a", size = 1956669, upload-time = "2025-10-14T10:20:57.874Z" }, + { url = "https://files.pythonhosted.org/packages/60/a4/24271cc71a17f64589be49ab8bd0751f6a0a03046c690df60989f2f95c2c/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de7c42f897e689ee6f9e93c4bec72b99ae3b32a2ade1c7e4798e690ff5246e02", size = 2051629, upload-time = "2025-10-14T10:21:00.006Z" }, + { url = "https://files.pythonhosted.org/packages/68/de/45af3ca2f175d91b96bfb62e1f2d2f1f9f3b14a734afe0bfeff079f78181/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:664b3199193262277b8b3cd1e754fb07f2c6023289c815a1e1e8fb415cb247b1", size = 2224049, upload-time = "2025-10-14T10:21:01.801Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/ae4e1ff84672bf869d0a77af24fd78387850e9497753c432875066b5d622/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d95b253b88f7d308b1c0b417c4624f44553ba4762816f94e6986819b9c273fb2", size = 2342409, upload-time = "2025-10-14T10:21:03.556Z" }, + { url = "https://files.pythonhosted.org/packages/18/62/273dd70b0026a085c7b74b000394e1ef95719ea579c76ea2f0cc8893736d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1351f5bbdbbabc689727cb91649a00cb9ee7203e0a6e54e9f5ba9e22e384b84", size = 2069635, upload-time = "2025-10-14T10:21:05.385Z" }, + { url = "https://files.pythonhosted.org/packages/30/03/cf485fff699b4cdaea469bc481719d3e49f023241b4abb656f8d422189fc/pydantic_core-2.41.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1affa4798520b148d7182da0615d648e752de4ab1a9566b7471bc803d88a062d", size = 2194284, upload-time = "2025-10-14T10:21:07.122Z" }, + { url = "https://files.pythonhosted.org/packages/f9/7e/c8e713db32405dfd97211f2fc0a15d6bf8adb7640f3d18544c1f39526619/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7b74e18052fea4aa8dea2fb7dbc23d15439695da6cbe6cfc1b694af1115df09d", size = 2137566, upload-time = "2025-10-14T10:21:08.981Z" }, + { url = "https://files.pythonhosted.org/packages/04/f7/db71fd4cdccc8b75990f79ccafbbd66757e19f6d5ee724a6252414483fb4/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:285b643d75c0e30abda9dc1077395624f314a37e3c09ca402d4015ef5979f1a2", size = 2316809, upload-time = "2025-10-14T10:21:10.805Z" }, + { url = "https://files.pythonhosted.org/packages/76/63/a54973ddb945f1bca56742b48b144d85c9fc22f819ddeb9f861c249d5464/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f52679ff4218d713b3b33f88c89ccbf3a5c2c12ba665fb80ccc4192b4608dbab", size = 2311119, upload-time = "2025-10-14T10:21:12.583Z" }, + { url = "https://files.pythonhosted.org/packages/f8/03/5d12891e93c19218af74843a27e32b94922195ded2386f7b55382f904d2f/pydantic_core-2.41.4-cp313-cp313-win32.whl", hash = "sha256:ecde6dedd6fff127c273c76821bb754d793be1024bc33314a120f83a3c69460c", size = 1981398, upload-time = "2025-10-14T10:21:14.584Z" }, + { url = "https://files.pythonhosted.org/packages/be/d8/fd0de71f39db91135b7a26996160de71c073d8635edfce8b3c3681be0d6d/pydantic_core-2.41.4-cp313-cp313-win_amd64.whl", hash = "sha256:d081a1f3800f05409ed868ebb2d74ac39dd0c1ff6c035b5162356d76030736d4", size = 2030735, upload-time = "2025-10-14T10:21:16.432Z" }, + { url = "https://files.pythonhosted.org/packages/72/86/c99921c1cf6650023c08bfab6fe2d7057a5142628ef7ccfa9921f2dda1d5/pydantic_core-2.41.4-cp313-cp313-win_arm64.whl", hash = "sha256:f8e49c9c364a7edcbe2a310f12733aad95b022495ef2a8d653f645e5d20c1564", size = 1973209, upload-time = "2025-10-14T10:21:18.213Z" }, + { url = "https://files.pythonhosted.org/packages/36/0d/b5706cacb70a8414396efdda3d72ae0542e050b591119e458e2490baf035/pydantic_core-2.41.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ed97fd56a561f5eb5706cebe94f1ad7c13b84d98312a05546f2ad036bafe87f4", size = 1877324, upload-time = "2025-10-14T10:21:20.363Z" }, + { url = "https://files.pythonhosted.org/packages/de/2d/cba1fa02cfdea72dfb3a9babb067c83b9dff0bbcb198368e000a6b756ea7/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a870c307bf1ee91fc58a9a61338ff780d01bfae45922624816878dce784095d2", size = 1884515, upload-time = "2025-10-14T10:21:22.339Z" }, + { url = "https://files.pythonhosted.org/packages/07/ea/3df927c4384ed9b503c9cc2d076cf983b4f2adb0c754578dfb1245c51e46/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d25e97bc1f5f8f7985bdc2335ef9e73843bb561eb1fa6831fdfc295c1c2061cf", size = 2042819, upload-time = "2025-10-14T10:21:26.683Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ee/df8e871f07074250270a3b1b82aad4cd0026b588acd5d7d3eb2fcb1471a3/pydantic_core-2.41.4-cp313-cp313t-win_amd64.whl", hash = "sha256:d405d14bea042f166512add3091c1af40437c2e7f86988f3915fabd27b1e9cd2", size = 1995866, upload-time = "2025-10-14T10:21:28.951Z" }, + { url = "https://files.pythonhosted.org/packages/fc/de/b20f4ab954d6d399499c33ec4fafc46d9551e11dc1858fb7f5dca0748ceb/pydantic_core-2.41.4-cp313-cp313t-win_arm64.whl", hash = "sha256:19f3684868309db5263a11bace3c45d93f6f24afa2ffe75a647583df22a2ff89", size = 1970034, upload-time = "2025-10-14T10:21:30.869Z" }, + { url = "https://files.pythonhosted.org/packages/54/28/d3325da57d413b9819365546eb9a6e8b7cbd9373d9380efd5f74326143e6/pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1", size = 2102022, upload-time = "2025-10-14T10:21:32.809Z" }, + { url = "https://files.pythonhosted.org/packages/9e/24/b58a1bc0d834bf1acc4361e61233ee217169a42efbdc15a60296e13ce438/pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac", size = 1905495, upload-time = "2025-10-14T10:21:34.812Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a4/71f759cc41b7043e8ecdaab81b985a9b6cad7cec077e0b92cff8b71ecf6b/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554", size = 1956131, upload-time = "2025-10-14T10:21:36.924Z" }, + { url = "https://files.pythonhosted.org/packages/b0/64/1e79ac7aa51f1eec7c4cda8cbe456d5d09f05fdd68b32776d72168d54275/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e", size = 2052236, upload-time = "2025-10-14T10:21:38.927Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e3/a3ffc363bd4287b80f1d43dc1c28ba64831f8dfc237d6fec8f2661138d48/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616", size = 2223573, upload-time = "2025-10-14T10:21:41.574Z" }, + { url = "https://files.pythonhosted.org/packages/28/27/78814089b4d2e684a9088ede3790763c64693c3d1408ddc0a248bc789126/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af", size = 2342467, upload-time = "2025-10-14T10:21:44.018Z" }, + { url = "https://files.pythonhosted.org/packages/92/97/4de0e2a1159cb85ad737e03306717637842c88c7fd6d97973172fb183149/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12", size = 2063754, upload-time = "2025-10-14T10:21:46.466Z" }, + { url = "https://files.pythonhosted.org/packages/0f/50/8cb90ce4b9efcf7ae78130afeb99fd1c86125ccdf9906ef64b9d42f37c25/pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d", size = 2196754, upload-time = "2025-10-14T10:21:48.486Z" }, + { url = "https://files.pythonhosted.org/packages/34/3b/ccdc77af9cd5082723574a1cc1bcae7a6acacc829d7c0a06201f7886a109/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad", size = 2137115, upload-time = "2025-10-14T10:21:50.63Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ba/e7c7a02651a8f7c52dc2cff2b64a30c313e3b57c7d93703cecea76c09b71/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a", size = 2317400, upload-time = "2025-10-14T10:21:52.959Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ba/6c533a4ee8aec6b812c643c49bb3bd88d3f01e3cebe451bb85512d37f00f/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025", size = 2312070, upload-time = "2025-10-14T10:21:55.419Z" }, + { url = "https://files.pythonhosted.org/packages/22/ae/f10524fcc0ab8d7f96cf9a74c880243576fd3e72bd8ce4f81e43d22bcab7/pydantic_core-2.41.4-cp314-cp314-win32.whl", hash = "sha256:5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e", size = 1982277, upload-time = "2025-10-14T10:21:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/b4/dc/e5aa27aea1ad4638f0c3fb41132f7eb583bd7420ee63204e2d4333a3bbf9/pydantic_core-2.41.4-cp314-cp314-win_amd64.whl", hash = "sha256:557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894", size = 2024608, upload-time = "2025-10-14T10:21:59.557Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/51d89cc2612bd147198e120a13f150afbf0bcb4615cddb049ab10b81b79e/pydantic_core-2.41.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d", size = 1967614, upload-time = "2025-10-14T10:22:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c2/472f2e31b95eff099961fa050c376ab7156a81da194f9edb9f710f68787b/pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da", size = 1876904, upload-time = "2025-10-14T10:22:04.062Z" }, + { url = "https://files.pythonhosted.org/packages/4a/07/ea8eeb91173807ecdae4f4a5f4b150a520085b35454350fc219ba79e66a3/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e", size = 1882538, upload-time = "2025-10-14T10:22:06.39Z" }, + { url = "https://files.pythonhosted.org/packages/1e/29/b53a9ca6cd366bfc928823679c6a76c7a4c69f8201c0ba7903ad18ebae2f/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa", size = 2041183, upload-time = "2025-10-14T10:22:08.812Z" }, + { url = "https://files.pythonhosted.org/packages/c7/3d/f8c1a371ceebcaf94d6dd2d77c6cf4b1c078e13a5837aee83f760b4f7cfd/pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl", hash = "sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d", size = 1993542, upload-time = "2025-10-14T10:22:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ac/9fc61b4f9d079482a290afe8d206b8f490e9fd32d4fc03ed4fc698214e01/pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0", size = 1973897, upload-time = "2025-10-14T10:22:13.444Z" }, + { url = "https://files.pythonhosted.org/packages/b0/12/5ba58daa7f453454464f92b3ca7b9d7c657d8641c48e370c3ebc9a82dd78/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:a1b2cfec3879afb742a7b0bcfa53e4f22ba96571c9e54d6a3afe1052d17d843b", size = 2122139, upload-time = "2025-10-14T10:22:47.288Z" }, + { url = "https://files.pythonhosted.org/packages/21/fb/6860126a77725c3108baecd10fd3d75fec25191d6381b6eb2ac660228eac/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:d175600d975b7c244af6eb9c9041f10059f20b8bbffec9e33fdd5ee3f67cdc42", size = 1936674, upload-time = "2025-10-14T10:22:49.555Z" }, + { url = "https://files.pythonhosted.org/packages/de/be/57dcaa3ed595d81f8757e2b44a38240ac5d37628bce25fb20d02c7018776/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f184d657fa4947ae5ec9c47bd7e917730fa1cbb78195037e32dcbab50aca5ee", size = 1956398, upload-time = "2025-10-14T10:22:52.19Z" }, + { url = "https://files.pythonhosted.org/packages/2f/1d/679a344fadb9695f1a6a294d739fbd21d71fa023286daeea8c0ed49e7c2b/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed810568aeffed3edc78910af32af911c835cc39ebbfacd1f0ab5dd53028e5c", size = 2138674, upload-time = "2025-10-14T10:22:54.499Z" }, + { url = "https://files.pythonhosted.org/packages/c4/48/ae937e5a831b7c0dc646b2ef788c27cd003894882415300ed21927c21efa/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:4f5d640aeebb438517150fdeec097739614421900e4a08db4a3ef38898798537", size = 2112087, upload-time = "2025-10-14T10:22:56.818Z" }, + { url = "https://files.pythonhosted.org/packages/5e/db/6db8073e3d32dae017da7e0d16a9ecb897d0a4d92e00634916e486097961/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:4a9ab037b71927babc6d9e7fc01aea9e66dc2a4a34dff06ef0724a4049629f94", size = 1920387, upload-time = "2025-10-14T10:22:59.342Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c1/dd3542d072fcc336030d66834872f0328727e3b8de289c662faa04aa270e/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4dab9484ec605c3016df9ad4fd4f9a390bc5d816a3b10c6550f8424bb80b18c", size = 1951495, upload-time = "2025-10-14T10:23:02.089Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c6/db8d13a1f8ab3f1eb08c88bd00fd62d44311e3456d1e85c0e59e0a0376e7/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8a5028425820731d8c6c098ab642d7b8b999758e24acae03ed38a66eca8335", size = 2139008, upload-time = "2025-10-14T10:23:04.539Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7d/138e902ed6399b866f7cfe4435d22445e16fff888a1c00560d9dc79a780f/pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:491535d45cd7ad7e4a2af4a5169b0d07bebf1adfd164b0368da8aa41e19907a5", size = 2104721, upload-time = "2025-10-14T10:23:26.906Z" }, + { url = "https://files.pythonhosted.org/packages/47/13/0525623cf94627f7b53b4c2034c81edc8491cbfc7c28d5447fa318791479/pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:54d86c0cada6aba4ec4c047d0e348cbad7063b87ae0f005d9f8c9ad04d4a92a2", size = 1931608, upload-time = "2025-10-14T10:23:29.306Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f9/744bc98137d6ef0a233f808bfc9b18cf94624bf30836a18d3b05d08bf418/pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca1124aced216b2500dc2609eade086d718e8249cb9696660ab447d50a758bd", size = 2132986, upload-time = "2025-10-14T10:23:32.057Z" }, + { url = "https://files.pythonhosted.org/packages/17/c8/629e88920171173f6049386cc71f893dff03209a9ef32b4d2f7e7c264bcf/pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c9024169becccf0cb470ada03ee578d7348c119a0d42af3dcf9eda96e3a247c", size = 2187516, upload-time = "2025-10-14T10:23:34.871Z" }, + { url = "https://files.pythonhosted.org/packages/2e/0f/4f2734688d98488782218ca61bcc118329bf5de05bb7fe3adc7dd79b0b86/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:26895a4268ae5a2849269f4991cdc97236e4b9c010e51137becf25182daac405", size = 2146146, upload-time = "2025-10-14T10:23:37.342Z" }, + { url = "https://files.pythonhosted.org/packages/ed/f2/ab385dbd94a052c62224b99cf99002eee99dbec40e10006c78575aead256/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:ca4df25762cf71308c446e33c9b1fdca2923a3f13de616e2a949f38bf21ff5a8", size = 2311296, upload-time = "2025-10-14T10:23:40.145Z" }, + { url = "https://files.pythonhosted.org/packages/fc/8e/e4f12afe1beeb9823bba5375f8f258df0cc61b056b0195fb1cf9f62a1a58/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:5a28fcedd762349519276c36634e71853b4541079cab4acaaac60c4421827308", size = 2315386, upload-time = "2025-10-14T10:23:42.624Z" }, + { url = "https://files.pythonhosted.org/packages/48/f7/925f65d930802e3ea2eb4d5afa4cb8730c8dc0d2cb89a59dc4ed2fcb2d74/pydantic_core-2.41.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c173ddcd86afd2535e2b695217e82191580663a1d1928239f877f5a1649ef39f", size = 2147775, upload-time = "2025-10-14T10:23:45.406Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "starlette" +version = "0.48.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/a5/d6f429d43394057b67a6b5bbe6eae2f77a6bf7459d961fdb224bf206eee6/starlette-0.48.0.tar.gz", hash = "sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46", size = 2652949, upload-time = "2025-09-13T08:41:05.699Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/72/2db2f49247d0a18b4f1bb9a5a39a0162869acf235f3a96418363947b3d46/starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659", size = 73736, upload-time = "2025-09-13T08:41:03.869Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605, upload-time = "2025-10-18T13:46:44.63Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" }, + { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" }, + { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" }, + { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" }, + { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" }, + { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, + { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, + { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, + { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, + { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, + { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, + { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, + { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, + { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +]