Initial commit with project setup
This commit is contained in:
7
.dockerignore
Normal file
7
.dockerignore
Normal file
@@ -0,0 +1,7 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
venv/
|
||||
.env
|
||||
.git
|
||||
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
# Python virtual environment
|
||||
venv/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# Distribution / packaging
|
||||
dist/
|
||||
build/
|
||||
*.egg-info/
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
|
||||
# IDE specific files
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS specific files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
26
Dockerfile
Normal file
26
Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
||||
# Use an official Python runtime as a parent image
|
||||
FROM python:3.11-slim
|
||||
|
||||
# Set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
|
||||
# Set work directory
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies
|
||||
COPY requirements.txt /app/
|
||||
RUN pip install --upgrade pip && pip install -r requirements.txt
|
||||
|
||||
# Copy project files
|
||||
COPY . /app/
|
||||
|
||||
# Expose the port Flask runs on
|
||||
EXPOSE 5000
|
||||
|
||||
# Set environment variable for Flask
|
||||
ENV FLASK_APP=app.py
|
||||
ENV FLASK_RUN_HOST=0.0.0.0
|
||||
|
||||
# Run the application
|
||||
CMD ["flask", "run"]
|
||||
38
README.md
Normal file
38
README.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Flask SQL Website
|
||||
|
||||
A modern web application built with Flask and SQLAlchemy.
|
||||
|
||||
## Setup
|
||||
|
||||
1. Create a virtual environment:
|
||||
```bash
|
||||
python -m venv venv
|
||||
```
|
||||
|
||||
2. Activate the virtual environment:
|
||||
- Windows:
|
||||
```bash
|
||||
.\venv\Scripts\activate
|
||||
```
|
||||
- Unix/MacOS:
|
||||
```bash
|
||||
source venv/bin/activate
|
||||
```
|
||||
|
||||
3. Install dependencies:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
4. Run the application:
|
||||
```bash
|
||||
python app.py
|
||||
```
|
||||
|
||||
The application will be available at http://localhost:5000
|
||||
|
||||
## Features
|
||||
- SQL database integration
|
||||
- Modern web interface
|
||||
- RESTful API endpoints
|
||||
- Form handling and validation
|
||||
BIN
instance/site.db
Normal file
BIN
instance/site.db
Normal file
Binary file not shown.
6
requirements.txt
Normal file
6
requirements.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
Flask==2.3.3
|
||||
Flask-SQLAlchemy==3.0.5
|
||||
SQLAlchemy==2.0.20
|
||||
python-dotenv==1.0.0
|
||||
Flask-WTF==1.1.1
|
||||
Werkzeug==2.3.7
|
||||
11
static/images/placeholder-plant.svg
Normal file
11
static/images/placeholder-plant.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="200" height="200" fill="#f5f7f2"/>
|
||||
<g transform="translate(50, 50)">
|
||||
<!-- Pot -->
|
||||
<path d="M50,100 L90,100 L80,140 L20,140 L10,100 Z" fill="#6b8f71"/>
|
||||
<!-- Plant -->
|
||||
<path d="M50,20 C30,20 20,40 20,60 C20,80 40,90 50,90 C60,90 80,80 80,60 C80,40 70,20 50,20 Z" fill="#b7c7a3"/>
|
||||
<path d="M50,90 L50,100" stroke="#6b8f71" stroke-width="4"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 543 B |
171
static/styles/planty.css
Normal file
171
static/styles/planty.css
Normal file
@@ -0,0 +1,171 @@
|
||||
/* Planty theme variables (converted from LESS) */
|
||||
body {
|
||||
font-family: 'Quicksand', 'Segoe UI', Arial, sans-serif;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #e6ebe0, #b7c7a3 60%, #6b8f71 100%);
|
||||
}
|
||||
|
||||
nav {
|
||||
background: rgba(245, 247, 242, 0.95);
|
||||
box-shadow: 0 2px 8px rgba(62, 86, 55, 0.10);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.site-title {
|
||||
color: #3e5637;
|
||||
font-weight: bold;
|
||||
font-size: 1.25rem;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.btn-main {
|
||||
background: #6b8f71;
|
||||
color: #fff;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 2px 6px rgba(62, 86, 55, 0.10);
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.btn-main:hover {
|
||||
background: #4e6b50;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #e6ebe0;
|
||||
color: #3e5637;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
.btn-secondary:hover {
|
||||
background: #b7c7a3;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #f5f7f2;
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 4px 16px rgba(62, 86, 55, 0.08);
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.admin-header {
|
||||
background: linear-gradient(90deg, #b7c7a3, #6b8f71);
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 4px 16px rgba(62, 86, 55, 0.10);
|
||||
padding: 1.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.admin-header-title {
|
||||
color: #3e5637;
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.admin-header-desc {
|
||||
color: #6b8f71;
|
||||
}
|
||||
|
||||
.badge-env, .badge-climate, .badge-product, .badge-plant {
|
||||
background: none;
|
||||
color: inherit;
|
||||
box-shadow: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-edit {
|
||||
background: #6b8f71;
|
||||
color: #fff;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.4em 1.2em;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 2px 6px rgba(62, 86, 55, 0.08);
|
||||
border: none;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.btn-edit:hover {
|
||||
background: #4e6b50;
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
background: #c94c4c;
|
||||
color: #fff;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.4em 1.2em;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 2px 6px rgba(201, 76, 76, 0.10);
|
||||
border: none;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.btn-delete:hover {
|
||||
background: #a63a3a;
|
||||
}
|
||||
|
||||
/* Admin navigation override: fully transparent */
|
||||
.admin-panel-nav {
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
backdrop-filter: none !important;
|
||||
}
|
||||
|
||||
/* Custom colors for each admin nav item */
|
||||
.nav-plants {
|
||||
color: #4e6b50;
|
||||
}
|
||||
.nav-plants svg { color: #4e6b50; }
|
||||
.nav-plants:hover { background: #e6ebe0 !important; }
|
||||
|
||||
.nav-environments {
|
||||
color: #3b5a6b;
|
||||
}
|
||||
.nav-environments svg { color: #3b5a6b; }
|
||||
.nav-environments:hover { background: #e0f0eb !important; }
|
||||
|
||||
.nav-climates {
|
||||
color: #8a6b3b;
|
||||
}
|
||||
.nav-climates svg { color: #8a6b3b; }
|
||||
.nav-climates:hover { background: #f7f2e6 !important; }
|
||||
|
||||
.nav-lights {
|
||||
color: #b89c1d;
|
||||
}
|
||||
.nav-lights svg { color: #b89c1d; }
|
||||
.nav-lights:hover { background: #fffbe6 !important; }
|
||||
|
||||
.nav-toxicities {
|
||||
color: #b87c1d;
|
||||
}
|
||||
.nav-toxicities svg { color: #b87c1d; }
|
||||
.nav-toxicities:hover { background: #fff4e6 !important; }
|
||||
|
||||
.nav-sizes {
|
||||
color: #4eb6a6;
|
||||
}
|
||||
.nav-sizes svg { color: #4eb6a6; }
|
||||
.nav-sizes:hover { background: #e6f7f5 !important; }
|
||||
|
||||
.nav-difficulties {
|
||||
color: #8a4eb6;
|
||||
}
|
||||
.nav-difficulties svg { color: #8a4eb6; }
|
||||
.nav-difficulties:hover { background: #f3e6ff !important; }
|
||||
|
||||
.nav-growth {
|
||||
color: #7a8f3b;
|
||||
}
|
||||
.nav-growth svg { color: #7a8f3b; }
|
||||
.nav-growth:hover { background: #f7fae6 !important; }
|
||||
|
||||
.nav-products {
|
||||
color: #8b5c2a;
|
||||
}
|
||||
.nav-products svg { color: #8b5c2a; }
|
||||
.nav-products:hover { background: #f7efe6 !important; }
|
||||
|
||||
.masonry {
|
||||
column-gap: 2rem;
|
||||
}
|
||||
.masonry > * {
|
||||
break-inside: avoid;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
BIN
static/uploads/Screenshot_20250520_130603_Photos.jpg
Normal file
BIN
static/uploads/Screenshot_20250520_130603_Photos.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 119 KiB |
104
templates/admin_base.html
Normal file
104
templates/admin_base.html
Normal file
@@ -0,0 +1,104 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-6xl mx-auto">
|
||||
<!-- Admin Header -->
|
||||
<div class="flex items-center bg-gradient-to-r from-[#b7c7a3] to-[#6b8f71] rounded-xl p-7 mb-6 shadow-lg">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-14 h-14 text-[#4e6b50] mr-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 2C7 2 2 7 2 12c0 5 5 10 10 10s10-5 10-10c0-5-5-10-10-10zm0 0c0 4 4 8 8 8" />
|
||||
</svg>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-[#3e5637] mb-1 tracking-tight">Admin Panel</h1>
|
||||
<p class="text-[#4e6b50]">Welcome to Verpot Je Lot's plant care management dashboard.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Navigation (now outside background wrapper) -->
|
||||
<nav class="admin-panel-nav grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4 mb-8">
|
||||
<a href="{{ url_for('manage_plants') }}" class="nav-plants flex items-center justify-between p-4 rounded-lg transition-colors hover:bg-[#e6ebe0]">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v18m9-9H3" />
|
||||
</svg>
|
||||
<span class="font-medium">Plants</span>
|
||||
</div>
|
||||
<span class="badge-plant text-xs font-semibold px-2 py-0.5 rounded">{{ plant_count }}</span>
|
||||
</a>
|
||||
<a href="{{ url_for('manage_environments') }}" class="nav-environments flex items-center justify-between p-4 rounded-lg transition-colors hover:bg-[#e0f0eb]">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v18m9-9H3" />
|
||||
</svg>
|
||||
<span class="font-medium">Environments</span>
|
||||
</div>
|
||||
<span class="badge-env text-xs font-semibold px-2 py-0.5 rounded">{{ environment_count }}</span>
|
||||
</a>
|
||||
<a href="{{ url_for('manage_climates') }}" class="nav-climates flex items-center justify-between p-4 rounded-lg transition-colors hover:bg-[#f7f2e6]">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v18m9-9H3" />
|
||||
</svg>
|
||||
<span class="font-medium">Climates</span>
|
||||
</div>
|
||||
<span class="badge-climate text-xs font-semibold px-2 py-0.5 rounded">{{ climate_count }}</span>
|
||||
</a>
|
||||
<a href="{{ url_for('manage_lights') }}" class="nav-lights flex items-center justify-between p-4 rounded-lg transition-colors hover:bg-[#fffbe6]">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v18m9-9H3" />
|
||||
</svg>
|
||||
<span class="font-medium">Light Requirements</span>
|
||||
</div>
|
||||
<span class="badge-light text-xs font-semibold px-2 py-0.5 rounded">{{ light_count }}</span>
|
||||
</a>
|
||||
<a href="{{ url_for('manage_toxicities') }}" class="nav-toxicities flex items-center justify-between p-4 rounded-lg transition-colors hover:bg-[#fff4e6]">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v18m9-9H3" />
|
||||
</svg>
|
||||
<span class="font-medium">Toxicity Levels</span>
|
||||
</div>
|
||||
<span class="badge-toxicity text-xs font-semibold px-2 py-0.5 rounded">{{ toxicity_count }}</span>
|
||||
</a>
|
||||
<a href="{{ url_for('manage_sizes') }}" class="nav-sizes flex items-center justify-between p-4 rounded-lg transition-colors hover:bg-[#e6f7f5]">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v18m9-9H3" />
|
||||
</svg>
|
||||
<span class="font-medium">Size Categories</span>
|
||||
</div>
|
||||
<span class="badge-size text-xs font-semibold px-2 py-0.5 rounded">{{ size_count }}</span>
|
||||
</a>
|
||||
<a href="{{ url_for('manage_care_difficulties') }}" class="nav-difficulties flex items-center justify-between p-4 rounded-lg transition-colors hover:bg-[#f3e6ff]">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v18m9-9H3" />
|
||||
</svg>
|
||||
<span class="font-medium">Care Difficulties</span>
|
||||
</div>
|
||||
<span class="badge-difficulty text-xs font-semibold px-2 py-0.5 rounded">{{ difficulty_count }}</span>
|
||||
</a>
|
||||
<a href="{{ url_for('manage_growth_rates') }}" class="nav-growth flex items-center justify-between p-4 rounded-lg transition-colors hover:bg-[#f7fae6]">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v18m9-9H3" />
|
||||
</svg>
|
||||
<span class="font-medium">Growth Rates</span>
|
||||
</div>
|
||||
<span class="badge-rate text-xs font-semibold px-2 py-0.5 rounded">{{ rate_count }}</span>
|
||||
</a>
|
||||
<a href="{{ url_for('manage_products') }}" class="nav-products flex items-center justify-between p-4 rounded-lg transition-colors hover:bg-[#f7efe6]">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v18m9-9H3" />
|
||||
</svg>
|
||||
<span class="font-medium">Products</span>
|
||||
</div>
|
||||
<span class="badge-product text-xs font-semibold px-2 py-0.5 rounded">{{ product_count }}</span>
|
||||
</a>
|
||||
</nav>
|
||||
<!-- Only admin content is inside the background wrapper now -->
|
||||
<div class="bg-[#f5f7f2] shadow-lg rounded-xl p-8 mb-6">
|
||||
{% block admin_content %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
161
templates/base.html
Normal file
161
templates/base.html
Normal file
@@ -0,0 +1,161 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}{% endblock %} - Verpot Je Lot</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@500;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='styles/planty.css') }}">
|
||||
</head>
|
||||
<body class="min-h-screen bg-gradient-to-br from-[#e6ebe0] via-[#b7c7a3] to-[#6b8f71] bg-fixed">
|
||||
<nav class="bg-[#f5f7f2]/95 shadow-lg backdrop-blur-md" id="main-navbar">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between">
|
||||
<div class="flex space-x-7">
|
||||
<div>
|
||||
<a href="{{ url_for('home') }}" class="flex items-center py-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-7 h-7 text-[#6b8f71] mr-2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 2C7 2 2 7 2 12c0 5 5 10 10 10s10-5 10-10c0-5-5-10-10-10zm0 0c0 4 4 8 8 8" />
|
||||
</svg>
|
||||
<span class="font-bold text-[#3e5637] text-xl tracking-tight">Verpot Je Lot</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
<!-- Admin links moved to footer -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="container mx-auto px-4 py-8">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="mb-4 p-4 rounded-lg {% if category == 'success' %}bg-green-100 text-green-700{% else %}bg-red-100 text-red-700{% endif %} shadow">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
{% if request.endpoint == 'home' %}
|
||||
<footer class="bg-[#f5f7f2]/95 text-[#3e5637] text-sm py-2 px-4 flex justify-between items-center shadow-t z-40 mt-8">
|
||||
<span>Made by Kobe Amerijckx and Roos Amerijckx</span>
|
||||
{% if is_logged_in %}
|
||||
<a href="{{ url_for('manage_environments') }}" class="py-1 px-3 bg-[#6b8f71] hover:bg-[#4e6b50] text-white rounded-lg transition duration-200 shadow">Admin Panel</a>
|
||||
<a href="{{ url_for('logout') }}" class="py-1 px-3 bg-[#e6ebe0] hover:bg-[#b7c7a3] text-[#3e5637] rounded-lg transition duration-200 ml-2">Logout</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('login') }}" class="py-1 px-3 bg-[#6b8f71] hover:bg-[#4e6b50] text-white rounded-lg transition duration-200 shadow">Admin Login</a>
|
||||
{% endif %}
|
||||
</footer>
|
||||
{% endif %}
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div id="delete-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
|
||||
<div class="bg-white rounded-lg p-6 max-w-md w-full mx-4 transform transition-all">
|
||||
<div class="text-center mb-6">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 text-red-500 mx-auto mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<h3 class="text-xl font-bold text-gray-900 mb-2">Confirm Deletion</h3>
|
||||
<p class="text-gray-600" id="delete-modal-message">Are you sure you want to delete this item?</p>
|
||||
</div>
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button onclick="closeDeleteModal()" class="px-4 py-2 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300 transition duration-200">
|
||||
Cancel
|
||||
</button>
|
||||
<form id="delete-form" method="POST" class="inline">
|
||||
<button type="submit" class="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition duration-200">
|
||||
Delete
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function showDeleteModal(formId, message) {
|
||||
const modal = document.getElementById('delete-modal');
|
||||
const form = document.getElementById(formId);
|
||||
const modalForm = document.getElementById('delete-form');
|
||||
const messageEl = document.getElementById('delete-modal-message');
|
||||
|
||||
// Set the message
|
||||
messageEl.textContent = message || 'Are you sure you want to delete this item?';
|
||||
|
||||
// Set the form action
|
||||
modalForm.action = form.action;
|
||||
|
||||
// Show the modal
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
|
||||
// Add animation
|
||||
const modalContent = modal.querySelector('.bg-white');
|
||||
modalContent.classList.add('scale-95', 'opacity-0');
|
||||
requestAnimationFrame(() => {
|
||||
modalContent.classList.remove('scale-95', 'opacity-0');
|
||||
});
|
||||
}
|
||||
|
||||
function closeDeleteModal() {
|
||||
const modal = document.getElementById('delete-modal');
|
||||
const modalContent = modal.querySelector('.bg-white');
|
||||
|
||||
// Add animation
|
||||
modalContent.classList.add('scale-95', 'opacity-0');
|
||||
|
||||
// Hide after animation
|
||||
setTimeout(() => {
|
||||
modal.classList.remove('flex');
|
||||
modal.classList.add('hidden');
|
||||
modalContent.classList.remove('scale-95', 'opacity-0');
|
||||
}, 200);
|
||||
}
|
||||
|
||||
// Close modal when clicking outside
|
||||
document.getElementById('delete-modal').addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
closeDeleteModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Animate plant cards on scroll
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const cards = document.querySelectorAll('.plant-card');
|
||||
if ('IntersectionObserver' in window) {
|
||||
const observer = new IntersectionObserver((entries, obs) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.remove('opacity-0', 'translate-y-8');
|
||||
entry.target.classList.add('opacity-100', 'translate-y-0');
|
||||
obs.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}, { threshold: 0.15 });
|
||||
cards.forEach(card => observer.observe(card));
|
||||
} else {
|
||||
// Fallback: show all
|
||||
cards.forEach(card => card.classList.remove('opacity-0', 'translate-y-8'));
|
||||
}
|
||||
|
||||
// Localize all .local-date elements
|
||||
document.querySelectorAll('.local-date').forEach(function(el) {
|
||||
const date = new Date(el.dataset.date);
|
||||
el.textContent = date.toLocaleDateString(undefined, { day: '2-digit', month: '2-digit', year: 'numeric' }) +
|
||||
' ' + date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
.plant-card {
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
||||
106
templates/create_plant.html
Normal file
106
templates/create_plant.html
Normal file
@@ -0,0 +1,106 @@
|
||||
{% extends "admin_base.html" %}
|
||||
|
||||
{% block title %}New Plant{% endblock %}
|
||||
|
||||
{% block admin_content %}
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<h1 class="text-3xl font-bold mb-6 text-center">Add New Plant</h1>
|
||||
<div class="bg-white shadow-lg rounded-xl p-8">
|
||||
<form method="POST" enctype="multipart/form-data" class="space-y-6">
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-semibold text-gray-700 mb-1">Name</label>
|
||||
<input type="text" name="name" id="name" required
|
||||
class="mt-1 block w-full rounded-lg border border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 px-3 py-2">
|
||||
</div>
|
||||
<div>
|
||||
<label for="picture" class="block text-sm font-semibold text-gray-700 mb-1">Picture</label>
|
||||
<input type="file" name="picture" id="picture"
|
||||
class="mt-1 block w-full text-sm text-gray-700 border border-gray-300 rounded-lg px-3 py-2 bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="climate_id" class="block text-sm font-medium text-gray-700">Climate</label>
|
||||
<select name="climate_id" id="climate_id" required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<option value="">Select a climate</option>
|
||||
{% for climate in climates %}
|
||||
<option value="{{ climate.id }}">{{ climate.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="environment_id" class="block text-sm font-medium text-gray-700">Environment</label>
|
||||
<select name="environment_id" id="environment_id" required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<option value="">Select an environment</option>
|
||||
{% for environment in environments %}
|
||||
<option value="{{ environment.id }}">{{ environment.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="light_id" class="block text-sm font-medium text-gray-700">Light</label>
|
||||
<select name="light_id" id="light_id" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<option value="">Select light requirement</option>
|
||||
{% for light in lights %}
|
||||
<option value="{{ light.id }}">{{ light.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="toxicity_id" class="block text-sm font-medium text-gray-700">Toxicity</label>
|
||||
<select name="toxicity_id" id="toxicity_id" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<option value="">Select toxicity level</option>
|
||||
{% for toxicity in toxicities %}
|
||||
<option value="{{ toxicity.id }}">{{ toxicity.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="size_id" class="block text-sm font-medium text-gray-700">Size</label>
|
||||
<select name="size_id" id="size_id" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<option value="">Select size category</option>
|
||||
{% for size in sizes %}
|
||||
<option value="{{ size.id }}">{{ size.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="care_difficulty_id" class="block text-sm font-medium text-gray-700">Care Difficulty</label>
|
||||
<select name="care_difficulty_id" id="care_difficulty_id" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<option value="">Select difficulty level</option>
|
||||
{% for difficulty in difficulties %}
|
||||
<option value="{{ difficulty.id }}">{{ difficulty.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="growth_rate_id" class="block text-sm font-medium text-gray-700">Growth Rate</label>
|
||||
<select name="growth_rate_id" id="growth_rate_id" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<option value="">Select growth rate</option>
|
||||
{% for rate in growth_rates %}
|
||||
<option value="{{ rate.id }}">{{ rate.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label for="product_ids" class="block text-sm font-medium text-gray-700">Products</label>
|
||||
<select name="product_ids" id="product_ids" multiple required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
{% for product in products %}
|
||||
<option value="{{ product.id }}">{{ product.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<p class="mt-1 text-sm text-gray-500">Hold Ctrl (or Cmd on Mac) to select multiple products</p>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label for="description" class="block text-sm font-medium text-gray-700">Description</label>
|
||||
<textarea name="description" id="description" rows="6" required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"></textarea>
|
||||
</div>
|
||||
<button type="submit" class="w-full btn-main text-lg font-semibold">Add Plant</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
32
templates/edit_care_difficulty.html
Normal file
32
templates/edit_care_difficulty.html
Normal file
@@ -0,0 +1,32 @@
|
||||
{% extends "admin_base.html" %}
|
||||
|
||||
{% block title %}Edit Care Difficulty{% endblock %}
|
||||
|
||||
{% block admin_content %}
|
||||
<div class="max-w-xl mx-auto bg-gray-50 p-6 rounded-lg">
|
||||
<h2 class="text-2xl font-bold mb-4">Edit Care Difficulty</h2>
|
||||
<form method="POST" enctype="multipart/form-data" class="space-y-4">
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700">Name</label>
|
||||
<input type="text" name="name" id="name" value="{{ difficulty.name }}" required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label for="description" class="block text-sm font-medium text-gray-700">Description</label>
|
||||
<textarea name="description" id="description" rows="3"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">{{ difficulty.description }}</textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label for="icon" class="block text-sm font-medium text-gray-700">Icon (SVG)</label>
|
||||
<input type="file" name="icon" id="icon" accept=".svg" class="mt-1 block w-full text-sm text-gray-700">
|
||||
{% if difficulty.icon %}
|
||||
<div class="mt-2">
|
||||
<span class="text-xs text-gray-500">Current icon:</span>
|
||||
<img src="{{ url_for('static', filename='icons/' ~ difficulty.icon) }}" alt="Icon" class="w-8 h-8 inline-block align-middle">
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<button type="submit" class="btn-main w-full">Save Changes</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
33
templates/edit_climate.html
Normal file
33
templates/edit_climate.html
Normal file
@@ -0,0 +1,33 @@
|
||||
{% extends "admin_base.html" %}
|
||||
|
||||
{% block title %}Edit Climate{% endblock %}
|
||||
|
||||
{% block admin_content %}
|
||||
<div class="max-w-xl mx-auto bg-white p-8 rounded-lg shadow-md">
|
||||
<h1 class="text-2xl font-bold mb-6">Edit Climate</h1>
|
||||
<form method="POST" class="space-y-4">
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700">Name</label>
|
||||
<input type="text" name="name" id="name" value="{{ climate.name }}" required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label for="description" class="block text-sm font-medium text-gray-700">Description</label>
|
||||
<textarea name="description" id="description" rows="3"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">{{ climate.description }}</textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label for="icon" class="block text-sm font-medium text-gray-700">Icon (SVG)</label>
|
||||
<input type="file" name="icon" id="icon" accept=".svg" class="mt-1 block w-full text-sm text-gray-700">
|
||||
{% if climate.icon %}
|
||||
<img src="{{ url_for('static', filename='icons/' ~ climate.icon) }}" alt="Icon" class="w-8 h-8 mt-2">
|
||||
{% endif %}
|
||||
</div>
|
||||
<button type="submit"
|
||||
class="w-full bg-blue-500 text-white py-2 px-4 rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
|
||||
Save Changes
|
||||
</button>
|
||||
<a href="{{ url_for('manage_climates') }}" class="block text-center mt-4 text-gray-500 hover:text-gray-700">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
33
templates/edit_environment.html
Normal file
33
templates/edit_environment.html
Normal file
@@ -0,0 +1,33 @@
|
||||
{% extends "admin_base.html" %}
|
||||
|
||||
{% block title %}Edit Environment{% endblock %}
|
||||
|
||||
{% block admin_content %}
|
||||
<div class="max-w-xl mx-auto bg-white p-8 rounded-lg shadow-md">
|
||||
<h1 class="text-2xl font-bold mb-6">Edit Environment</h1>
|
||||
<form method="POST" class="space-y-4">
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700">Name</label>
|
||||
<input type="text" name="name" id="name" value="{{ environment.name }}" required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label for="description" class="block text-sm font-medium text-gray-700">Description</label>
|
||||
<textarea name="description" id="description" rows="3"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">{{ environment.description }}</textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label for="icon" class="block text-sm font-medium text-gray-700">Icon (SVG)</label>
|
||||
<input type="file" name="icon" id="icon" accept=".svg" class="mt-1 block w-full text-sm text-gray-700">
|
||||
{% if environment.icon %}
|
||||
<img src="{{ url_for('static', filename='icons/' ~ environment.icon) }}" alt="Icon" class="w-8 h-8 mt-2">
|
||||
{% endif %}
|
||||
</div>
|
||||
<button type="submit"
|
||||
class="w-full bg-blue-500 text-white py-2 px-4 rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
|
||||
Save Changes
|
||||
</button>
|
||||
<a href="{{ url_for('manage_environments') }}" class="block text-center mt-4 text-gray-500 hover:text-gray-700">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
32
templates/edit_growth_rate.html
Normal file
32
templates/edit_growth_rate.html
Normal file
@@ -0,0 +1,32 @@
|
||||
{% extends "admin_base.html" %}
|
||||
|
||||
{% block title %}Edit Growth Rate{% endblock %}
|
||||
|
||||
{% block admin_content %}
|
||||
<div class="max-w-xl mx-auto bg-gray-50 p-6 rounded-lg">
|
||||
<h2 class="text-2xl font-bold mb-4">Edit Growth Rate</h2>
|
||||
<form method="POST" enctype="multipart/form-data" class="space-y-4">
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700">Name</label>
|
||||
<input type="text" name="name" id="name" value="{{ rate.name }}" required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label for="description" class="block text-sm font-medium text-gray-700">Description</label>
|
||||
<textarea name="description" id="description" rows="3"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">{{ rate.description }}</textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label for="icon" class="block text-sm font-medium text-gray-700">Icon (SVG)</label>
|
||||
<input type="file" name="icon" id="icon" accept=".svg" class="mt-1 block w-full text-sm text-gray-700">
|
||||
{% if rate.icon %}
|
||||
<div class="mt-2">
|
||||
<span class="text-xs text-gray-500">Current icon:</span>
|
||||
<img src="{{ url_for('static', filename='icons/' ~ rate.icon) }}" alt="Icon" class="w-8 h-8 inline-block align-middle">
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<button type="submit" class="btn-main w-full">Save Changes</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
32
templates/edit_light.html
Normal file
32
templates/edit_light.html
Normal file
@@ -0,0 +1,32 @@
|
||||
{% extends "admin_base.html" %}
|
||||
|
||||
{% block title %}Edit Light Requirement{% endblock %}
|
||||
|
||||
{% block admin_content %}
|
||||
<div class="max-w-xl mx-auto bg-gray-50 p-6 rounded-lg">
|
||||
<h2 class="text-2xl font-bold mb-4">Edit Light Requirement</h2>
|
||||
<form method="POST" enctype="multipart/form-data" class="space-y-4">
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700">Name</label>
|
||||
<input type="text" name="name" id="name" value="{{ light.name }}" required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label for="description" class="block text-sm font-medium text-gray-700">Description</label>
|
||||
<textarea name="description" id="description" rows="3"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">{{ light.description }}</textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label for="icon" class="block text-sm font-medium text-gray-700">Icon (SVG)</label>
|
||||
<input type="file" name="icon" id="icon" accept=".svg" class="mt-1 block w-full text-sm text-gray-700">
|
||||
{% if light.icon %}
|
||||
<div class="mt-2">
|
||||
<span class="text-xs text-gray-500">Current icon:</span>
|
||||
<img src="{{ url_for('static', filename='icons/' ~ light.icon) }}" alt="Icon" class="w-8 h-8 inline-block align-middle">
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<button type="submit" class="btn-main w-full">Save Changes</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
128
templates/edit_plant.html
Normal file
128
templates/edit_plant.html
Normal file
@@ -0,0 +1,128 @@
|
||||
{% extends "admin_base.html" %}
|
||||
|
||||
{% block title %}Edit Plant{% endblock %}
|
||||
|
||||
{% block admin_content %}
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<h1 class="text-3xl font-bold mb-6 text-center">Edit Plant</h1>
|
||||
<div class="bg-white shadow-lg rounded-xl p-8">
|
||||
<form method="POST" enctype="multipart/form-data" class="space-y-6">
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-semibold text-gray-700 mb-1">Name</label>
|
||||
<input type="text" name="name" id="name" value="{{ plant.name }}" required
|
||||
class="mt-1 block w-full rounded-lg border border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 px-3 py-2">
|
||||
</div>
|
||||
<div>
|
||||
<label for="picture" class="block text-sm font-semibold text-gray-700 mb-1">Picture</label>
|
||||
<input type="file" name="picture" id="picture"
|
||||
class="mt-1 block w-full text-sm text-gray-700 border border-gray-300 rounded-lg px-3 py-2 bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
{% if plant.picture %}
|
||||
<img src="{{ url_for('static', filename='uploads/' ~ plant.picture) }}" alt="{{ plant.name }}" class="w-24 h-24 object-cover rounded mt-2">
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="climate_id" class="block text-sm font-medium text-gray-700">Climate</label>
|
||||
<select name="climate_id" id="climate_id" required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<option value="">Select a climate</option>
|
||||
{% for climate in climates %}
|
||||
<option value="{{ climate.id }}" {% if plant.climate_id == climate.id %}selected{% endif %}>{{ climate.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="environment_id" class="block text-sm font-medium text-gray-700">Environment</label>
|
||||
<select name="environment_id" id="environment_id" required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<option value="">Select an environment</option>
|
||||
{% for environment in environments %}
|
||||
<option value="{{ environment.id }}" {% if plant.environment_id == environment.id %}selected{% endif %}>{{ environment.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="light_id" class="block text-sm font-medium text-gray-700">Light</label>
|
||||
<select name="light_id" id="light_id" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<option value="">Select light requirement</option>
|
||||
{% for light in lights %}
|
||||
<option value="{{ light.id }}" {% if plant.light_id == light.id %}selected{% endif %}>{{ light.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="toxicity_id" class="block text-sm font-medium text-gray-700">Toxicity</label>
|
||||
<select name="toxicity_id" id="toxicity_id" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<option value="">Select toxicity level</option>
|
||||
{% for toxicity in toxicities %}
|
||||
<option value="{{ toxicity.id }}" {% if plant.toxicity_id == toxicity.id %}selected{% endif %}>{{ toxicity.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="size_id" class="block text-sm font-medium text-gray-700">Size</label>
|
||||
<select name="size_id" id="size_id" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<option value="">Select size category</option>
|
||||
{% for size in sizes %}
|
||||
<option value="{{ size.id }}" {% if plant.size_id == size.id %}selected{% endif %}>{{ size.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="care_difficulty_id" class="block text-sm font-medium text-gray-700">Care Difficulty</label>
|
||||
<select name="care_difficulty_id" id="care_difficulty_id" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<option value="">Select difficulty level</option>
|
||||
{% for difficulty in difficulties %}
|
||||
<option value="{{ difficulty.id }}" {% if plant.care_difficulty_id == difficulty.id %}selected{% endif %}>{{ difficulty.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="growth_rate_id" class="block text-sm font-medium text-gray-700">Growth Rate</label>
|
||||
<select name="growth_rate_id" id="growth_rate_id" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<option value="">Select growth rate</option>
|
||||
{% for rate in growth_rates %}
|
||||
<option value="{{ rate.id }}" {% if plant.growth_rate_id == rate.id %}selected{% endif %}>{{ rate.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Products</label>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
{% for product in products %}
|
||||
<label class="inline-flex items-center">
|
||||
<input type="checkbox" name="product_ids" value="{{ product.id }}" {% if product.id|string in selected_products %}checked{% endif %} class="form-checkbox rounded text-[#6b8f71] focus:ring-[#6b8f71]">
|
||||
<span class="ml-2">{{ product.name }}</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label for="description" class="block text-sm font-medium text-gray-700">Description</label>
|
||||
<textarea name="description" id="description" rows="6" required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">{{ plant.description }}</textarea>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label for="care_guide" class="block text-sm font-medium text-gray-700">Care Guide</label>
|
||||
<div id="quill-care-guide" class="bg-white rounded border border-gray-300" style="min-height: 120px;">{{ plant.care_guide|safe }}</div>
|
||||
<textarea name="care_guide" id="care_guide" style="display:none;">{{ plant.care_guide }}</textarea>
|
||||
</div>
|
||||
<button type="submit" class="w-full btn-main text-lg font-semibold">Save Changes</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script src="https://cdn.quilljs.com/1.3.6/quill.js"></script>
|
||||
<link href="https://cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet">
|
||||
<script>
|
||||
var quill = new Quill('#quill-care-guide', { theme: 'snow' });
|
||||
// Set initial content from textarea if not already set
|
||||
var careGuideTextarea = document.getElementById('care_guide');
|
||||
if (careGuideTextarea.value) {
|
||||
quill.root.innerHTML = careGuideTextarea.value;
|
||||
}
|
||||
document.querySelector('form').onsubmit = function() {
|
||||
careGuideTextarea.value = quill.root.innerHTML;
|
||||
};
|
||||
</script>
|
||||
{% endblock %}
|
||||
26
templates/edit_product.html
Normal file
26
templates/edit_product.html
Normal file
@@ -0,0 +1,26 @@
|
||||
{% extends "admin_base.html" %}
|
||||
|
||||
{% block title %}Edit Product{% endblock %}
|
||||
|
||||
{% block admin_content %}
|
||||
<div class="max-w-xl mx-auto bg-white p-8 rounded-lg shadow-md">
|
||||
<h1 class="text-2xl font-bold mb-6">Edit Product</h1>
|
||||
<form method="POST" class="space-y-4">
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700">Name</label>
|
||||
<input type="text" name="name" id="name" value="{{ product.name }}" required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label for="description" class="block text-sm font-medium text-gray-700">Description</label>
|
||||
<textarea name="description" id="description" rows="3"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">{{ product.description }}</textarea>
|
||||
</div>
|
||||
<button type="submit"
|
||||
class="w-full bg-blue-500 text-white py-2 px-4 rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
|
||||
Save Changes
|
||||
</button>
|
||||
<a href="{{ url_for('manage_products') }}" class="block text-center mt-4 text-gray-500 hover:text-gray-700">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
32
templates/edit_size.html
Normal file
32
templates/edit_size.html
Normal file
@@ -0,0 +1,32 @@
|
||||
{% extends "admin_base.html" %}
|
||||
|
||||
{% block title %}Edit Size Category{% endblock %}
|
||||
|
||||
{% block admin_content %}
|
||||
<div class="max-w-xl mx-auto bg-gray-50 p-6 rounded-lg">
|
||||
<h2 class="text-2xl font-bold mb-4">Edit Size Category</h2>
|
||||
<form method="POST" enctype="multipart/form-data" class="space-y-4">
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700">Name</label>
|
||||
<input type="text" name="name" id="name" value="{{ size.name }}" required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label for="description" class="block text-sm font-medium text-gray-700">Description</label>
|
||||
<textarea name="description" id="description" rows="3"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">{{ size.description }}</textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label for="icon" class="block text-sm font-medium text-gray-700">Icon (SVG)</label>
|
||||
<input type="file" name="icon" id="icon" accept=".svg" class="mt-1 block w-full text-sm text-gray-700">
|
||||
{% if size.icon %}
|
||||
<div class="mt-2">
|
||||
<span class="text-xs text-gray-500">Current icon:</span>
|
||||
<img src="{{ url_for('static', filename='icons/' ~ size.icon) }}" alt="Icon" class="w-8 h-8 inline-block align-middle">
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<button type="submit" class="btn-main w-full">Save Changes</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
32
templates/edit_toxicity.html
Normal file
32
templates/edit_toxicity.html
Normal file
@@ -0,0 +1,32 @@
|
||||
{% extends "admin_base.html" %}
|
||||
|
||||
{% block title %}Edit Toxicity Level{% endblock %}
|
||||
|
||||
{% block admin_content %}
|
||||
<div class="max-w-xl mx-auto bg-gray-50 p-6 rounded-lg">
|
||||
<h2 class="text-2xl font-bold mb-4">Edit Toxicity Level</h2>
|
||||
<form method="POST" enctype="multipart/form-data" class="space-y-4">
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700">Name</label>
|
||||
<input type="text" name="name" id="name" value="{{ toxicity.name }}" required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label for="description" class="block text-sm font-medium text-gray-700">Description</label>
|
||||
<textarea name="description" id="description" rows="3"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">{{ toxicity.description }}</textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label for="icon" class="block text-sm font-medium text-gray-700">Icon (SVG)</label>
|
||||
<input type="file" name="icon" id="icon" accept=".svg" class="mt-1 block w-full text-sm text-gray-700">
|
||||
{% if toxicity.icon %}
|
||||
<div class="mt-2">
|
||||
<span class="text-xs text-gray-500">Current icon:</span>
|
||||
<img src="{{ url_for('static', filename='icons/' ~ toxicity.icon) }}" alt="Icon" class="w-8 h-8 inline-block align-middle">
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<button type="submit" class="btn-main w-full">Save Changes</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
173
templates/home.html
Normal file
173
templates/home.html
Normal file
@@ -0,0 +1,173 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Home{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<button id="open-filter-menu" class="sm:hidden btn-main mb-4 w-full">Filter</button>
|
||||
<div id="filter-modal" class="fixed inset-0 bg-black bg-opacity-40 z-50 flex items-center justify-center hidden sm:static sm:bg-transparent sm:z-auto sm:flex sm:items-start sm:justify-start">
|
||||
<div class="bg-white rounded-xl p-6 w-full max-w-md mx-auto relative sm:bg-transparent sm:p-0 sm:w-auto sm:max-w-none sm:mx-0">
|
||||
<button id="close-filter-menu" class="sm:hidden absolute top-4 right-4 text-2xl">×</button>
|
||||
<form id="plant-filter-form" method="get"
|
||||
class="flex flex-col sm:flex-row flex-wrap gap-4 items-center p-0 w-full mb-8">
|
||||
<input type="text" id="search" name="search" value="{{ search }}" placeholder="Search..." class="rounded-lg border border-gray-300 px-3 py-1 text-sm focus:border-[#6b8f71] focus:ring-[#6b8f71]" style="min-width: 160px;" autocomplete="off">
|
||||
<select id="climate" name="climate" class="rounded-lg border border-gray-300 px-3 py-1 text-sm focus:border-[#6b8f71] focus:ring-[#6b8f71]">
|
||||
<option value="">All Climates</option>
|
||||
{% for climate in climates %}
|
||||
<option value="{{ climate.id }}" {% if selected_climate == climate.id|string %}selected{% endif %}>{{ climate.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<select id="environment" name="environment" class="rounded-lg border border-gray-300 px-3 py-1 text-sm focus:border-[#6b8f71] focus:ring-[#6b8f71]">
|
||||
<option value="">All Environments</option>
|
||||
{% for environment in environments %}
|
||||
<option value="{{ environment.id }}" {% if selected_environment == environment.id|string %}selected{% endif %}>{{ environment.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<select id="light" name="light" class="rounded-lg border border-gray-300 px-3 py-1 text-sm focus:border-[#6b8f71] focus:ring-[#6b8f71]">
|
||||
<option value="">All Light</option>
|
||||
{% for light in lights %}
|
||||
<option value="{{ light.id }}" {% if selected_light == light.id|string %}selected{% endif %}>{{ light.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<select id="toxicity" name="toxicity" class="rounded-lg border border-gray-300 px-3 py-1 text-sm focus:border-[#6b8f71] focus:ring-[#6b8f71]">
|
||||
<option value="">All Toxicity</option>
|
||||
{% for toxicity in toxicities %}
|
||||
<option value="{{ toxicity.id }}" {% if selected_toxicity == toxicity.id|string %}selected{% endif %}>{{ toxicity.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<select id="size" name="size" class="rounded-lg border border-gray-300 px-3 py-1 text-sm focus:border-[#6b8f71] focus:ring-[#6b8f71]">
|
||||
<option value="">All Sizes</option>
|
||||
{% for size in sizes %}
|
||||
<option value="{{ size.id }}" {% if selected_size == size.id|string %}selected{% endif %}>{{ size.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<select id="care_difficulty" name="care_difficulty" class="rounded-lg border border-gray-300 px-3 py-1 text-sm focus:border-[#6b8f71] focus:ring-[#6b8f71]">
|
||||
<option value="">All Care Difficulty</option>
|
||||
{% for difficulty in difficulties %}
|
||||
<option value="{{ difficulty.id }}" {% if selected_care_difficulty == difficulty.id|string %}selected{% endif %}>{{ difficulty.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<select id="growth_rate" name="growth_rate" class="rounded-lg border border-gray-300 px-3 py-1 text-sm focus:border-[#6b8f71] focus:ring-[#6b8f71]">
|
||||
<option value="">All Growth Rates</option>
|
||||
{% for rate in growth_rates %}
|
||||
<option value="{{ rate.id }}" {% if selected_growth_rate == rate.id|string %}selected{% endif %}>{{ rate.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<a href="{{ url_for('home') }}" class="btn-secondary px-4 py-1 text-sm font-semibold ml-2 sm:ml-0">Clear</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// Debounce function
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function(...args) {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func.apply(this, args), wait);
|
||||
};
|
||||
}
|
||||
const form = document.getElementById('plant-filter-form');
|
||||
const searchInput = document.getElementById('search');
|
||||
const selects = [document.getElementById('climate'), document.getElementById('environment'), document.getElementById('light'), document.getElementById('toxicity'), document.getElementById('size'), document.getElementById('care_difficulty'), document.getElementById('growth_rate')];
|
||||
|
||||
// Auto-submit on select change
|
||||
selects.forEach(sel => sel.addEventListener('change', () => form.submit()));
|
||||
// Debounced auto-submit on search
|
||||
searchInput.addEventListener('input', debounce(() => form.submit(), 400));
|
||||
</script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const openBtn = document.getElementById('open-filter-menu');
|
||||
const closeBtn = document.getElementById('close-filter-menu');
|
||||
const modal = document.getElementById('filter-modal');
|
||||
if (openBtn && modal) {
|
||||
openBtn.addEventListener('click', () => modal.classList.remove('hidden'));
|
||||
}
|
||||
if (closeBtn && modal) {
|
||||
closeBtn.addEventListener('click', () => modal.classList.add('hidden'));
|
||||
}
|
||||
if (modal) {
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) modal.classList.add('hidden');
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<div class="masonry md:columns-2 xl:columns-3 gap-8">
|
||||
{% for plant in plants %}
|
||||
<a href="{{ url_for('plant', plant_id=plant.id) }}" class="block group focus:outline-none focus:ring-2 focus:ring-[#6b8f71] rounded-2xl">
|
||||
<article class="plant-card opacity-0 translate-y-8 transition-all duration-700 bg-white/90 shadow-xl rounded-2xl p-6 flex flex-col hover:shadow-2xl hover:scale-[1.02] group-hover:shadow-2xl group-hover:scale-[1.02] cursor-pointer">
|
||||
{% if plant.picture %}
|
||||
<img src="{{ url_for('static', filename='uploads/' ~ plant.picture) }}" alt="{{ plant.name }}" class="w-full h-64 object-cover rounded-xl shadow-md border-2 border-[#e6ebe0] mb-4">
|
||||
{% else %}
|
||||
<img src="{{ url_for('static', filename='images/placeholder-plant.svg') }}" alt="No image available" class="w-full h-64 object-cover rounded-xl shadow-md border-2 border-[#e6ebe0] mb-4 opacity-50">
|
||||
{% endif %}
|
||||
<div class="w-full flex flex-col gap-2">
|
||||
<h2 class="text-2xl font-bold mb-1 text-[#4e6b50] group-hover:text-[#3e5637] transition">{{ plant.name }}</h2>
|
||||
<div class="flex flex-wrap gap-2 mb-1">
|
||||
{% if plant.climate %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded bg-[#d0e7d2] text-[#3e5637] text-xs font-semibold">
|
||||
{% if plant.climate_icon %}
|
||||
<img src="{{ url_for('static', filename='icons/' ~ plant.climate_icon) }}" alt="Climate icon" class="w-4 h-4 mr-1 inline-block align-middle" />
|
||||
{% endif %}
|
||||
{{ plant.climate }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if plant.environment %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded bg-[#d0e7d2] text-[#3e5637] text-xs font-semibold">
|
||||
{% if plant.environment_icon %}
|
||||
<img src="{{ url_for('static', filename='icons/' ~ plant.environment_icon) }}" alt="Environment icon" class="w-4 h-4 mr-1 inline-block align-middle" />
|
||||
{% endif %}
|
||||
{{ plant.environment }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if plant.light %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded bg-[#d0e7d2] text-[#3e5637] text-xs font-semibold">
|
||||
{% if plant.light_icon %}
|
||||
<img src="{{ url_for('static', filename='icons/' ~ plant.light_icon) }}" alt="Light icon" class="w-4 h-4 mr-1 inline-block align-middle" />
|
||||
{% endif %}
|
||||
{{ plant.light }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if plant.toxicity %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded bg-[#d0e7d2] text-[#3e5637] text-xs font-semibold">
|
||||
{% if plant.toxicity_icon %}
|
||||
<img src="{{ url_for('static', filename='icons/' ~ plant.toxicity_icon) }}" alt="Toxicity icon" class="w-4 h-4 mr-1 inline-block align-middle" />
|
||||
{% endif %}
|
||||
{{ plant.toxicity }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2 text-xs mb-1">
|
||||
{% if plant.size %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded bg-[#e6ebe0] text-[#3e5637] font-semibold">
|
||||
{% if plant.size_icon %}
|
||||
<img src="{{ url_for('static', filename='icons/' ~ plant.size_icon) }}" alt="Size icon" class="w-4 h-4 mr-1 inline-block align-middle" />
|
||||
{% endif %}
|
||||
{{ plant.size }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if plant.care_difficulty %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded bg-[#e6ebe0] text-[#3e5637] font-semibold">
|
||||
{% if plant.care_difficulty_icon %}
|
||||
<img src="{{ url_for('static', filename='icons/' ~ plant.care_difficulty_icon) }}" alt="Care difficulty icon" class="w-4 h-4 mr-1 inline-block align-middle" />
|
||||
{% endif %}
|
||||
{{ plant.care_difficulty }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if plant.growth_rate %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded bg-[#e6ebe0] text-[#3e5637] font-semibold">
|
||||
{% if plant.growth_rate_icon %}
|
||||
<img src="{{ url_for('static', filename='icons/' ~ plant.growth_rate_icon) }}" alt="Growth rate icon" class="w-4 h-4 mr-1 inline-block align-middle" />
|
||||
{% endif %}
|
||||
{{ plant.growth_rate }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<p class="text-[#4e6b50] mt-2 text-sm">{{ plant.description[:120] }}{% if plant.description and plant.description|length > 120 %}...{% endif %}</p>
|
||||
<div class="text-xs text-[#6b8f71] mt-2">Added on <span class="local-date" data-date="{{ plant.date_added.isoformat() }}"></span></div>
|
||||
</div>
|
||||
</article>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
20
templates/login.html
Normal file
20
templates/login.html
Normal file
@@ -0,0 +1,20 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Admin Login{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-md mx-auto card">
|
||||
<h1 class="text-2xl font-bold mb-6 text-center text-[#3e5637]">Admin Login</h1>
|
||||
<form method="POST" class="space-y-4">
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-[#3e5637]">Username</label>
|
||||
<input type="text" name="username" id="username" required class="mt-1 block w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-[#6b8f71] focus:ring-[#6b8f71]">
|
||||
</div>
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-[#3e5637]">Password</label>
|
||||
<input type="password" name="password" id="password" required class="mt-1 block w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-[#6b8f71] focus:ring-[#6b8f71]">
|
||||
</div>
|
||||
<button type="submit" class="w-full btn-main text-lg font-semibold">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
139
templates/manage_attributes.html
Normal file
139
templates/manage_attributes.html
Normal file
@@ -0,0 +1,139 @@
|
||||
{% extends "admin_base.html" %}
|
||||
|
||||
{% block title %}Manage Plant Attributes{% endblock %}
|
||||
|
||||
{% block admin_content %}
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<h1 class="text-3xl font-bold mb-6 text-center">Manage Plant Attributes</h1>
|
||||
|
||||
<!-- Light Requirements -->
|
||||
<div class="bg-white shadow-lg rounded-xl p-6 mb-6">
|
||||
<h2 class="text-xl font-semibold mb-4 text-[#4e6b50]">Light Requirements</h2>
|
||||
<form method="POST" action="{{ url_for('manage_light') }}" class="space-y-4">
|
||||
<div class="flex gap-4">
|
||||
<input type="text" name="name" placeholder="New light requirement" required
|
||||
class="flex-1 rounded-lg border border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 px-3 py-2">
|
||||
<button type="submit" class="btn-main px-6">Add</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="mt-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{% for light in lights %}
|
||||
<div class="flex items-center justify-between p-3 bg-[#f8f9fa] rounded-lg">
|
||||
<span class="text-[#4e6b50]">{{ light.name }}</span>
|
||||
<form method="POST" action="{{ url_for('delete_light', light_id=light.id) }}" class="inline">
|
||||
<button type="submit" class="text-red-600 hover:text-red-800">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toxicity Levels -->
|
||||
<div class="bg-white shadow-lg rounded-xl p-6 mb-6">
|
||||
<h2 class="text-xl font-semibold mb-4 text-[#4e6b50]">Toxicity Levels</h2>
|
||||
<form method="POST" action="{{ url_for('manage_toxicity') }}" class="space-y-4">
|
||||
<div class="flex gap-4">
|
||||
<input type="text" name="name" placeholder="New toxicity level" required
|
||||
class="flex-1 rounded-lg border border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 px-3 py-2">
|
||||
<button type="submit" class="btn-main px-6">Add</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="mt-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{% for toxicity in toxicities %}
|
||||
<div class="flex items-center justify-between p-3 bg-[#f8f9fa] rounded-lg">
|
||||
<span class="text-[#4e6b50]">{{ toxicity.name }}</span>
|
||||
<form method="POST" action="{{ url_for('delete_toxicity', toxicity_id=toxicity.id) }}" class="inline">
|
||||
<button type="submit" class="text-red-600 hover:text-red-800">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Size Categories -->
|
||||
<div class="bg-white shadow-lg rounded-xl p-6 mb-6">
|
||||
<h2 class="text-xl font-semibold mb-4 text-[#4e6b50]">Size Categories</h2>
|
||||
<form method="POST" action="{{ url_for('manage_size') }}" class="space-y-4">
|
||||
<div class="flex gap-4">
|
||||
<input type="text" name="name" placeholder="New size category" required
|
||||
class="flex-1 rounded-lg border border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 px-3 py-2">
|
||||
<button type="submit" class="btn-main px-6">Add</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="mt-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{% for size in sizes %}
|
||||
<div class="flex items-center justify-between p-3 bg-[#f8f9fa] rounded-lg">
|
||||
<span class="text-[#4e6b50]">{{ size.name }}</span>
|
||||
<form method="POST" action="{{ url_for('delete_size', size_id=size.id) }}" class="inline">
|
||||
<button type="submit" class="text-red-600 hover:text-red-800">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Care Difficulty Levels -->
|
||||
<div class="bg-white shadow-lg rounded-xl p-6 mb-6">
|
||||
<h2 class="text-xl font-semibold mb-4 text-[#4e6b50]">Care Difficulty Levels</h2>
|
||||
<form method="POST" action="{{ url_for('manage_care_difficulty') }}" class="space-y-4">
|
||||
<div class="flex gap-4">
|
||||
<input type="text" name="name" placeholder="New difficulty level" required
|
||||
class="flex-1 rounded-lg border border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 px-3 py-2">
|
||||
<button type="submit" class="btn-main px-6">Add</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="mt-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{% for difficulty in difficulties %}
|
||||
<div class="flex items-center justify-between p-3 bg-[#f8f9fa] rounded-lg">
|
||||
<span class="text-[#4e6b50]">{{ difficulty.name }}</span>
|
||||
<form method="POST" action="{{ url_for('delete_care_difficulty', difficulty_id=difficulty.id) }}" class="inline">
|
||||
<button type="submit" class="text-red-600 hover:text-red-800">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Growth Rate Categories -->
|
||||
<div class="bg-white shadow-lg rounded-xl p-6 mb-6">
|
||||
<h2 class="text-xl font-semibold mb-4 text-[#4e6b50]">Growth Rate Categories</h2>
|
||||
<form method="POST" action="{{ url_for('manage_growth_rate') }}" class="space-y-4">
|
||||
<div class="flex gap-4">
|
||||
<input type="text" name="name" placeholder="New growth rate category" required
|
||||
class="flex-1 rounded-lg border border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 px-3 py-2">
|
||||
<button type="submit" class="btn-main px-6">Add</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="mt-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{% for rate in growth_rates %}
|
||||
<div class="flex items-center justify-between p-3 bg-[#f8f9fa] rounded-lg">
|
||||
<span class="text-[#4e6b50]">{{ rate.name }}</span>
|
||||
<form method="POST" action="{{ url_for('delete_growth_rate', rate_id=rate.id) }}" class="inline">
|
||||
<button type="submit" class="text-red-600 hover:text-red-800">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
59
templates/manage_care_difficulties.html
Normal file
59
templates/manage_care_difficulties.html
Normal file
@@ -0,0 +1,59 @@
|
||||
{% extends "admin_base.html" %}
|
||||
|
||||
{% block title %}Manage Care Difficulties{% endblock %}
|
||||
|
||||
{% block admin_content %}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Add new care difficulty form -->
|
||||
<div class="bg-gray-50 p-6 rounded-lg">
|
||||
<h2 class="text-xl font-bold mb-4">Add New Care Difficulty</h2>
|
||||
<form method="POST" enctype="multipart/form-data" class="space-y-4">
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700">Name</label>
|
||||
<input type="text" name="name" id="name" required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label for="description" class="block text-sm font-medium text-gray-700">Description</label>
|
||||
<textarea name="description" id="description" rows="3"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label for="icon" class="block text-sm font-medium text-gray-700">Icon (SVG)</label>
|
||||
<input type="file" name="icon" id="icon" accept=".svg" class="mt-1 block w-full text-sm text-gray-700">
|
||||
</div>
|
||||
<button type="submit" class="w-full btn-main text-lg font-semibold">Add Care Difficulty</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- List of existing care difficulties -->
|
||||
<div class="bg-gray-50 p-6 rounded-lg">
|
||||
<h2 class="text-xl font-bold mb-4">Existing Care Difficulties</h2>
|
||||
<div class="space-y-4">
|
||||
{% for difficulty in difficulties %}
|
||||
<div class="p-4 rounded-lg shadow flex justify-between items-center">
|
||||
<div class="flex items-center gap-3">
|
||||
{% if difficulty.icon %}
|
||||
<img src="{{ url_for('static', filename='icons/' ~ difficulty.icon) }}" alt="Icon" class="w-8 h-8 inline-block">
|
||||
{% endif %}
|
||||
<div>
|
||||
<h3 class="font-bold">{{ difficulty.name }}</h3>
|
||||
{% if difficulty.description %}
|
||||
<p class="text-gray-600 mt-2">{{ difficulty.description }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<a href="{{ url_for('edit_care_difficulty', difficulty_id=difficulty.id) }}" class="btn-edit">Edit</a>
|
||||
<form id="delete-difficulty-{{ difficulty.id }}" method="POST" action="{{ url_for('delete_care_difficulty', difficulty_id=difficulty.id) }}" style="display:inline;">
|
||||
<button type="button" onclick="showDeleteModal('delete-difficulty-{{ difficulty.id }}', 'Are you sure you want to delete the care difficulty {{ difficulty.name }}?')" class="btn-delete">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-gray-500">No care difficulties added yet.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
59
templates/manage_climates.html
Normal file
59
templates/manage_climates.html
Normal file
@@ -0,0 +1,59 @@
|
||||
{% extends "admin_base.html" %}
|
||||
|
||||
{% block title %}Manage Climates{% endblock %}
|
||||
|
||||
{% block admin_content %}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Add new climate form -->
|
||||
<div class="bg-gray-50 p-6 rounded-lg">
|
||||
<h2 class="text-xl font-bold mb-4">Add New Climate</h2>
|
||||
<form method="POST" class="space-y-4">
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700">Name</label>
|
||||
<input type="text" name="name" id="name" required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label for="description" class="block text-sm font-medium text-gray-700">Description</label>
|
||||
<textarea name="description" id="description" rows="3"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label for="icon" class="block text-sm font-medium text-gray-700">Icon (SVG)</label>
|
||||
<input type="file" name="icon" id="icon" accept=".svg" class="mt-1 block w-full text-sm text-gray-700">
|
||||
</div>
|
||||
<button type="submit" class="w-full btn-main text-lg font-semibold">Add Climate</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- List of existing climates -->
|
||||
<div class="bg-gray-50 p-6 rounded-lg">
|
||||
<h2 class="text-xl font-bold mb-4">Existing Climates</h2>
|
||||
<div class="space-y-4">
|
||||
{% for climate in climates %}
|
||||
<div class="p-4 rounded-lg shadow flex justify-between items-center">
|
||||
<div class="flex items-center gap-3">
|
||||
{% if climate.icon %}
|
||||
<img src="{{ url_for('static', filename='icons/' ~ climate.icon) }}" alt="Icon" class="w-8 h-8 inline-block">
|
||||
{% endif %}
|
||||
<div>
|
||||
<h3 class="font-bold">{{ climate.name }}</h3>
|
||||
{% if climate.description %}
|
||||
<p class="text-gray-600 mt-2">{{ climate.description }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<a href="{{ url_for('edit_climate', climate_id=climate.id) }}" class="btn-edit">Edit</a>
|
||||
<form id="delete-climate-{{ climate.id }}" method="POST" action="{{ url_for('delete_climate', climate_id=climate.id) }}" style="display:inline;">
|
||||
<button type="button" onclick="showDeleteModal('delete-climate-{{ climate.id }}', 'Are you sure you want to delete the climate {{ climate.name }}?')" class="btn-delete">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-gray-500">No climates added yet.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
59
templates/manage_environments.html
Normal file
59
templates/manage_environments.html
Normal file
@@ -0,0 +1,59 @@
|
||||
{% extends "admin_base.html" %}
|
||||
|
||||
{% block title %}Manage Environments{% endblock %}
|
||||
|
||||
{% block admin_content %}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Add new environment form -->
|
||||
<div class="bg-gray-50 p-6 rounded-lg">
|
||||
<h2 class="text-xl font-bold mb-4">Add New Environment</h2>
|
||||
<form method="POST" class="space-y-4">
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700">Name</label>
|
||||
<input type="text" name="name" id="name" required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label for="description" class="block text-sm font-medium text-gray-700">Description</label>
|
||||
<textarea name="description" id="description" rows="3"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label for="icon" class="block text-sm font-medium text-gray-700">Icon (SVG)</label>
|
||||
<input type="file" name="icon" id="icon" accept=".svg" class="mt-1 block w-full text-sm text-gray-700">
|
||||
</div>
|
||||
<button type="submit" class="w-full btn-main text-lg font-semibold">Add Environment</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- List of existing environments -->
|
||||
<div class="bg-gray-50 p-6 rounded-lg">
|
||||
<h2 class="text-xl font-bold mb-4">Existing Environments</h2>
|
||||
<div class="space-y-4">
|
||||
{% for environment in environments %}
|
||||
<div class="p-4 rounded-lg shadow flex justify-between items-center">
|
||||
<div class="flex items-center gap-3">
|
||||
{% if environment.icon %}
|
||||
<img src="{{ url_for('static', filename='icons/' ~ environment.icon) }}" alt="Icon" class="w-8 h-8 inline-block">
|
||||
{% endif %}
|
||||
<div>
|
||||
<h3 class="font-bold">{{ environment.name }}</h3>
|
||||
{% if environment.description %}
|
||||
<p class="text-gray-600 mt-2">{{ environment.description }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<a href="{{ url_for('edit_environment', environment_id=environment.id) }}" class="btn-edit">Edit</a>
|
||||
<form id="delete-environment-{{ environment.id }}" method="POST" action="{{ url_for('delete_environment', environment_id=environment.id) }}" style="display:inline;">
|
||||
<button type="button" onclick="showDeleteModal('delete-environment-{{ environment.id }}', 'Are you sure you want to delete the environment {{ environment.name }}?')" class="btn-delete">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-gray-500">No environments added yet.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
59
templates/manage_growth_rates.html
Normal file
59
templates/manage_growth_rates.html
Normal file
@@ -0,0 +1,59 @@
|
||||
{% extends "admin_base.html" %}
|
||||
|
||||
{% block title %}Manage Growth Rates{% endblock %}
|
||||
|
||||
{% block admin_content %}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Add new growth rate form -->
|
||||
<div class="bg-gray-50 p-6 rounded-lg">
|
||||
<h2 class="text-xl font-bold mb-4">Add New Growth Rate</h2>
|
||||
<form method="POST" enctype="multipart/form-data" class="space-y-4">
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700">Name</label>
|
||||
<input type="text" name="name" id="name" required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label for="description" class="block text-sm font-medium text-gray-700">Description</label>
|
||||
<textarea name="description" id="description" rows="3"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label for="icon" class="block text-sm font-medium text-gray-700">Icon (SVG)</label>
|
||||
<input type="file" name="icon" id="icon" accept=".svg" class="mt-1 block w-full text-sm text-gray-700">
|
||||
</div>
|
||||
<button type="submit" class="w-full btn-main text-lg font-semibold">Add Growth Rate</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- List of existing growth rates -->
|
||||
<div class="bg-gray-50 p-6 rounded-lg">
|
||||
<h2 class="text-xl font-bold mb-4">Existing Growth Rates</h2>
|
||||
<div class="space-y-4">
|
||||
{% for rate in rates %}
|
||||
<div class="p-4 rounded-lg shadow flex justify-between items-center">
|
||||
<div class="flex items-center gap-3">
|
||||
{% if rate.icon %}
|
||||
<img src="{{ url_for('static', filename='icons/' ~ rate.icon) }}" alt="Icon" class="w-8 h-8 inline-block">
|
||||
{% endif %}
|
||||
<div>
|
||||
<h3 class="font-bold">{{ rate.name }}</h3>
|
||||
{% if rate.description %}
|
||||
<p class="text-gray-600 mt-2">{{ rate.description }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<a href="{{ url_for('edit_growth_rate', rate_id=rate.id) }}" class="btn-edit">Edit</a>
|
||||
<form id="delete-rate-{{ rate.id }}" method="POST" action="{{ url_for('delete_growth_rate', rate_id=rate.id) }}" style="display:inline;">
|
||||
<button type="button" onclick="showDeleteModal('delete-rate-{{ rate.id }}', 'Are you sure you want to delete the growth rate {{ rate.name }}?')" class="btn-delete">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-gray-500">No growth rates added yet.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
59
templates/manage_lights.html
Normal file
59
templates/manage_lights.html
Normal file
@@ -0,0 +1,59 @@
|
||||
{% extends "admin_base.html" %}
|
||||
|
||||
{% block title %}Manage Light Requirements{% endblock %}
|
||||
|
||||
{% block admin_content %}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Add new light requirement form -->
|
||||
<div class="bg-gray-50 p-6 rounded-lg">
|
||||
<h2 class="text-xl font-bold mb-4">Add New Light Requirement</h2>
|
||||
<form method="POST" enctype="multipart/form-data" class="space-y-4">
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700">Name</label>
|
||||
<input type="text" name="name" id="name" required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label for="description" class="block text-sm font-medium text-gray-700">Description</label>
|
||||
<textarea name="description" id="description" rows="3"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label for="icon" class="block text-sm font-medium text-gray-700">Icon (SVG)</label>
|
||||
<input type="file" name="icon" id="icon" accept=".svg" class="mt-1 block w-full text-sm text-gray-700">
|
||||
</div>
|
||||
<button type="submit" class="w-full btn-main text-lg font-semibold">Add Light Requirement</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- List of existing light requirements -->
|
||||
<div class="bg-gray-50 p-6 rounded-lg">
|
||||
<h2 class="text-xl font-bold mb-4">Existing Light Requirements</h2>
|
||||
<div class="space-y-4">
|
||||
{% for light in lights %}
|
||||
<div class="p-4 rounded-lg shadow flex justify-between items-center">
|
||||
<div class="flex items-center gap-3">
|
||||
{% if light.icon %}
|
||||
<img src="{{ url_for('static', filename='icons/' ~ light.icon) }}" alt="Icon" class="w-8 h-8 inline-block">
|
||||
{% endif %}
|
||||
<div>
|
||||
<h3 class="font-bold">{{ light.name }}</h3>
|
||||
{% if light.description %}
|
||||
<p class="text-gray-600 mt-2">{{ light.description }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<a href="{{ url_for('edit_light', light_id=light.id) }}" class="btn-edit">Edit</a>
|
||||
<form id="delete-light-{{ light.id }}" method="POST" action="{{ url_for('delete_light', light_id=light.id) }}" style="display:inline;">
|
||||
<button type="button" onclick="showDeleteModal('delete-light-{{ light.id }}', 'Are you sure you want to delete the light requirement {{ light.name }}?')" class="btn-delete">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-gray-500">No light requirements added yet.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
170
templates/manage_plants.html
Normal file
170
templates/manage_plants.html
Normal file
@@ -0,0 +1,170 @@
|
||||
{% extends "admin_base.html" %}
|
||||
|
||||
{% block title %}Plants{% endblock %}
|
||||
|
||||
{% block admin_content %}
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-2xl font-bold">All Plants</h2>
|
||||
<button id="show-add-plant" class="btn-main px-6 py-2 font-semibold">Add Plant</button>
|
||||
</div>
|
||||
<div id="add-plant-form-card" class="hidden mb-8">
|
||||
<div class="bg-gray-50 rounded-lg shadow p-6 max-w-2xl mx-auto">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-xl font-bold">Add New Plant</h3>
|
||||
<button type="button" id="close-add-plant" class="text-gray-500 hover:text-red-500 text-2xl font-bold leading-none">×</button>
|
||||
</div>
|
||||
<form id="add-plant-form" method="POST" enctype="multipart/form-data" class="space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700">Name</label>
|
||||
<input type="text" name="name" id="name" required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label for="picture" class="block text-sm font-medium text-gray-700">Picture</label>
|
||||
<input type="file" name="picture" id="picture"
|
||||
class="mt-1 block w-full text-sm text-gray-700">
|
||||
</div>
|
||||
<div>
|
||||
<label for="climate_id" class="block text-sm font-medium text-gray-700">Climate</label>
|
||||
<select name="climate_id" id="climate_id" required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<option value="">Select a climate</option>
|
||||
{% for climate in climates %}
|
||||
<option value="{{ climate.id }}">{{ climate.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="environment_id" class="block text-sm font-medium text-gray-700">Environment</label>
|
||||
<select name="environment_id" id="environment_id" required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<option value="">Select an environment</option>
|
||||
{% for environment in environments %}
|
||||
<option value="{{ environment.id }}">{{ environment.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="light" class="block text-sm font-medium text-gray-700">Light</label>
|
||||
<select name="light" id="light" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<option value="">Select light</option>
|
||||
<option value="Full Sun">Full Sun</option>
|
||||
<option value="Partial Shade">Partial Shade</option>
|
||||
<option value="Low Light">Low Light</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="toxicity" class="block text-sm font-medium text-gray-700">Toxicity</label>
|
||||
<select name="toxicity" id="toxicity" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<option value="">Select toxicity</option>
|
||||
<option value="Pet Safe">Pet Safe</option>
|
||||
<option value="Toxic to Pets">Toxic to Pets</option>
|
||||
<option value="Unknown">Unknown</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="size" class="block text-sm font-medium text-gray-700">Size</label>
|
||||
<select name="size" id="size" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<option value="">Select size</option>
|
||||
<option value="Small">Small</option>
|
||||
<option value="Medium">Medium</option>
|
||||
<option value="Large">Large</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="care_difficulty" class="block text-sm font-medium text-gray-700">Care Difficulty</label>
|
||||
<select name="care_difficulty" id="care_difficulty" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<option value="">Select difficulty</option>
|
||||
<option value="Easy">Easy</option>
|
||||
<option value="Moderate">Moderate</option>
|
||||
<option value="Hard">Hard</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="growth_rate" class="block text-sm font-medium text-gray-700">Growth Rate</label>
|
||||
<select name="growth_rate" id="growth_rate" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<option value="">Select growth rate</option>
|
||||
<option value="Fast">Fast</option>
|
||||
<option value="Moderate">Moderate</option>
|
||||
<option value="Slow">Slow</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Products</label>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
{% for product in products %}
|
||||
<label class="inline-flex items-center">
|
||||
<input type="checkbox" name="product_ids" value="{{ product.id }}" class="form-checkbox rounded text-[#6b8f71] focus:ring-[#6b8f71]">
|
||||
<span class="ml-2">{{ product.name }}</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label for="description" class="block text-sm font-medium text-gray-700">Description</label>
|
||||
<textarea name="description" id="description" rows="6" required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"></textarea>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label for="care_guide" class="block text-sm font-medium text-gray-700">Care Guide</label>
|
||||
<div id="quill-care-guide" class="bg-white rounded border border-gray-300" style="min-height: 120px;"></div>
|
||||
<textarea name="care_guide" id="care_guide" style="display:none;"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="w-full btn-main text-lg font-semibold">Add Plant</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full text-left text-sm">
|
||||
<thead>
|
||||
<tr class="border-b">
|
||||
<th class="py-2 px-3">Name</th>
|
||||
<th class="py-2 px-3">Climate</th>
|
||||
<th class="py-2 px-3">Environment</th>
|
||||
<th class="py-2 px-3">Added</th>
|
||||
<th class="py-2 px-3">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for plant in plants %}
|
||||
<tr class="border-b hover:bg-[#f5f7f2]">
|
||||
<td class="py-2 px-3 font-semibold">{{ plant.name }}</td>
|
||||
<td class="py-2 px-3">{{ climates[plant.climate_id] if plant.climate_id in climates else '' }}</td>
|
||||
<td class="py-2 px-3">{{ environments[plant.environment_id] if plant.environment_id in environments else '' }}</td>
|
||||
<td class="py-2 px-3"><span class="local-date" data-date="{{ plant.date_added.isoformat() }}"></span></td>
|
||||
<td class="py-2 px-3">
|
||||
<a href="{{ url_for('edit_plant', plant_id=plant.id) }}" class="btn-edit">Edit</a>
|
||||
<form id="delete-plant-{{ plant.id }}" method="POST" action="{{ url_for('delete_plant', plant_id=plant.id) }}" style="display:inline;">
|
||||
<button type="button" onclick="showDeleteModal('delete-plant-{{ plant.id }}', 'Are you sure you want to delete {{ plant.name }}?')" class="btn-delete ml-2">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="5" class="py-4 text-center text-gray-500">No plants found.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<script src="https://cdn.quilljs.com/1.3.6/quill.js"></script>
|
||||
<link href="https://cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet">
|
||||
<script>
|
||||
document.getElementById('show-add-plant').onclick = function() {
|
||||
var card = document.getElementById('add-plant-form-card');
|
||||
card.classList.toggle('hidden');
|
||||
if (!card.classList.contains('hidden')) {
|
||||
card.scrollIntoView({behavior: 'smooth'});
|
||||
}
|
||||
};
|
||||
document.getElementById('close-add-plant').onclick = function() {
|
||||
document.getElementById('add-plant-form-card').classList.add('hidden');
|
||||
};
|
||||
var quill = new Quill('#quill-care-guide', { theme: 'snow' });
|
||||
document.getElementById('add-plant-form').onsubmit = function() {
|
||||
document.getElementById('care_guide').value = quill.root.innerHTML;
|
||||
};
|
||||
</script>
|
||||
{% endblock %}
|
||||
50
templates/manage_products.html
Normal file
50
templates/manage_products.html
Normal file
@@ -0,0 +1,50 @@
|
||||
{% extends "admin_base.html" %}
|
||||
|
||||
{% block title %}Manage Products{% endblock %}
|
||||
|
||||
{% block admin_content %}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Add new product form -->
|
||||
<div class="bg-gray-50 p-6 rounded-lg">
|
||||
<h2 class="text-xl font-bold mb-4">Add New Product</h2>
|
||||
<form method="POST" class="space-y-4">
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700">Name</label>
|
||||
<input type="text" name="name" id="name" required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label for="description" class="block text-sm font-medium text-gray-700">Description</label>
|
||||
<textarea name="description" id="description" rows="3"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"></textarea>
|
||||
</div>
|
||||
<button type="submit" class="w-full btn-main text-lg font-semibold">Add Product</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- List of existing products -->
|
||||
<div class="bg-gray-50 p-6 rounded-lg">
|
||||
<h2 class="text-xl font-bold mb-4">Existing Products</h2>
|
||||
<div class="space-y-4">
|
||||
{% for product in products %}
|
||||
<div class="p-4 rounded-lg shadow flex justify-between items-center">
|
||||
<div>
|
||||
<h3 class="font-bold">{{ product.name }}</h3>
|
||||
{% if product.description %}
|
||||
<p class="text-gray-600 mt-2">{{ product.description }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<a href="{{ url_for('edit_product', product_id=product.id) }}" class="btn-edit">Edit</a>
|
||||
<form method="POST" action="{{ url_for('delete_product', product_id=product.id) }}" onsubmit="return confirm('Are you sure you want to delete this product?');">
|
||||
<button type="submit" class="btn-delete">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-gray-500">No products added yet.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
59
templates/manage_sizes.html
Normal file
59
templates/manage_sizes.html
Normal file
@@ -0,0 +1,59 @@
|
||||
{% extends "admin_base.html" %}
|
||||
|
||||
{% block title %}Manage Size Categories{% endblock %}
|
||||
|
||||
{% block admin_content %}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Add new size category form -->
|
||||
<div class="bg-gray-50 p-6 rounded-lg">
|
||||
<h2 class="text-xl font-bold mb-4">Add New Size Category</h2>
|
||||
<form method="POST" enctype="multipart/form-data" class="space-y-4">
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700">Name</label>
|
||||
<input type="text" name="name" id="name" required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label for="description" class="block text-sm font-medium text-gray-700">Description</label>
|
||||
<textarea name="description" id="description" rows="3"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label for="icon" class="block text-sm font-medium text-gray-700">Icon (SVG)</label>
|
||||
<input type="file" name="icon" id="icon" accept=".svg" class="mt-1 block w-full text-sm text-gray-700">
|
||||
</div>
|
||||
<button type="submit" class="w-full btn-main text-lg font-semibold">Add Size Category</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- List of existing size categories -->
|
||||
<div class="bg-gray-50 p-6 rounded-lg">
|
||||
<h2 class="text-xl font-bold mb-4">Existing Size Categories</h2>
|
||||
<div class="space-y-4">
|
||||
{% for size in sizes %}
|
||||
<div class="p-4 rounded-lg shadow flex justify-between items-center">
|
||||
<div class="flex items-center gap-3">
|
||||
{% if size.icon %}
|
||||
<img src="{{ url_for('static', filename='icons/' ~ size.icon) }}" alt="Icon" class="w-8 h-8 inline-block">
|
||||
{% endif %}
|
||||
<div>
|
||||
<h3 class="font-bold">{{ size.name }}</h3>
|
||||
{% if size.description %}
|
||||
<p class="text-gray-600 mt-2">{{ size.description }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<a href="{{ url_for('edit_size', size_id=size.id) }}" class="btn-edit">Edit</a>
|
||||
<form id="delete-size-{{ size.id }}" method="POST" action="{{ url_for('delete_size', size_id=size.id) }}" style="display:inline;">
|
||||
<button type="button" onclick="showDeleteModal('delete-size-{{ size.id }}', 'Are you sure you want to delete the size category {{ size.name }}?')" class="btn-delete">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-gray-500">No size categories added yet.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
59
templates/manage_toxicities.html
Normal file
59
templates/manage_toxicities.html
Normal file
@@ -0,0 +1,59 @@
|
||||
{% extends "admin_base.html" %}
|
||||
|
||||
{% block title %}Manage Toxicity Levels{% endblock %}
|
||||
|
||||
{% block admin_content %}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Add new toxicity level form -->
|
||||
<div class="bg-gray-50 p-6 rounded-lg">
|
||||
<h2 class="text-xl font-bold mb-4">Add New Toxicity Level</h2>
|
||||
<form method="POST" enctype="multipart/form-data" class="space-y-4">
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700">Name</label>
|
||||
<input type="text" name="name" id="name" required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label for="description" class="block text-sm font-medium text-gray-700">Description</label>
|
||||
<textarea name="description" id="description" rows="3"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label for="icon" class="block text-sm font-medium text-gray-700">Icon (SVG)</label>
|
||||
<input type="file" name="icon" id="icon" accept=".svg" class="mt-1 block w-full text-sm text-gray-700">
|
||||
</div>
|
||||
<button type="submit" class="w-full btn-main text-lg font-semibold">Add Toxicity Level</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- List of existing toxicity levels -->
|
||||
<div class="bg-gray-50 p-6 rounded-lg">
|
||||
<h2 class="text-xl font-bold mb-4">Existing Toxicity Levels</h2>
|
||||
<div class="space-y-4">
|
||||
{% for toxicity in toxicities %}
|
||||
<div class="p-4 rounded-lg shadow flex justify-between items-center">
|
||||
<div class="flex items-center gap-3">
|
||||
{% if toxicity.icon %}
|
||||
<img src="{{ url_for('static', filename='icons/' ~ toxicity.icon) }}" alt="Icon" class="w-8 h-8 inline-block">
|
||||
{% endif %}
|
||||
<div>
|
||||
<h3 class="font-bold">{{ toxicity.name }}</h3>
|
||||
{% if toxicity.description %}
|
||||
<p class="text-gray-600 mt-2">{{ toxicity.description }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<a href="{{ url_for('edit_toxicity', toxicity_id=toxicity.id) }}" class="btn-edit">Edit</a>
|
||||
<form id="delete-toxicity-{{ toxicity.id }}" method="POST" action="{{ url_for('delete_toxicity', toxicity_id=toxicity.id) }}" style="display:inline;">
|
||||
<button type="button" onclick="showDeleteModal('delete-toxicity-{{ toxicity.id }}', 'Are you sure you want to delete the toxicity level {{ toxicity.name }}?')" class="btn-delete">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-gray-500">No toxicity levels added yet.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
93
templates/post.html
Normal file
93
templates/post.html
Normal file
@@ -0,0 +1,93 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ plant.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<article class="max-w-4xl mx-auto bg-white p-8 rounded-2xl shadow-xl mt-8">
|
||||
<div class="flex flex-col md:flex-row gap-4 md:gap-6">
|
||||
<div class="md:w-1/2 flex-shrink-0 mb-6 md:mb-0">
|
||||
{% if plant.picture %}
|
||||
<img src="{{ url_for('static', filename='uploads/' ~ plant.picture) }}" alt="{{ plant.name }}" class="w-full h-96 object-cover rounded-xl shadow-md border-2 border-[#e6ebe0]">
|
||||
{% else %}
|
||||
<img src="{{ url_for('static', filename='images/placeholder-plant.svg') }}" alt="No image available" class="w-full h-96 object-cover rounded-xl shadow-md border-2 border-[#e6ebe0] opacity-50">
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="md:w-1/2 flex flex-col gap-4">
|
||||
<h1 class="text-4xl font-bold text-[#4e6b50] mb-2">{{ plant.name }}</h1>
|
||||
<div class="flex flex-wrap gap-2 mb-2">
|
||||
{% if plant.climate %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded bg-[#d0e7d2] text-[#3e5637] text-xs font-semibold">
|
||||
{% if plant.climate.icon %}<img src="{{ url_for('static', filename='icons/' ~ plant.climate.icon) }}" alt="Climate icon" class="w-4 h-4 mr-1 inline-block align-middle" />{% endif %}
|
||||
{{ plant.climate.name }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if plant.environment %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded bg-[#d0e7d2] text-[#3e5637] text-xs font-semibold">
|
||||
{% if plant.environment.icon %}<img src="{{ url_for('static', filename='icons/' ~ plant.environment.icon) }}" alt="Environment icon" class="w-4 h-4 mr-1 inline-block align-middle" />{% endif %}
|
||||
{{ plant.environment.name }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if plant.light %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded bg-[#e6ebe0] text-[#3e5637] text-xs font-semibold">
|
||||
{% if plant.light.icon %}<img src="{{ url_for('static', filename='icons/' ~ plant.light.icon) }}" alt="Light icon" class="w-4 h-4 mr-1 inline-block align-middle" />{% endif %}
|
||||
{{ plant.light.name }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if plant.toxicity %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded bg-[#fff4e6] text-[#3e5637] text-xs font-semibold">
|
||||
{% if plant.toxicity.icon %}<img src="{{ url_for('static', filename='icons/' ~ plant.toxicity.icon) }}" alt="Toxicity icon" class="w-4 h-4 mr-1 inline-block align-middle" />{% endif %}
|
||||
{{ plant.toxicity.name }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if plant.size %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded bg-[#e6f7f5] text-[#3e5637] text-xs font-semibold">
|
||||
{% if plant.size.icon %}<img src="{{ url_for('static', filename='icons/' ~ plant.size.icon) }}" alt="Size icon" class="w-4 h-4 mr-1 inline-block align-middle" />{% endif %}
|
||||
{{ plant.size.name }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if plant.care_difficulty %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded bg-[#f3e6ff] text-[#3e5637] text-xs font-semibold">
|
||||
{% if plant.care_difficulty.icon %}<img src="{{ url_for('static', filename='icons/' ~ plant.care_difficulty.icon) }}" alt="Care icon" class="w-4 h-4 mr-1 inline-block align-middle" />{% endif %}
|
||||
{{ plant.care_difficulty.name }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if plant.growth_rate %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded bg-[#f7fae6] text-[#3e5637] text-xs font-semibold">
|
||||
{% if plant.growth_rate.icon %}<img src="{{ url_for('static', filename='icons/' ~ plant.growth_rate.icon) }}" alt="Growth icon" class="w-4 h-4 mr-1 inline-block align-middle" />{% endif %}
|
||||
{{ plant.growth_rate.name }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="prose max-w-none mb-4 text-[#4e6b50]">{{ plant.description|safe }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if products %}
|
||||
<div class="mb-8 mt-4">
|
||||
<span class="font-semibold text-[#4e6b50]">Products:</span>
|
||||
<span class="flex flex-wrap gap-2 mt-1">
|
||||
{% for product in products %}
|
||||
<span class="inline-block bg-[#e6ebe0] text-[#3e5637] px-3 py-1 rounded text-xs font-semibold border border-[#d0e7d2]">{{ product.name }}</span>
|
||||
{% endfor %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if plant.care_guide %}
|
||||
<div class="mt-12">
|
||||
<h2 class="text-2xl font-bold text-[#6b8f71] mb-3">Care Guide</h2>
|
||||
<div class="prose max-w-none bg-[#f8f9fa] border border-[#e6ebe0] rounded-xl p-6 text-[#3e5637] shadow" style="min-height:80px;">
|
||||
{{ plant.care_guide|safe }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="text-xs text-[#6b8f71] mt-8">Added on <span class="local-date" data-date="{{ plant.date_added.isoformat() }}"></span></div>
|
||||
<div class="mt-8 flex flex-wrap gap-4">
|
||||
<a href="{{ url_for('home', climate=plant.climate_id, environment=plant.environment_id) }}"
|
||||
class="inline-block bg-[#6b8f71] text-white hover:bg-[#4e6b50] font-semibold px-6 py-2 rounded-lg shadow transition-colors duration-200">
|
||||
🌱 View Similar Plants
|
||||
</a>
|
||||
<a href="{{ url_for('home') }}" class="inline-block bg-[#e6ebe0] text-[#3e5637] hover:bg-[#b7c7a3] hover:text-[#4e6b50] font-semibold px-6 py-2 rounded-lg shadow transition-colors duration-200">
|
||||
← Back to Home
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
{% endblock %}
|
||||
164
venv/Include/site/python3.10/greenlet/greenlet.h
Normal file
164
venv/Include/site/python3.10/greenlet/greenlet.h
Normal file
@@ -0,0 +1,164 @@
|
||||
/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */
|
||||
|
||||
/* Greenlet object interface */
|
||||
|
||||
#ifndef Py_GREENLETOBJECT_H
|
||||
#define Py_GREENLETOBJECT_H
|
||||
|
||||
|
||||
#include <Python.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* This is deprecated and undocumented. It does not change. */
|
||||
#define GREENLET_VERSION "1.0.0"
|
||||
|
||||
#ifndef GREENLET_MODULE
|
||||
#define implementation_ptr_t void*
|
||||
#endif
|
||||
|
||||
typedef struct _greenlet {
|
||||
PyObject_HEAD
|
||||
PyObject* weakreflist;
|
||||
PyObject* dict;
|
||||
implementation_ptr_t pimpl;
|
||||
} PyGreenlet;
|
||||
|
||||
#define PyGreenlet_Check(op) (op && PyObject_TypeCheck(op, &PyGreenlet_Type))
|
||||
|
||||
|
||||
/* C API functions */
|
||||
|
||||
/* Total number of symbols that are exported */
|
||||
#define PyGreenlet_API_pointers 12
|
||||
|
||||
#define PyGreenlet_Type_NUM 0
|
||||
#define PyExc_GreenletError_NUM 1
|
||||
#define PyExc_GreenletExit_NUM 2
|
||||
|
||||
#define PyGreenlet_New_NUM 3
|
||||
#define PyGreenlet_GetCurrent_NUM 4
|
||||
#define PyGreenlet_Throw_NUM 5
|
||||
#define PyGreenlet_Switch_NUM 6
|
||||
#define PyGreenlet_SetParent_NUM 7
|
||||
|
||||
#define PyGreenlet_MAIN_NUM 8
|
||||
#define PyGreenlet_STARTED_NUM 9
|
||||
#define PyGreenlet_ACTIVE_NUM 10
|
||||
#define PyGreenlet_GET_PARENT_NUM 11
|
||||
|
||||
#ifndef GREENLET_MODULE
|
||||
/* This section is used by modules that uses the greenlet C API */
|
||||
static void** _PyGreenlet_API = NULL;
|
||||
|
||||
# define PyGreenlet_Type \
|
||||
(*(PyTypeObject*)_PyGreenlet_API[PyGreenlet_Type_NUM])
|
||||
|
||||
# define PyExc_GreenletError \
|
||||
((PyObject*)_PyGreenlet_API[PyExc_GreenletError_NUM])
|
||||
|
||||
# define PyExc_GreenletExit \
|
||||
((PyObject*)_PyGreenlet_API[PyExc_GreenletExit_NUM])
|
||||
|
||||
/*
|
||||
* PyGreenlet_New(PyObject *args)
|
||||
*
|
||||
* greenlet.greenlet(run, parent=None)
|
||||
*/
|
||||
# define PyGreenlet_New \
|
||||
(*(PyGreenlet * (*)(PyObject * run, PyGreenlet * parent)) \
|
||||
_PyGreenlet_API[PyGreenlet_New_NUM])
|
||||
|
||||
/*
|
||||
* PyGreenlet_GetCurrent(void)
|
||||
*
|
||||
* greenlet.getcurrent()
|
||||
*/
|
||||
# define PyGreenlet_GetCurrent \
|
||||
(*(PyGreenlet * (*)(void)) _PyGreenlet_API[PyGreenlet_GetCurrent_NUM])
|
||||
|
||||
/*
|
||||
* PyGreenlet_Throw(
|
||||
* PyGreenlet *greenlet,
|
||||
* PyObject *typ,
|
||||
* PyObject *val,
|
||||
* PyObject *tb)
|
||||
*
|
||||
* g.throw(...)
|
||||
*/
|
||||
# define PyGreenlet_Throw \
|
||||
(*(PyObject * (*)(PyGreenlet * self, \
|
||||
PyObject * typ, \
|
||||
PyObject * val, \
|
||||
PyObject * tb)) \
|
||||
_PyGreenlet_API[PyGreenlet_Throw_NUM])
|
||||
|
||||
/*
|
||||
* PyGreenlet_Switch(PyGreenlet *greenlet, PyObject *args)
|
||||
*
|
||||
* g.switch(*args, **kwargs)
|
||||
*/
|
||||
# define PyGreenlet_Switch \
|
||||
(*(PyObject * \
|
||||
(*)(PyGreenlet * greenlet, PyObject * args, PyObject * kwargs)) \
|
||||
_PyGreenlet_API[PyGreenlet_Switch_NUM])
|
||||
|
||||
/*
|
||||
* PyGreenlet_SetParent(PyObject *greenlet, PyObject *new_parent)
|
||||
*
|
||||
* g.parent = new_parent
|
||||
*/
|
||||
# define PyGreenlet_SetParent \
|
||||
(*(int (*)(PyGreenlet * greenlet, PyGreenlet * nparent)) \
|
||||
_PyGreenlet_API[PyGreenlet_SetParent_NUM])
|
||||
|
||||
/*
|
||||
* PyGreenlet_GetParent(PyObject* greenlet)
|
||||
*
|
||||
* return greenlet.parent;
|
||||
*
|
||||
* This could return NULL even if there is no exception active.
|
||||
* If it does not return NULL, you are responsible for decrementing the
|
||||
* reference count.
|
||||
*/
|
||||
# define PyGreenlet_GetParent \
|
||||
(*(PyGreenlet* (*)(PyGreenlet*)) \
|
||||
_PyGreenlet_API[PyGreenlet_GET_PARENT_NUM])
|
||||
|
||||
/*
|
||||
* deprecated, undocumented alias.
|
||||
*/
|
||||
# define PyGreenlet_GET_PARENT PyGreenlet_GetParent
|
||||
|
||||
# define PyGreenlet_MAIN \
|
||||
(*(int (*)(PyGreenlet*)) \
|
||||
_PyGreenlet_API[PyGreenlet_MAIN_NUM])
|
||||
|
||||
# define PyGreenlet_STARTED \
|
||||
(*(int (*)(PyGreenlet*)) \
|
||||
_PyGreenlet_API[PyGreenlet_STARTED_NUM])
|
||||
|
||||
# define PyGreenlet_ACTIVE \
|
||||
(*(int (*)(PyGreenlet*)) \
|
||||
_PyGreenlet_API[PyGreenlet_ACTIVE_NUM])
|
||||
|
||||
|
||||
|
||||
|
||||
/* Macro that imports greenlet and initializes C API */
|
||||
/* NOTE: This has actually moved to ``greenlet._greenlet._C_API``, but we
|
||||
keep the older definition to be sure older code that might have a copy of
|
||||
the header still works. */
|
||||
# define PyGreenlet_Import() \
|
||||
{ \
|
||||
_PyGreenlet_API = (void**)PyCapsule_Import("greenlet._C_API", 0); \
|
||||
}
|
||||
|
||||
#endif /* GREENLET_MODULE */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* !Py_GREENLETOBJECT_H */
|
||||
@@ -0,0 +1 @@
|
||||
pip
|
||||
28
venv/Lib/site-packages/Flask_WTF-1.1.1.dist-info/LICENSE.rst
Normal file
28
venv/Lib/site-packages/Flask_WTF-1.1.1.dist-info/LICENSE.rst
Normal file
@@ -0,0 +1,28 @@
|
||||
Copyright 2010 WTForms
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
||||
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
50
venv/Lib/site-packages/Flask_WTF-1.1.1.dist-info/METADATA
Normal file
50
venv/Lib/site-packages/Flask_WTF-1.1.1.dist-info/METADATA
Normal file
@@ -0,0 +1,50 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: Flask-WTF
|
||||
Version: 1.1.1
|
||||
Summary: Form rendering, validation, and CSRF protection for Flask with WTForms.
|
||||
Home-page: https://github.com/wtforms/flask-wtf/
|
||||
Author: Dan Jacob
|
||||
Author-email: danjac354@gmail.com
|
||||
Maintainer: Hsiaoming Yang
|
||||
Maintainer-email: me@lepture.com
|
||||
License: BSD-3-Clause
|
||||
Project-URL: Documentation, https://flask-wtf.readthedocs.io/
|
||||
Project-URL: Changes, https://flask-wtf.readthedocs.io/changes/
|
||||
Project-URL: Source Code, https://github.com/wtforms/flask-wtf/
|
||||
Project-URL: Issue Tracker, https://github.com/wtforms/flask-wtf/issues/
|
||||
Project-URL: Chat, https://discord.gg/pallets
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Environment :: Web Environment
|
||||
Classifier: Framework :: Flask
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: BSD License
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
||||
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI
|
||||
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
|
||||
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
||||
Requires-Python: >=3.7
|
||||
Description-Content-Type: text/x-rst
|
||||
License-File: LICENSE.rst
|
||||
Requires-Dist: Flask
|
||||
Requires-Dist: WTForms
|
||||
Requires-Dist: itsdangerous
|
||||
Provides-Extra: email
|
||||
Requires-Dist: email-validator ; extra == 'email'
|
||||
|
||||
Flask-WTF
|
||||
=========
|
||||
|
||||
Simple integration of Flask and WTForms, including CSRF, file upload,
|
||||
and reCAPTCHA.
|
||||
|
||||
Links
|
||||
-----
|
||||
|
||||
- Documentation: https://flask-wtf.readthedocs.io/
|
||||
- Changes: https://flask-wtf.readthedocs.io/changes/
|
||||
- PyPI Releases: https://pypi.org/project/Flask-WTF/
|
||||
- Source Code: https://github.com/wtforms/flask-wtf/
|
||||
- Issue Tracker: https://github.com/wtforms/flask-wtf/issues/
|
||||
- Chat: https://discord.gg/pallets
|
||||
27
venv/Lib/site-packages/Flask_WTF-1.1.1.dist-info/RECORD
Normal file
27
venv/Lib/site-packages/Flask_WTF-1.1.1.dist-info/RECORD
Normal file
@@ -0,0 +1,27 @@
|
||||
Flask_WTF-1.1.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
Flask_WTF-1.1.1.dist-info/LICENSE.rst,sha256=1fGQNkUVeMs27u8EyZ6_fXyi5w3PBDY2UZvEIOFafGI,1475
|
||||
Flask_WTF-1.1.1.dist-info/METADATA,sha256=YR-t2rpU1ZnLGjB4H_LEm3ns3EPcs8VbAxASuoaWrgE,1868
|
||||
Flask_WTF-1.1.1.dist-info/RECORD,,
|
||||
Flask_WTF-1.1.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
Flask_WTF-1.1.1.dist-info/WHEEL,sha256=2wepM1nk4DS4eFpYrW1TTqPcoGNfHhhO_i5m4cOimbo,92
|
||||
Flask_WTF-1.1.1.dist-info/top_level.txt,sha256=zK3flQPSjYTkAMjB0V6Jhu3jyotC0biL1mMhzitYoog,10
|
||||
flask_wtf/__init__.py,sha256=9N5z_8Nkzsla9cgqGKxlLmkgdHGuU3UI49_O2M1odr8,214
|
||||
flask_wtf/__pycache__/__init__.cpython-310.pyc,,
|
||||
flask_wtf/__pycache__/_compat.cpython-310.pyc,,
|
||||
flask_wtf/__pycache__/csrf.cpython-310.pyc,,
|
||||
flask_wtf/__pycache__/file.cpython-310.pyc,,
|
||||
flask_wtf/__pycache__/form.cpython-310.pyc,,
|
||||
flask_wtf/__pycache__/i18n.cpython-310.pyc,,
|
||||
flask_wtf/_compat.py,sha256=N3sqC9yzFWY-3MZ7QazX1sidvkO3d5yy4NR6lkp0s94,248
|
||||
flask_wtf/csrf.py,sha256=Z407bCLwNpqjmdh6vK162hG1dHxdrZ2kly4n-Hrbyhs,10156
|
||||
flask_wtf/file.py,sha256=SKm-Tjk9mYrP94cMnIdEOab1vvQEjfKZ1PwPzXNhH6o,3644
|
||||
flask_wtf/form.py,sha256=TmR7xCrxin2LHp6thn7fq1OeU8aLB7xsZzvv52nH7Ss,4049
|
||||
flask_wtf/i18n.py,sha256=TyO8gqt9DocHMSaNhj0KKgxoUrPYs-G1nVW-jns0SOw,1166
|
||||
flask_wtf/recaptcha/__init__.py,sha256=m4eNGoU3Q0Wnt_wP8VvOlA0mwWuoMtAcK9pYT7sPFp8,106
|
||||
flask_wtf/recaptcha/__pycache__/__init__.cpython-310.pyc,,
|
||||
flask_wtf/recaptcha/__pycache__/fields.cpython-310.pyc,,
|
||||
flask_wtf/recaptcha/__pycache__/validators.cpython-310.pyc,,
|
||||
flask_wtf/recaptcha/__pycache__/widgets.cpython-310.pyc,,
|
||||
flask_wtf/recaptcha/fields.py,sha256=M1-RFuUKOsJAzsLm3xaaxuhX2bB9oRqS-HVSN-NpkmI,433
|
||||
flask_wtf/recaptcha/validators.py,sha256=K4e_pvPoq0JBcSFXEB2XRzWbvi9LZef3ioNbS2jdNgU,2437
|
||||
flask_wtf/recaptcha/widgets.py,sha256=OWSFCZDWaLBLkNJvzyqcIbRQVBD5tUyEOijfTv0Dpjo,1503
|
||||
5
venv/Lib/site-packages/Flask_WTF-1.1.1.dist-info/WHEEL
Normal file
5
venv/Lib/site-packages/Flask_WTF-1.1.1.dist-info/WHEEL
Normal file
@@ -0,0 +1,5 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: bdist_wheel (0.38.4)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
flask_wtf
|
||||
@@ -0,0 +1 @@
|
||||
pip
|
||||
@@ -0,0 +1,28 @@
|
||||
Copyright 2010 Pallets
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
||||
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
92
venv/Lib/site-packages/MarkupSafe-3.0.2.dist-info/METADATA
Normal file
92
venv/Lib/site-packages/MarkupSafe-3.0.2.dist-info/METADATA
Normal file
@@ -0,0 +1,92 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: MarkupSafe
|
||||
Version: 3.0.2
|
||||
Summary: Safely add untrusted strings to HTML/XML markup.
|
||||
Maintainer-email: Pallets <contact@palletsprojects.com>
|
||||
License: Copyright 2010 Pallets
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
||||
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Project-URL: Donate, https://palletsprojects.com/donate
|
||||
Project-URL: Documentation, https://markupsafe.palletsprojects.com/
|
||||
Project-URL: Changes, https://markupsafe.palletsprojects.com/changes/
|
||||
Project-URL: Source, https://github.com/pallets/markupsafe/
|
||||
Project-URL: Chat, https://discord.gg/pallets
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Environment :: Web Environment
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: BSD License
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
||||
Classifier: Topic :: Text Processing :: Markup :: HTML
|
||||
Classifier: Typing :: Typed
|
||||
Requires-Python: >=3.9
|
||||
Description-Content-Type: text/markdown
|
||||
License-File: LICENSE.txt
|
||||
|
||||
# MarkupSafe
|
||||
|
||||
MarkupSafe implements a text object that escapes characters so it is
|
||||
safe to use in HTML and XML. Characters that have special meanings are
|
||||
replaced so that they display as the actual characters. This mitigates
|
||||
injection attacks, meaning untrusted user input can safely be displayed
|
||||
on a page.
|
||||
|
||||
|
||||
## Examples
|
||||
|
||||
```pycon
|
||||
>>> from markupsafe import Markup, escape
|
||||
|
||||
>>> # escape replaces special characters and wraps in Markup
|
||||
>>> escape("<script>alert(document.cookie);</script>")
|
||||
Markup('<script>alert(document.cookie);</script>')
|
||||
|
||||
>>> # wrap in Markup to mark text "safe" and prevent escaping
|
||||
>>> Markup("<strong>Hello</strong>")
|
||||
Markup('<strong>hello</strong>')
|
||||
|
||||
>>> escape(Markup("<strong>Hello</strong>"))
|
||||
Markup('<strong>hello</strong>')
|
||||
|
||||
>>> # Markup is a str subclass
|
||||
>>> # methods and operators escape their arguments
|
||||
>>> template = Markup("Hello <em>{name}</em>")
|
||||
>>> template.format(name='"World"')
|
||||
Markup('Hello <em>"World"</em>')
|
||||
```
|
||||
|
||||
## Donate
|
||||
|
||||
The Pallets organization develops and supports MarkupSafe and other
|
||||
popular packages. In order to grow the community of contributors and
|
||||
users, and allow the maintainers to devote more time to the projects,
|
||||
[please donate today][].
|
||||
|
||||
[please donate today]: https://palletsprojects.com/donate
|
||||
14
venv/Lib/site-packages/MarkupSafe-3.0.2.dist-info/RECORD
Normal file
14
venv/Lib/site-packages/MarkupSafe-3.0.2.dist-info/RECORD
Normal file
@@ -0,0 +1,14 @@
|
||||
MarkupSafe-3.0.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
MarkupSafe-3.0.2.dist-info/LICENSE.txt,sha256=RjHsDbX9kKVH4zaBcmTGeYIUM4FG-KyUtKV_lu6MnsQ,1503
|
||||
MarkupSafe-3.0.2.dist-info/METADATA,sha256=nhoabjupBG41j_JxPCJ3ylgrZ6Fx8oMCFbiLF9Kafqc,4067
|
||||
MarkupSafe-3.0.2.dist-info/RECORD,,
|
||||
MarkupSafe-3.0.2.dist-info/WHEEL,sha256=IqiWNwTSPPvorR7mTezuRY2eqj__44JKKkjOiewDX64,101
|
||||
MarkupSafe-3.0.2.dist-info/top_level.txt,sha256=qy0Plje5IJuvsCBjejJyhDCjEAdcDLK_2agVcex8Z6U,11
|
||||
markupsafe/__init__.py,sha256=pREerPwvinB62tNCMOwqxBS2YHV6R52Wcq1d-rB4Z5o,13609
|
||||
markupsafe/__pycache__/__init__.cpython-310.pyc,,
|
||||
markupsafe/__pycache__/_native.cpython-310.pyc,,
|
||||
markupsafe/_native.py,sha256=2ptkJ40yCcp9kq3L1NqpgjfpZB-obniYKFFKUOkHh4Q,218
|
||||
markupsafe/_speedups.c,sha256=SglUjn40ti9YgQAO--OgkSyv9tXq9vvaHyVhQows4Ok,4353
|
||||
markupsafe/_speedups.cp310-win_amd64.pyd,sha256=RTvh-UzJTX7J_4j-A5jZmnqwRKBe0pQiDPd_j60jft8,13312
|
||||
markupsafe/_speedups.pyi,sha256=LSDmXYOefH4HVpAXuL8sl7AttLw0oXh1njVoVZp2wqQ,42
|
||||
markupsafe/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
5
venv/Lib/site-packages/MarkupSafe-3.0.2.dist-info/WHEEL
Normal file
5
venv/Lib/site-packages/MarkupSafe-3.0.2.dist-info/WHEEL
Normal file
@@ -0,0 +1,5 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: setuptools (75.2.0)
|
||||
Root-Is-Purelib: false
|
||||
Tag: cp310-cp310-win_amd64
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
markupsafe
|
||||
@@ -0,0 +1 @@
|
||||
pip
|
||||
19
venv/Lib/site-packages/SQLAlchemy-2.0.20.dist-info/LICENSE
Normal file
19
venv/Lib/site-packages/SQLAlchemy-2.0.20.dist-info/LICENSE
Normal file
@@ -0,0 +1,19 @@
|
||||
Copyright 2005-2023 SQLAlchemy authors and contributors <see AUTHORS file>.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
238
venv/Lib/site-packages/SQLAlchemy-2.0.20.dist-info/METADATA
Normal file
238
venv/Lib/site-packages/SQLAlchemy-2.0.20.dist-info/METADATA
Normal file
@@ -0,0 +1,238 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: SQLAlchemy
|
||||
Version: 2.0.20
|
||||
Summary: Database Abstraction Library
|
||||
Home-page: https://www.sqlalchemy.org
|
||||
Author: Mike Bayer
|
||||
Author-email: mike_mp@zzzcomputing.com
|
||||
License: MIT
|
||||
Project-URL: Documentation, https://docs.sqlalchemy.org
|
||||
Project-URL: Issue Tracker, https://github.com/sqlalchemy/sqlalchemy/
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3.7
|
||||
Classifier: Programming Language :: Python :: 3.8
|
||||
Classifier: Programming Language :: Python :: 3.9
|
||||
Classifier: Programming Language :: Python :: 3.10
|
||||
Classifier: Programming Language :: Python :: 3.11
|
||||
Classifier: Programming Language :: Python :: Implementation :: CPython
|
||||
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
||||
Classifier: Topic :: Database :: Front-Ends
|
||||
Requires-Python: >=3.7
|
||||
Description-Content-Type: text/x-rst
|
||||
License-File: LICENSE
|
||||
Requires-Dist: typing-extensions >=4.2.0
|
||||
Requires-Dist: greenlet !=0.4.17 ; platform_machine == "aarch64" or (platform_machine == "ppc64le" or (platform_machine == "x86_64" or (platform_machine == "amd64" or (platform_machine == "AMD64" or (platform_machine == "win32" or platform_machine == "WIN32")))))
|
||||
Requires-Dist: importlib-metadata ; python_version < "3.8"
|
||||
Provides-Extra: aiomysql
|
||||
Requires-Dist: greenlet !=0.4.17 ; extra == 'aiomysql'
|
||||
Requires-Dist: aiomysql >=0.2.0 ; extra == 'aiomysql'
|
||||
Provides-Extra: aiosqlite
|
||||
Requires-Dist: greenlet !=0.4.17 ; extra == 'aiosqlite'
|
||||
Requires-Dist: aiosqlite ; extra == 'aiosqlite'
|
||||
Requires-Dist: typing-extensions !=3.10.0.1 ; extra == 'aiosqlite'
|
||||
Provides-Extra: asyncio
|
||||
Requires-Dist: greenlet !=0.4.17 ; extra == 'asyncio'
|
||||
Provides-Extra: asyncmy
|
||||
Requires-Dist: greenlet !=0.4.17 ; extra == 'asyncmy'
|
||||
Requires-Dist: asyncmy !=0.2.4,!=0.2.6,>=0.2.3 ; extra == 'asyncmy'
|
||||
Provides-Extra: mariadb_connector
|
||||
Requires-Dist: mariadb !=1.1.2,!=1.1.5,>=1.0.1 ; extra == 'mariadb_connector'
|
||||
Provides-Extra: mssql
|
||||
Requires-Dist: pyodbc ; extra == 'mssql'
|
||||
Provides-Extra: mssql_pymssql
|
||||
Requires-Dist: pymssql ; extra == 'mssql_pymssql'
|
||||
Provides-Extra: mssql_pyodbc
|
||||
Requires-Dist: pyodbc ; extra == 'mssql_pyodbc'
|
||||
Provides-Extra: mypy
|
||||
Requires-Dist: mypy >=0.910 ; extra == 'mypy'
|
||||
Provides-Extra: mysql
|
||||
Requires-Dist: mysqlclient >=1.4.0 ; extra == 'mysql'
|
||||
Provides-Extra: mysql_connector
|
||||
Requires-Dist: mysql-connector-python ; extra == 'mysql_connector'
|
||||
Provides-Extra: oracle
|
||||
Requires-Dist: cx-oracle >=7 ; extra == 'oracle'
|
||||
Provides-Extra: oracle_oracledb
|
||||
Requires-Dist: oracledb >=1.0.1 ; extra == 'oracle_oracledb'
|
||||
Provides-Extra: postgresql
|
||||
Requires-Dist: psycopg2 >=2.7 ; extra == 'postgresql'
|
||||
Provides-Extra: postgresql_asyncpg
|
||||
Requires-Dist: greenlet !=0.4.17 ; extra == 'postgresql_asyncpg'
|
||||
Requires-Dist: asyncpg ; extra == 'postgresql_asyncpg'
|
||||
Provides-Extra: postgresql_pg8000
|
||||
Requires-Dist: pg8000 >=1.29.1 ; extra == 'postgresql_pg8000'
|
||||
Provides-Extra: postgresql_psycopg
|
||||
Requires-Dist: psycopg >=3.0.7 ; extra == 'postgresql_psycopg'
|
||||
Provides-Extra: postgresql_psycopg2binary
|
||||
Requires-Dist: psycopg2-binary ; extra == 'postgresql_psycopg2binary'
|
||||
Provides-Extra: postgresql_psycopg2cffi
|
||||
Requires-Dist: psycopg2cffi ; extra == 'postgresql_psycopg2cffi'
|
||||
Provides-Extra: postgresql_psycopgbinary
|
||||
Requires-Dist: psycopg[binary] >=3.0.7 ; extra == 'postgresql_psycopgbinary'
|
||||
Provides-Extra: pymysql
|
||||
Requires-Dist: pymysql ; extra == 'pymysql'
|
||||
Provides-Extra: sqlcipher
|
||||
Requires-Dist: sqlcipher3-binary ; extra == 'sqlcipher'
|
||||
|
||||
SQLAlchemy
|
||||
==========
|
||||
|
||||
|PyPI| |Python| |Downloads|
|
||||
|
||||
.. |PyPI| image:: https://img.shields.io/pypi/v/sqlalchemy
|
||||
:target: https://pypi.org/project/sqlalchemy
|
||||
:alt: PyPI
|
||||
|
||||
.. |Python| image:: https://img.shields.io/pypi/pyversions/sqlalchemy
|
||||
:target: https://pypi.org/project/sqlalchemy
|
||||
:alt: PyPI - Python Version
|
||||
|
||||
.. |Downloads| image:: https://static.pepy.tech/badge/sqlalchemy/month
|
||||
:target: https://pepy.tech/project/sqlalchemy
|
||||
:alt: PyPI - Downloads
|
||||
|
||||
|
||||
The Python SQL Toolkit and Object Relational Mapper
|
||||
|
||||
Introduction
|
||||
-------------
|
||||
|
||||
SQLAlchemy is the Python SQL toolkit and Object Relational Mapper
|
||||
that gives application developers the full power and
|
||||
flexibility of SQL. SQLAlchemy provides a full suite
|
||||
of well known enterprise-level persistence patterns,
|
||||
designed for efficient and high-performing database
|
||||
access, adapted into a simple and Pythonic domain
|
||||
language.
|
||||
|
||||
Major SQLAlchemy features include:
|
||||
|
||||
* An industrial strength ORM, built
|
||||
from the core on the identity map, unit of work,
|
||||
and data mapper patterns. These patterns
|
||||
allow transparent persistence of objects
|
||||
using a declarative configuration system.
|
||||
Domain models
|
||||
can be constructed and manipulated naturally,
|
||||
and changes are synchronized with the
|
||||
current transaction automatically.
|
||||
* A relationally-oriented query system, exposing
|
||||
the full range of SQL's capabilities
|
||||
explicitly, including joins, subqueries,
|
||||
correlation, and most everything else,
|
||||
in terms of the object model.
|
||||
Writing queries with the ORM uses the same
|
||||
techniques of relational composition you use
|
||||
when writing SQL. While you can drop into
|
||||
literal SQL at any time, it's virtually never
|
||||
needed.
|
||||
* A comprehensive and flexible system
|
||||
of eager loading for related collections and objects.
|
||||
Collections are cached within a session,
|
||||
and can be loaded on individual access, all
|
||||
at once using joins, or by query per collection
|
||||
across the full result set.
|
||||
* A Core SQL construction system and DBAPI
|
||||
interaction layer. The SQLAlchemy Core is
|
||||
separate from the ORM and is a full database
|
||||
abstraction layer in its own right, and includes
|
||||
an extensible Python-based SQL expression
|
||||
language, schema metadata, connection pooling,
|
||||
type coercion, and custom types.
|
||||
* All primary and foreign key constraints are
|
||||
assumed to be composite and natural. Surrogate
|
||||
integer primary keys are of course still the
|
||||
norm, but SQLAlchemy never assumes or hardcodes
|
||||
to this model.
|
||||
* Database introspection and generation. Database
|
||||
schemas can be "reflected" in one step into
|
||||
Python structures representing database metadata;
|
||||
those same structures can then generate
|
||||
CREATE statements right back out - all within
|
||||
the Core, independent of the ORM.
|
||||
|
||||
SQLAlchemy's philosophy:
|
||||
|
||||
* SQL databases behave less and less like object
|
||||
collections the more size and performance start to
|
||||
matter; object collections behave less and less like
|
||||
tables and rows the more abstraction starts to matter.
|
||||
SQLAlchemy aims to accommodate both of these
|
||||
principles.
|
||||
* An ORM doesn't need to hide the "R". A relational
|
||||
database provides rich, set-based functionality
|
||||
that should be fully exposed. SQLAlchemy's
|
||||
ORM provides an open-ended set of patterns
|
||||
that allow a developer to construct a custom
|
||||
mediation layer between a domain model and
|
||||
a relational schema, turning the so-called
|
||||
"object relational impedance" issue into
|
||||
a distant memory.
|
||||
* The developer, in all cases, makes all decisions
|
||||
regarding the design, structure, and naming conventions
|
||||
of both the object model as well as the relational
|
||||
schema. SQLAlchemy only provides the means
|
||||
to automate the execution of these decisions.
|
||||
* With SQLAlchemy, there's no such thing as
|
||||
"the ORM generated a bad query" - you
|
||||
retain full control over the structure of
|
||||
queries, including how joins are organized,
|
||||
how subqueries and correlation is used, what
|
||||
columns are requested. Everything SQLAlchemy
|
||||
does is ultimately the result of a developer-initiated
|
||||
decision.
|
||||
* Don't use an ORM if the problem doesn't need one.
|
||||
SQLAlchemy consists of a Core and separate ORM
|
||||
component. The Core offers a full SQL expression
|
||||
language that allows Pythonic construction
|
||||
of SQL constructs that render directly to SQL
|
||||
strings for a target database, returning
|
||||
result sets that are essentially enhanced DBAPI
|
||||
cursors.
|
||||
* Transactions should be the norm. With SQLAlchemy's
|
||||
ORM, nothing goes to permanent storage until
|
||||
commit() is called. SQLAlchemy encourages applications
|
||||
to create a consistent means of delineating
|
||||
the start and end of a series of operations.
|
||||
* Never render a literal value in a SQL statement.
|
||||
Bound parameters are used to the greatest degree
|
||||
possible, allowing query optimizers to cache
|
||||
query plans effectively and making SQL injection
|
||||
attacks a non-issue.
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
Latest documentation is at:
|
||||
|
||||
https://www.sqlalchemy.org/docs/
|
||||
|
||||
Installation / Requirements
|
||||
---------------------------
|
||||
|
||||
Full documentation for installation is at
|
||||
`Installation <https://www.sqlalchemy.org/docs/intro.html#installation>`_.
|
||||
|
||||
Getting Help / Development / Bug reporting
|
||||
------------------------------------------
|
||||
|
||||
Please refer to the `SQLAlchemy Community Guide <https://www.sqlalchemy.org/support.html>`_.
|
||||
|
||||
Code of Conduct
|
||||
---------------
|
||||
|
||||
Above all, SQLAlchemy places great emphasis on polite, thoughtful, and
|
||||
constructive communication between users and developers.
|
||||
Please see our current Code of Conduct at
|
||||
`Code of Conduct <https://www.sqlalchemy.org/codeofconduct.html>`_.
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
SQLAlchemy is distributed under the `MIT license
|
||||
<https://www.opensource.org/licenses/mit-license.php>`_.
|
||||
|
||||
524
venv/Lib/site-packages/SQLAlchemy-2.0.20.dist-info/RECORD
Normal file
524
venv/Lib/site-packages/SQLAlchemy-2.0.20.dist-info/RECORD
Normal file
@@ -0,0 +1,524 @@
|
||||
SQLAlchemy-2.0.20.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
SQLAlchemy-2.0.20.dist-info/LICENSE,sha256=ZbcQGZNtpoLy8YjvH-nyoobTdOwtEgtXopPVzxy6pCo,1119
|
||||
SQLAlchemy-2.0.20.dist-info/METADATA,sha256=nEFiWWNkJBol1Lb751ILyjQAvlSImwjdLuqoWTJ621A,9667
|
||||
SQLAlchemy-2.0.20.dist-info/RECORD,,
|
||||
SQLAlchemy-2.0.20.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
SQLAlchemy-2.0.20.dist-info/WHEEL,sha256=Wb1el1iP4ORW7FiLElw7HfxLpDiHzwvd2B382b2Idl0,102
|
||||
SQLAlchemy-2.0.20.dist-info/top_level.txt,sha256=rp-ZgB7D8G11ivXON5VGPjupT1voYmWqkciDt5Uaw_Q,11
|
||||
sqlalchemy/__init__.py,sha256=mnL2g2e81Pw9K-IxeLNmqYYpQ6ZMc4_0dOUVB3lpAyk,12993
|
||||
sqlalchemy/__pycache__/__init__.cpython-310.pyc,,
|
||||
sqlalchemy/__pycache__/events.cpython-310.pyc,,
|
||||
sqlalchemy/__pycache__/exc.cpython-310.pyc,,
|
||||
sqlalchemy/__pycache__/inspection.cpython-310.pyc,,
|
||||
sqlalchemy/__pycache__/log.cpython-310.pyc,,
|
||||
sqlalchemy/__pycache__/schema.cpython-310.pyc,,
|
||||
sqlalchemy/__pycache__/types.cpython-310.pyc,,
|
||||
sqlalchemy/connectors/__init__.py,sha256=sjPX1Mb2nWgRHLZY0mF350mGiqpi2CYBs2A1b8dh_wE,494
|
||||
sqlalchemy/connectors/__pycache__/__init__.cpython-310.pyc,,
|
||||
sqlalchemy/connectors/__pycache__/pyodbc.cpython-310.pyc,,
|
||||
sqlalchemy/connectors/pyodbc.py,sha256=GFZ_OqBAwQpEprwEmVVCm0imUEqw36PGv1YFOsH2Lag,8730
|
||||
sqlalchemy/cyextension/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
sqlalchemy/cyextension/__pycache__/__init__.cpython-310.pyc,,
|
||||
sqlalchemy/cyextension/collections.cp310-win_amd64.pyd,sha256=7-Q6dQbt5KmaO6gtS17XhtPGx0yNiDFixx2G_Ovh9_g,173568
|
||||
sqlalchemy/cyextension/collections.pyx,sha256=UY81HxvMAD4MOFR52SjzUbLHCGGcHZDSfK6gw6AYB8A,12726
|
||||
sqlalchemy/cyextension/immutabledict.cp310-win_amd64.pyd,sha256=BLlAQZ1KNSPut7J14edvsMXDkbwSaGuNGei24P5ZfNM,72192
|
||||
sqlalchemy/cyextension/immutabledict.pxd,sha256=JsNJYZIekkbtQQ2Tz6Bn1bO1g07yXztY9bb3rvH1e0Y,43
|
||||
sqlalchemy/cyextension/immutabledict.pyx,sha256=VmhtF8aDXjEVVdA80LRY1iP85lNMwcz7vB6hZkAOGB0,3412
|
||||
sqlalchemy/cyextension/processors.cp310-win_amd64.pyd,sha256=8IJwIPDE7K7JKHp5FshSv4X-w9fv4HMO9o8nPTJqrK0,58880
|
||||
sqlalchemy/cyextension/processors.pyx,sha256=ZXuoi-hPRI9pVSbp6QbfJwy6S5kVCUZ8qj_h5-NvAFA,1607
|
||||
sqlalchemy/cyextension/resultproxy.cp310-win_amd64.pyd,sha256=3HcHkSMTU4dksuzq0v2MD7Q_BJ-c9KidQIlTwyBO28E,60416
|
||||
sqlalchemy/cyextension/resultproxy.pyx,sha256=qlk8eBpFo3UYbwQChdIWa3RqWXczuUL8ahulcLCL1bI,2573
|
||||
sqlalchemy/cyextension/util.cp310-win_amd64.pyd,sha256=5PWlHJDSaHyempijzTE4GTrYKAGOqsQOZzZjoCz1fSg,72704
|
||||
sqlalchemy/cyextension/util.pyx,sha256=H2FEg9uAAWO9UcNFyrfVuPhOznTq3h9UdjfmJ2BRD1Y,2374
|
||||
sqlalchemy/dialects/__init__.py,sha256=vUDqtIsKolzjds0KK763SAnVCCF1SGQ64zY4WNIxbwM,1847
|
||||
sqlalchemy/dialects/__pycache__/__init__.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/__pycache__/_typing.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/_typing.py,sha256=hA9jNttJjmVWDHKybSMSFWIlmRv2r5kuQPp8IeViifY,667
|
||||
sqlalchemy/dialects/mssql/__init__.py,sha256=dvJCLhXDMkDvtAXT_26kBllhR7-geM9nrZOUxr2IG6Q,1928
|
||||
sqlalchemy/dialects/mssql/__pycache__/__init__.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/mssql/__pycache__/base.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/mssql/__pycache__/information_schema.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/mssql/__pycache__/json.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/mssql/__pycache__/provision.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/mssql/__pycache__/pymssql.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/mssql/__pycache__/pyodbc.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/mssql/base.py,sha256=D8kqfvgGhZX98rfPlpluJmJBO72wx9QM3oirzj2CqO8,137310
|
||||
sqlalchemy/dialects/mssql/information_schema.py,sha256=ufbEeGAFsciot3MmKPmFFhW95r57F_ORHULBS_UuUSw,8320
|
||||
sqlalchemy/dialects/mssql/json.py,sha256=VOrBSxJWh7Fj-zIBA5aYZwx37DJq1OrWpJqc0xtWPhQ,4700
|
||||
sqlalchemy/dialects/mssql/provision.py,sha256=hlKU-pYiCAMlLtEOjEhPr7ESgvQ4iLCAywtDXNQqZus,5142
|
||||
sqlalchemy/dialects/mssql/pymssql.py,sha256=yA5NnGBs0YSzzjnGlqMrtHKdo4XHyJ6zKcySOvAw2ZA,4154
|
||||
sqlalchemy/dialects/mssql/pyodbc.py,sha256=y4yayQokx8NWPvmod_JVI8BshoNpohfhclLxQlEhTIE,27444
|
||||
sqlalchemy/dialects/mysql/__init__.py,sha256=060B9NtuQqUb8vNXDm8bdOGUUi6SUi_757-19DDhOQM,2245
|
||||
sqlalchemy/dialects/mysql/__pycache__/__init__.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/aiomysql.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/asyncmy.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/base.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/cymysql.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/dml.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/enumerated.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/expression.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/json.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/mariadb.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/mariadbconnector.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/mysqlconnector.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/mysqldb.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/provision.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/pymysql.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/pyodbc.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/reflection.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/reserved_words.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/types.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/mysql/aiomysql.py,sha256=xXvFyEjL613scXbU490iwK4fTegDGDojMUO565qo0iE,9831
|
||||
sqlalchemy/dialects/mysql/asyncmy.py,sha256=Ilpd6rTcXFr40iepVerDuZmni8db4tGLiK2j0B9pnII,9966
|
||||
sqlalchemy/dialects/mysql/base.py,sha256=JSYsIrsTQABPCE78llB7bYJyecJsAUlHkLBkPq4gUKw,122488
|
||||
sqlalchemy/dialects/mysql/cymysql.py,sha256=RdwzBclxwN3uXWTT34YySR0rQfTjVzZDSadhzlOqhag,2375
|
||||
sqlalchemy/dialects/mysql/dml.py,sha256=dDUvRalIG5l6_Iawkj0n-6p0NRfYfdP1wRl121qOYek,7855
|
||||
sqlalchemy/dialects/mysql/enumerated.py,sha256=whCwVR5DmKh455d4EVg2XHItfvLtuzxA5bWOWzK6Cnw,8683
|
||||
sqlalchemy/dialects/mysql/expression.py,sha256=-RmnmFCjWn16L_Nn82541wr6I3bUvvMk_6L7WhmeAK4,4206
|
||||
sqlalchemy/dialects/mysql/json.py,sha256=hZr1MD4W6BaItKT5LRStDRQbr7wcer4YdWbkh47-RTA,2341
|
||||
sqlalchemy/dialects/mysql/mariadb.py,sha256=SdFqsWMjIFwoC0bhlqksN4ju0Mnq3-iUDCKq7_idWk0,876
|
||||
sqlalchemy/dialects/mysql/mariadbconnector.py,sha256=yajW-43yKl94IxjCCFTzJ1Amr5RRDq0wkvBm_9IGBXU,7703
|
||||
sqlalchemy/dialects/mysql/mysqlconnector.py,sha256=8a2BZ_ChVR5HvTzonq0DtYoInD3iV4ihbA_9EbgtUxY,5845
|
||||
sqlalchemy/dialects/mysql/mysqldb.py,sha256=4ZLfGACIMD5icglEqx6jTj3W4adT_ANCV1xo5kvAHYw,9962
|
||||
sqlalchemy/dialects/mysql/provision.py,sha256=jdtfrsATv7hoMcepkMxHVG5QV2YkA92bg01CnOR9VMs,3327
|
||||
sqlalchemy/dialects/mysql/pymysql.py,sha256=f09IxnUy9HFCEnNrW-RqxvByVUREYWRpbHsa1yIPDGk,3045
|
||||
sqlalchemy/dialects/mysql/pyodbc.py,sha256=dfz0mekJDsOIvjs5utBRNltUr9YyhYuUH1rsUPb4NjI,4426
|
||||
sqlalchemy/dialects/mysql/reflection.py,sha256=4-lXatfmSjcmv7YocofW3WUvLeQV8f0qrn73H40evpg,23198
|
||||
sqlalchemy/dialects/mysql/reserved_words.py,sha256=KOR71_hBCzivGutG54Yq_K5t7dT6lgRBey0TcztWI3I,9712
|
||||
sqlalchemy/dialects/mysql/types.py,sha256=vOi0kn2OLaWTjPKTNz5xPcS-jiHRLv_l1no_oA_jdYQ,25031
|
||||
sqlalchemy/dialects/oracle/__init__.py,sha256=IghimaBtnKRrtkYdO5eFjJ5OUUGPJpYSZZJX0DIJb38,1368
|
||||
sqlalchemy/dialects/oracle/__pycache__/__init__.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/oracle/__pycache__/base.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/oracle/__pycache__/cx_oracle.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/oracle/__pycache__/dictionary.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/oracle/__pycache__/oracledb.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/oracle/__pycache__/provision.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/oracle/__pycache__/types.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/oracle/base.py,sha256=Ouc583EpWEzQ25yOTeaNCXNgvwM78PDSxoP6ETT82Zw,121073
|
||||
sqlalchemy/dialects/oracle/cx_oracle.py,sha256=8HYHDRepG0X2sm2F_8VObomiaoRQzf8lnhlKNktruu8,56585
|
||||
sqlalchemy/dialects/oracle/dictionary.py,sha256=yRmt5b218G1Q5pZR5kF1ocsusT4XAgl3v_t9WhKqlko,19993
|
||||
sqlalchemy/dialects/oracle/oracledb.py,sha256=ZweQdl0ZKR3jaXowa14JBlWg1KXcabm_FWhOR4vqPsk,3566
|
||||
sqlalchemy/dialects/oracle/provision.py,sha256=i4Ja1rCJLs0jIcpzt0PfcANHHoDOSWEpxqBvWYAdOcs,8269
|
||||
sqlalchemy/dialects/oracle/types.py,sha256=vF5neW-vxJcl9nLL9x74zn05gmU-_wkCTNmF0diqqaw,7738
|
||||
sqlalchemy/dialects/postgresql/__init__.py,sha256=kx5Iwob5j2_gYXMVF2qM6qIH5DCXxw1UYXVjuCgpvys,3897
|
||||
sqlalchemy/dialects/postgresql/__pycache__/__init__.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/_psycopg_common.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/array.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/asyncpg.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/base.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/dml.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/ext.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/hstore.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/json.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/named_types.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/operators.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/pg8000.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/pg_catalog.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/provision.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/psycopg.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/psycopg2.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/psycopg2cffi.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/ranges.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/types.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/postgresql/_psycopg_common.py,sha256=_DTODxy9UrjvqDk20-ZjEUAYhrzRFiosaH4GHp4mnNM,5841
|
||||
sqlalchemy/dialects/postgresql/array.py,sha256=3C-g6RRWgB4Th_MV4UWpbxXo8UT2-GDRxmXowyyml5A,14108
|
||||
sqlalchemy/dialects/postgresql/asyncpg.py,sha256=ooMOPDZEF4AnOJW9Mqz4zBpkXCUzZQGJwpxQGYXgvyA,40591
|
||||
sqlalchemy/dialects/postgresql/base.py,sha256=3Zi_VZU801kJBsgrEzJC-GHG_rmCIp-v95S87uzBp48,180277
|
||||
sqlalchemy/dialects/postgresql/dml.py,sha256=jESBXHjBdT-xzHSVADNz-2ljH-4I-rjjGuhKzrkGJII,11513
|
||||
sqlalchemy/dialects/postgresql/ext.py,sha256=MdYxZshShFutBq657bDNyq68kzczovktkJeuGq-WqmM,16749
|
||||
sqlalchemy/dialects/postgresql/hstore.py,sha256=x9kAVfXLHYDQfqg6IHPUhr4dvFSsBPoMKoBhqyaF_Vo,11929
|
||||
sqlalchemy/dialects/postgresql/json.py,sha256=vrSiTBejjt54B9JSOixbh-oR8fg2OGIJLW2gOsmb0gI,11528
|
||||
sqlalchemy/dialects/postgresql/named_types.py,sha256=fsAflctGz5teUyM7h2s0Z2Na14Dtt4vEj0rzf2VtRwU,17588
|
||||
sqlalchemy/dialects/postgresql/operators.py,sha256=oe7NQRjOkJM46ISEnNIGxV4qOm2ANrqn3PLKHqj6bmY,2928
|
||||
sqlalchemy/dialects/postgresql/pg8000.py,sha256=F6P7YT5iXWq3sYDIyiP1Qxoq3PrE7zFaUnivwMdRJSQ,19284
|
||||
sqlalchemy/dialects/postgresql/pg_catalog.py,sha256=8imUP46LsmtnVozUZa2qEtDcDHwjrYg4c2tSJsbCpfo,9169
|
||||
sqlalchemy/dialects/postgresql/provision.py,sha256=_sD_42mkypvAwDxwT6xeCGnHD5EMRVubIN5eonZ8MsQ,5678
|
||||
sqlalchemy/dialects/postgresql/psycopg.py,sha256=vcqBE7Nr9mfSRGUeg9rSz94kfWDiCco_xabLu16qQyM,22984
|
||||
sqlalchemy/dialects/postgresql/psycopg2.py,sha256=z2oVzGAfKnEIvJqEiJjb1nrsb84NaMjcHw6cWPC1PyU,32479
|
||||
sqlalchemy/dialects/postgresql/psycopg2cffi.py,sha256=gITk63w4Gi4wXQxBW22a0VGVUg-oBMyfg_YgIHc5kww,1800
|
||||
sqlalchemy/dialects/postgresql/ranges.py,sha256=qrrFd9KWkaR83B69QhNQadnkQJe1u5hUf3nnCzQnf94,31206
|
||||
sqlalchemy/dialects/postgresql/types.py,sha256=HuXQ2XabYNmWRkClpHTXCaB8vbI6tzwQI7y1aNZKNEo,7240
|
||||
sqlalchemy/dialects/sqlite/__init__.py,sha256=CHJgBNgr7eufrgF-0l27xohu4N317u1IOy7Hyyrxx0o,1230
|
||||
sqlalchemy/dialects/sqlite/__pycache__/__init__.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/sqlite/__pycache__/aiosqlite.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/sqlite/__pycache__/base.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/sqlite/__pycache__/dml.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/sqlite/__pycache__/json.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/sqlite/__pycache__/provision.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/sqlite/__pycache__/pysqlcipher.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/sqlite/__pycache__/pysqlite.cpython-310.pyc,,
|
||||
sqlalchemy/dialects/sqlite/aiosqlite.py,sha256=PiO8heyB6GXeTC4lxsptX6eKQlKvyy0s9UGeBEAdxog,11258
|
||||
sqlalchemy/dialects/sqlite/base.py,sha256=ukLGX5LR8G6GlbmzG7bGDp8QmQ5-SzUzwAN9hTBWJl8,98978
|
||||
sqlalchemy/dialects/sqlite/dml.py,sha256=KRBENBnUuZrraGSMmg2ohgQgPPcgCMCmEoKkuhn6Yq8,8674
|
||||
sqlalchemy/dialects/sqlite/json.py,sha256=IZR_pBgC9sWLtP5SXm-o5FR6SScGLi4DEMGbLJzWN8E,2619
|
||||
sqlalchemy/dialects/sqlite/provision.py,sha256=2LNwUT3zftd3WeGBJ9UsVKtwXn38KEdDxwZaJ2WXNjM,5575
|
||||
sqlalchemy/dialects/sqlite/pysqlcipher.py,sha256=F8y3R0dILJcmqUzHotYF4tLUppe3PSU_C7Xdqm4YV0o,5502
|
||||
sqlalchemy/dialects/sqlite/pysqlite.py,sha256=fKhH8ZGMW5XK1F_H9mqz7ofmR4nrsGrGdAgNMPizAEI,28644
|
||||
sqlalchemy/dialects/type_migration_guidelines.txt,sha256=gyh3JCauAIFi_9XEfqm3vYv_jb2Eqcz2HjpmC9ZEPMM,8384
|
||||
sqlalchemy/engine/__init__.py,sha256=ZlB1LVIV2GjvwyvKm2W0qVYQf51g8AiQvTkHGb1C8A0,2880
|
||||
sqlalchemy/engine/__pycache__/__init__.cpython-310.pyc,,
|
||||
sqlalchemy/engine/__pycache__/_py_processors.cpython-310.pyc,,
|
||||
sqlalchemy/engine/__pycache__/_py_row.cpython-310.pyc,,
|
||||
sqlalchemy/engine/__pycache__/_py_util.cpython-310.pyc,,
|
||||
sqlalchemy/engine/__pycache__/base.cpython-310.pyc,,
|
||||
sqlalchemy/engine/__pycache__/characteristics.cpython-310.pyc,,
|
||||
sqlalchemy/engine/__pycache__/create.cpython-310.pyc,,
|
||||
sqlalchemy/engine/__pycache__/cursor.cpython-310.pyc,,
|
||||
sqlalchemy/engine/__pycache__/default.cpython-310.pyc,,
|
||||
sqlalchemy/engine/__pycache__/events.cpython-310.pyc,,
|
||||
sqlalchemy/engine/__pycache__/interfaces.cpython-310.pyc,,
|
||||
sqlalchemy/engine/__pycache__/mock.cpython-310.pyc,,
|
||||
sqlalchemy/engine/__pycache__/processors.cpython-310.pyc,,
|
||||
sqlalchemy/engine/__pycache__/reflection.cpython-310.pyc,,
|
||||
sqlalchemy/engine/__pycache__/result.cpython-310.pyc,,
|
||||
sqlalchemy/engine/__pycache__/row.cpython-310.pyc,,
|
||||
sqlalchemy/engine/__pycache__/strategies.cpython-310.pyc,,
|
||||
sqlalchemy/engine/__pycache__/url.cpython-310.pyc,,
|
||||
sqlalchemy/engine/__pycache__/util.cpython-310.pyc,,
|
||||
sqlalchemy/engine/_py_processors.py,sha256=XuNIr2kzSay7mAWy14Aq1q2H3ZcineWY-ERfy0yvWpw,3880
|
||||
sqlalchemy/engine/_py_row.py,sha256=WASkBfwldq9LApfe1ILn93amhSc0ReihKic2lLO0gxI,3671
|
||||
sqlalchemy/engine/_py_util.py,sha256=HB-18ta-qhMrA1oNIDeobt4TtyqLUTzbfRlkN6elRb0,2313
|
||||
sqlalchemy/engine/base.py,sha256=3jNhTGA_esLDFLZOIkYWOovsyu1_yfmV-HiqKI8DoaQ,125509
|
||||
sqlalchemy/engine/characteristics.py,sha256=P7JlS02X1DKRSpgqpQPwt2sFsatm1L1hVxdvvw3u95s,2419
|
||||
sqlalchemy/engine/create.py,sha256=B1K2S_kTzfXrlOcOaPxJlxmKJWo9pbCpyAnODlJOul8,33489
|
||||
sqlalchemy/engine/cursor.py,sha256=mNuHRp9SUS-_a_99y1Gj6Bx5jWnFbPf5mn0pa9EpjgQ,76545
|
||||
sqlalchemy/engine/default.py,sha256=C0ooBrV9Tv1rsJgR3shVB1416-iL1Lg8ZlZ_GV15RAs,86235
|
||||
sqlalchemy/engine/events.py,sha256=1ujDzJrUoanwgk5nlgldti-YukWleLS3wqmt4NFW68c,38375
|
||||
sqlalchemy/engine/interfaces.py,sha256=-h2-AdHja9NLAUU7vynDBLa7HxgN6SCRMvl1XmviuOg,116234
|
||||
sqlalchemy/engine/mock.py,sha256=y6-Magp0YKkuS0SsSihT8eYxDrt7aMgpY44rbhbxEDw,4326
|
||||
sqlalchemy/engine/processors.py,sha256=GvY0nS06PrGMwgwk4HHYX8QGvIUA0vEaNAmuov08BiY,2444
|
||||
sqlalchemy/engine/reflection.py,sha256=Ob-mKkHcKHtmxwIdYV3gjPHxaO_HGSyjg_3baOgoqTc,77365
|
||||
sqlalchemy/engine/result.py,sha256=I-XjIh-TUcoN4AWk3qBpIN9fa6Tmn22lJUw2CFm26k8,80278
|
||||
sqlalchemy/engine/row.py,sha256=dM3rJY3ASx_PzFKzu7CUGErJA_aIQYx1DibLAWnzY8M,12360
|
||||
sqlalchemy/engine/strategies.py,sha256=Ryy15JovfbIMsF2sM23z0HYJ_7lXRBQlzpLacbn0mLg,461
|
||||
sqlalchemy/engine/url.py,sha256=Lxdv29vz0l-FuWuZS7Gn5uLOxR75OPW1wxQpE58OEg4,31607
|
||||
sqlalchemy/engine/util.py,sha256=YSuXV8ngYMaQy7mouAFJiin-rrtD6Pm04KZiIfM-sTs,5849
|
||||
sqlalchemy/event/__init__.py,sha256=2QcWKXnqGRkl0lroK7ei8jT2Qbt8SRn_jqlTuYXGLu0,1022
|
||||
sqlalchemy/event/__pycache__/__init__.cpython-310.pyc,,
|
||||
sqlalchemy/event/__pycache__/api.cpython-310.pyc,,
|
||||
sqlalchemy/event/__pycache__/attr.cpython-310.pyc,,
|
||||
sqlalchemy/event/__pycache__/base.cpython-310.pyc,,
|
||||
sqlalchemy/event/__pycache__/legacy.cpython-310.pyc,,
|
||||
sqlalchemy/event/__pycache__/registry.cpython-310.pyc,,
|
||||
sqlalchemy/event/api.py,sha256=_TjSW28so8R84M_s7gV6oMlgpvZ0MuM0fNxWEMj4BGM,8452
|
||||
sqlalchemy/event/attr.py,sha256=o-Vr3RAGxE2mCXIvlMAGWmdb4My6q-JPkNTQdCW3IpI,21079
|
||||
sqlalchemy/event/base.py,sha256=7R8MtAATdP9EybWyvlQDsnoHg5Biauxf13INBlEuLmg,15491
|
||||
sqlalchemy/event/legacy.py,sha256=e_NtSjva3NmKLhM8kaUwLk2D705gjym2lYwrgQS0r9I,8457
|
||||
sqlalchemy/event/registry.py,sha256=s7MlVn2cOPndD9pm4F7_NFE4sIP5fTGhDdSaRQV4xAU,11247
|
||||
sqlalchemy/events.py,sha256=T8_TlVzRzd0Af9AAKUPXPxROwxeux7KuNhHTG0Cxamg,553
|
||||
sqlalchemy/exc.py,sha256=qAEWjEGvoPvEdzLalZfqWSCr7D1OUh1LikZPie0Ld3s,24844
|
||||
sqlalchemy/ext/__init__.py,sha256=2ow4CHEH4B_6wyAWKh1wqEbAUXG5ia2z2zASTa0Oqdk,333
|
||||
sqlalchemy/ext/__pycache__/__init__.cpython-310.pyc,,
|
||||
sqlalchemy/ext/__pycache__/associationproxy.cpython-310.pyc,,
|
||||
sqlalchemy/ext/__pycache__/automap.cpython-310.pyc,,
|
||||
sqlalchemy/ext/__pycache__/baked.cpython-310.pyc,,
|
||||
sqlalchemy/ext/__pycache__/compiler.cpython-310.pyc,,
|
||||
sqlalchemy/ext/__pycache__/horizontal_shard.cpython-310.pyc,,
|
||||
sqlalchemy/ext/__pycache__/hybrid.cpython-310.pyc,,
|
||||
sqlalchemy/ext/__pycache__/indexable.cpython-310.pyc,,
|
||||
sqlalchemy/ext/__pycache__/instrumentation.cpython-310.pyc,,
|
||||
sqlalchemy/ext/__pycache__/mutable.cpython-310.pyc,,
|
||||
sqlalchemy/ext/__pycache__/orderinglist.cpython-310.pyc,,
|
||||
sqlalchemy/ext/__pycache__/serializer.cpython-310.pyc,,
|
||||
sqlalchemy/ext/associationproxy.py,sha256=Bgp1WjYLRhr8zTBv_1eHEj6eKZlTEiinqXofuzreH1A,68020
|
||||
sqlalchemy/ext/asyncio/__init__.py,sha256=Qh5SCnlKUSkm1Ko5mzlNZ3_BUuU-aFg0vnlkhEOJdOE,1279
|
||||
sqlalchemy/ext/asyncio/__pycache__/__init__.cpython-310.pyc,,
|
||||
sqlalchemy/ext/asyncio/__pycache__/base.cpython-310.pyc,,
|
||||
sqlalchemy/ext/asyncio/__pycache__/engine.cpython-310.pyc,,
|
||||
sqlalchemy/ext/asyncio/__pycache__/exc.cpython-310.pyc,,
|
||||
sqlalchemy/ext/asyncio/__pycache__/result.cpython-310.pyc,,
|
||||
sqlalchemy/ext/asyncio/__pycache__/scoping.cpython-310.pyc,,
|
||||
sqlalchemy/ext/asyncio/__pycache__/session.cpython-310.pyc,,
|
||||
sqlalchemy/ext/asyncio/base.py,sha256=BRHwuXUqxmJDYBTgaOjRzvwwzG1qn9b3JOs_6opUs-g,9300
|
||||
sqlalchemy/ext/asyncio/engine.py,sha256=lWn-w4VrZPJNvtMXOJnwKjPev6RTqygsn0sb5NBqktU,49188
|
||||
sqlalchemy/ext/asyncio/exc.py,sha256=AeGYi7BtwZGHv4ZWaJl7wzE4u8dRlzi_06V_ipNPdfU,660
|
||||
sqlalchemy/ext/asyncio/result.py,sha256=DrGcMICQThnvVH3YabnERYWONrQ3-E1oM-oVrSFT66E,31546
|
||||
sqlalchemy/ext/asyncio/scoping.py,sha256=yVbP-AqxpYASySb5SerYIYMXj97jlauonMIYWr1idGA,52440
|
||||
sqlalchemy/ext/asyncio/session.py,sha256=mdrkOinsEgiwFvyfYwad49aG_0iQTrtPMY1_V9bfUqk,63005
|
||||
sqlalchemy/ext/automap.py,sha256=VC9p8sDu_EgWfqZ6aGAfVNBuUbnq4O2MjhUfgpY7keA,63089
|
||||
sqlalchemy/ext/baked.py,sha256=vHWGGYyceArr5v-nGxgDfwVgnvUjcuGOllAZ5zb_PXI,18392
|
||||
sqlalchemy/ext/compiler.py,sha256=pno-btbT4t16LEHUkRyVX5K6ct-MsPfixO41jhUI6R4,20946
|
||||
sqlalchemy/ext/declarative/__init__.py,sha256=4a8Wl2P_BqYVYmx-HsPtt_U-NvwpVsAKtfWUSNbA2uY,1883
|
||||
sqlalchemy/ext/declarative/__pycache__/__init__.cpython-310.pyc,,
|
||||
sqlalchemy/ext/declarative/__pycache__/extensions.cpython-310.pyc,,
|
||||
sqlalchemy/ext/declarative/extensions.py,sha256=GcAzNVSWKg6XXFbOIG6U4vXf0e__QB_Y7XWC8d23WhY,20095
|
||||
sqlalchemy/ext/horizontal_shard.py,sha256=I8-KZiCgbtSbGww_vOD6OPzMXXRNUg9X4n1fF8EZTRo,17249
|
||||
sqlalchemy/ext/hybrid.py,sha256=u7oR4DuriIuA2vNvde31fmtgMsRkBL667L7pqdzwGaE,54039
|
||||
sqlalchemy/ext/indexable.py,sha256=F3NC4VaUkhrx4jDmaEuJLQ2AXatk9l4l_aVI5Uzazbs,11369
|
||||
sqlalchemy/ext/instrumentation.py,sha256=biLs17X8UIGzisx-jC6JOphtldi-mlfR2bfumnlar70,16175
|
||||
sqlalchemy/ext/mutable.py,sha256=3s_qKPt6It7A-7gdQxjL5p7kFE72raf0lgjimK__lFk,38471
|
||||
sqlalchemy/ext/mypy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
sqlalchemy/ext/mypy/__pycache__/__init__.cpython-310.pyc,,
|
||||
sqlalchemy/ext/mypy/__pycache__/apply.cpython-310.pyc,,
|
||||
sqlalchemy/ext/mypy/__pycache__/decl_class.cpython-310.pyc,,
|
||||
sqlalchemy/ext/mypy/__pycache__/infer.cpython-310.pyc,,
|
||||
sqlalchemy/ext/mypy/__pycache__/names.cpython-310.pyc,,
|
||||
sqlalchemy/ext/mypy/__pycache__/plugin.cpython-310.pyc,,
|
||||
sqlalchemy/ext/mypy/__pycache__/util.cpython-310.pyc,,
|
||||
sqlalchemy/ext/mypy/apply.py,sha256=O3Rh-FCWiWJPeUT0dVsFD_fcL5oEJbrAkB0bAJ5I7Sg,10821
|
||||
sqlalchemy/ext/mypy/decl_class.py,sha256=hfTpozOGwxeX9Vbkm12i8SawR91ngs1nfsiPC_f0PSg,17892
|
||||
sqlalchemy/ext/mypy/infer.py,sha256=BsiKdH1IvbgRGpqGzDPt1bkDXCcBjzEhQcyHdaToohs,19954
|
||||
sqlalchemy/ext/mypy/names.py,sha256=syJhY2UYcodS_NjuNdg-7ay_ZW8kf6xXMbTpMjHrdLQ,11310
|
||||
sqlalchemy/ext/mypy/plugin.py,sha256=XF1E_XqZJA-RnI_d0_FWvwFIBTyBRA95sFUP0uqUygk,10053
|
||||
sqlalchemy/ext/mypy/util.py,sha256=daH6X26-zYEBR1dnX3K5MJ946HHtgcYSH848LGT_uko,9762
|
||||
sqlalchemy/ext/orderinglist.py,sha256=xeonIRL-m5Y4vB2n_1Nab8B61geRLHR0kyC_KnXTS7k,14800
|
||||
sqlalchemy/ext/serializer.py,sha256=BhyC7ydKcKKz4vlxyU_8ranVigiGSO1hw_LieCxLCgM,6363
|
||||
sqlalchemy/future/__init__.py,sha256=Iio4lD-SrIcuBq0gP7MRgVnU4v36gIMLHiQrSKy-sPM,532
|
||||
sqlalchemy/future/__pycache__/__init__.cpython-310.pyc,,
|
||||
sqlalchemy/future/__pycache__/engine.cpython-310.pyc,,
|
||||
sqlalchemy/future/engine.py,sha256=4iO5THuQWIy3UGpOOMml23daun4wHdRZ6JVSwWykjJI,514
|
||||
sqlalchemy/inspection.py,sha256=tJc_KriMGJ6kSRkKn5MvhxooFbPEAq3W5l-ggFw2sdE,5326
|
||||
sqlalchemy/log.py,sha256=_31kfcLRj9dtaQs-VRMHqjPq2sGVYUeSUyO-OiafdlQ,8918
|
||||
sqlalchemy/orm/__init__.py,sha256=M1pqaRU6NQuDcycnrbkKHcKjNAo3ZbGne3MOFPK0n1o,8633
|
||||
sqlalchemy/orm/__pycache__/__init__.cpython-310.pyc,,
|
||||
sqlalchemy/orm/__pycache__/_orm_constructors.cpython-310.pyc,,
|
||||
sqlalchemy/orm/__pycache__/_typing.cpython-310.pyc,,
|
||||
sqlalchemy/orm/__pycache__/attributes.cpython-310.pyc,,
|
||||
sqlalchemy/orm/__pycache__/base.cpython-310.pyc,,
|
||||
sqlalchemy/orm/__pycache__/bulk_persistence.cpython-310.pyc,,
|
||||
sqlalchemy/orm/__pycache__/clsregistry.cpython-310.pyc,,
|
||||
sqlalchemy/orm/__pycache__/collections.cpython-310.pyc,,
|
||||
sqlalchemy/orm/__pycache__/context.cpython-310.pyc,,
|
||||
sqlalchemy/orm/__pycache__/decl_api.cpython-310.pyc,,
|
||||
sqlalchemy/orm/__pycache__/decl_base.cpython-310.pyc,,
|
||||
sqlalchemy/orm/__pycache__/dependency.cpython-310.pyc,,
|
||||
sqlalchemy/orm/__pycache__/descriptor_props.cpython-310.pyc,,
|
||||
sqlalchemy/orm/__pycache__/dynamic.cpython-310.pyc,,
|
||||
sqlalchemy/orm/__pycache__/evaluator.cpython-310.pyc,,
|
||||
sqlalchemy/orm/__pycache__/events.cpython-310.pyc,,
|
||||
sqlalchemy/orm/__pycache__/exc.cpython-310.pyc,,
|
||||
sqlalchemy/orm/__pycache__/identity.cpython-310.pyc,,
|
||||
sqlalchemy/orm/__pycache__/instrumentation.cpython-310.pyc,,
|
||||
sqlalchemy/orm/__pycache__/interfaces.cpython-310.pyc,,
|
||||
sqlalchemy/orm/__pycache__/loading.cpython-310.pyc,,
|
||||
sqlalchemy/orm/__pycache__/mapped_collection.cpython-310.pyc,,
|
||||
sqlalchemy/orm/__pycache__/mapper.cpython-310.pyc,,
|
||||
sqlalchemy/orm/__pycache__/path_registry.cpython-310.pyc,,
|
||||
sqlalchemy/orm/__pycache__/persistence.cpython-310.pyc,,
|
||||
sqlalchemy/orm/__pycache__/properties.cpython-310.pyc,,
|
||||
sqlalchemy/orm/__pycache__/query.cpython-310.pyc,,
|
||||
sqlalchemy/orm/__pycache__/relationships.cpython-310.pyc,,
|
||||
sqlalchemy/orm/__pycache__/scoping.cpython-310.pyc,,
|
||||
sqlalchemy/orm/__pycache__/session.cpython-310.pyc,,
|
||||
sqlalchemy/orm/__pycache__/state.cpython-310.pyc,,
|
||||
sqlalchemy/orm/__pycache__/state_changes.cpython-310.pyc,,
|
||||
sqlalchemy/orm/__pycache__/strategies.cpython-310.pyc,,
|
||||
sqlalchemy/orm/__pycache__/strategy_options.cpython-310.pyc,,
|
||||
sqlalchemy/orm/__pycache__/sync.cpython-310.pyc,,
|
||||
sqlalchemy/orm/__pycache__/unitofwork.cpython-310.pyc,,
|
||||
sqlalchemy/orm/__pycache__/util.cpython-310.pyc,,
|
||||
sqlalchemy/orm/__pycache__/writeonly.cpython-310.pyc,,
|
||||
sqlalchemy/orm/_orm_constructors.py,sha256=5CbxrInx1FYY40QxUNytbWfHRh3X0WH7WeUYXeYOiwQ,102250
|
||||
sqlalchemy/orm/_typing.py,sha256=BmW1g-19ze_sbqF9uKlAqrPyp4jxVkmv-jjJmFpYhdg,5192
|
||||
sqlalchemy/orm/attributes.py,sha256=sjCxIVm8nVjQnPt_MDgI3-DKZz4WYOsMuLKbEeIDJLk,95458
|
||||
sqlalchemy/orm/base.py,sha256=KP34QXa9bLKPOMKHQ2Jf9bpEhTqqrbRG7uMxh7_2TLI,28459
|
||||
sqlalchemy/orm/bulk_persistence.py,sha256=_OqUjW9yi3l7VYj1564CYvc8p4CjEXsFYGDt6wR0M0I,72044
|
||||
sqlalchemy/orm/clsregistry.py,sha256=dgGogYd-ED3EUIgQkdUesequKBiGTs7csAB6Z5ROjrU,18521
|
||||
sqlalchemy/orm/collections.py,sha256=HhJOKqeaDdeYFVyFyU4blvoD6tRsXF4X7TG1xUyhbdk,53778
|
||||
sqlalchemy/orm/context.py,sha256=dmqih2qp7EkiIjp6ATihNOBTtAduYdjv8kLp2vah5EY,114925
|
||||
sqlalchemy/orm/decl_api.py,sha256=w2FWGSAgq2_T7r3g1DaaLLX9Y_AQw6UCwrTxU23EcVo,65649
|
||||
sqlalchemy/orm/decl_base.py,sha256=gGasl-rchnQxz9YcQESMe0zn201d5sFhx39Yph1trWc,83425
|
||||
sqlalchemy/orm/dependency.py,sha256=vXDcvgrY8_9x5lLWIpoLvzwuN0b-2-DwhXux1nNf0E8,48885
|
||||
sqlalchemy/orm/descriptor_props.py,sha256=fwsr88pQytZLQ8r0u-nXjgk1evU9MYtxmvgHs42gl_I,38502
|
||||
sqlalchemy/orm/dynamic.py,sha256=MWkalEIrgW0p_CRr5Uclt6Ak-jUvcpyYHaTWidIuXdc,8898
|
||||
sqlalchemy/orm/evaluator.py,sha256=lQ_uAoUBKJtQAyvhyFfHOf8gvxk0_-r4KpqQR53-COE,12293
|
||||
sqlalchemy/orm/events.py,sha256=UguRQ343Q-rGxKjPQTgqOs5uVUkW1rcLBWoOSZUHirI,130506
|
||||
sqlalchemy/orm/exc.py,sha256=k9K4M3zZvE7877TiTOI5gG4MOgnBEbKqvAsP43JU2Dk,7583
|
||||
sqlalchemy/orm/identity.py,sha256=mVaoHHtniM1-wSqJ0VPu2v6LaSJfer4_vLsxFVw8aXc,9551
|
||||
sqlalchemy/orm/instrumentation.py,sha256=VIWeAsEMveE2N-9W6mS5Lw40wwldHL_fGhln0EowEsY,25207
|
||||
sqlalchemy/orm/interfaces.py,sha256=smpV-eYelH0Eswfav_kxOFRB4VE9_Nm0BPUNDMSqYSI,49878
|
||||
sqlalchemy/orm/loading.py,sha256=qqr6wMPgP5qChbiaz4s4ea-fRNiqyc8Fu9_wj_UjfhQ,58032
|
||||
sqlalchemy/orm/mapped_collection.py,sha256=kxy8_wQPjbjEMxCEe-l5lgH9wza_JedWzcksXXjLK9E,20260
|
||||
sqlalchemy/orm/mapper.py,sha256=NGoiIjmTIK28MyvDnxgWkWdYv2e7fSUroqvoj0dPpOg,175446
|
||||
sqlalchemy/orm/path_registry.py,sha256=hvTSOojMC31-nUJFHz2DynIh4_x85QQzemn7RFTdddc,26436
|
||||
sqlalchemy/orm/persistence.py,sha256=flpr9E1BjytqAeijNJu13DiYcVf7rYUXCQce9-o4WCg,62271
|
||||
sqlalchemy/orm/properties.py,sha256=unDpqioAz2DGH-ftJrJ_V9G0QcqVeD-PzTvqbTY6bj4,27622
|
||||
sqlalchemy/orm/query.py,sha256=fp5Oe5J7_RMHBcw74ScgOLmrfEm-zkZFDiFJRTJVfP0,121046
|
||||
sqlalchemy/orm/relationships.py,sha256=1ZN1WTkSCEICtl0cRpUyIRfeF0VxvAjtjiRfQ_lMrPE,131317
|
||||
sqlalchemy/orm/scoping.py,sha256=O1-yguXL3W9Vtq8_wtC2B9Nc_0LtPB50NeOe8H6FrhA,77441
|
||||
sqlalchemy/orm/session.py,sha256=FivieQ2V4UjKP08zJij8RbiwHDqao9fbfqoO7XdIb6U,193191
|
||||
sqlalchemy/orm/state.py,sha256=UrAw3nTX9wlFoJbzKL1DXvD_KGvgfcD8xMnqzM5qO5U,38719
|
||||
sqlalchemy/orm/state_changes.py,sha256=9jHui0oF4V7xIoL8hSUiFDhHrJVPmq3doX5zxY5dR3g,7013
|
||||
sqlalchemy/orm/strategies.py,sha256=dUNlfzmWDzRb1JOG_xFisMtlmDTxeQLn4GkmBsN9b3o,117432
|
||||
sqlalchemy/orm/strategy_options.py,sha256=cBic_Tm6rwmOiHncwUC8eM3SWPcVJkHM5T4YFwSto4U,84304
|
||||
sqlalchemy/orm/sync.py,sha256=jLZWFRsn2iobdX23gQlthxhnFHQum3mWQHHcx8uj0t4,5912
|
||||
sqlalchemy/orm/unitofwork.py,sha256=Jyri2dH5VmkX7oN-XWRzzKMCjQAzvBPWWem5XQL5SYY,27829
|
||||
sqlalchemy/orm/util.py,sha256=4Mk2QOA-XjVybwEbdExpQ-9w9PKCPffYh1blvNl4CWU,82375
|
||||
sqlalchemy/orm/writeonly.py,sha256=AKgGFWiOSAvYoqvAaKA9wiOJkZ6pvCR3AM-K1I51crY,20154
|
||||
sqlalchemy/pool/__init__.py,sha256=rvWJtziqz1Yp_9NU7r-cKH1WKi8MwcjZX6kuBYu_s6s,1859
|
||||
sqlalchemy/pool/__pycache__/__init__.cpython-310.pyc,,
|
||||
sqlalchemy/pool/__pycache__/base.cpython-310.pyc,,
|
||||
sqlalchemy/pool/__pycache__/events.cpython-310.pyc,,
|
||||
sqlalchemy/pool/__pycache__/impl.cpython-310.pyc,,
|
||||
sqlalchemy/pool/base.py,sha256=3-o9EcxLi_D6lrd5SDR6dhBv9oiMf-JX5SD-BekG7FE,53869
|
||||
sqlalchemy/pool/events.py,sha256=dQDxP7Rwz3nDv2xkxQvkq3Eyj-00sBYIoF02lrKUHLM,13571
|
||||
sqlalchemy/pool/impl.py,sha256=FpgcyklqBciEorQq5M0XxsbZG6HD-tqrXuqxA3wEkO8,18292
|
||||
sqlalchemy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
sqlalchemy/schema.py,sha256=Liwt9G2PyOZZGfQkTGx7fFjTNH2o8t9kPcCAIHF9etw,3264
|
||||
sqlalchemy/sql/__init__.py,sha256=9ffSt_Gl2TJYE4_PyWxtRNfBF8yGmIV2J_CaoAyx2QQ,5965
|
||||
sqlalchemy/sql/__pycache__/__init__.cpython-310.pyc,,
|
||||
sqlalchemy/sql/__pycache__/_dml_constructors.cpython-310.pyc,,
|
||||
sqlalchemy/sql/__pycache__/_elements_constructors.cpython-310.pyc,,
|
||||
sqlalchemy/sql/__pycache__/_orm_types.cpython-310.pyc,,
|
||||
sqlalchemy/sql/__pycache__/_py_util.cpython-310.pyc,,
|
||||
sqlalchemy/sql/__pycache__/_selectable_constructors.cpython-310.pyc,,
|
||||
sqlalchemy/sql/__pycache__/_typing.cpython-310.pyc,,
|
||||
sqlalchemy/sql/__pycache__/annotation.cpython-310.pyc,,
|
||||
sqlalchemy/sql/__pycache__/base.cpython-310.pyc,,
|
||||
sqlalchemy/sql/__pycache__/cache_key.cpython-310.pyc,,
|
||||
sqlalchemy/sql/__pycache__/coercions.cpython-310.pyc,,
|
||||
sqlalchemy/sql/__pycache__/compiler.cpython-310.pyc,,
|
||||
sqlalchemy/sql/__pycache__/crud.cpython-310.pyc,,
|
||||
sqlalchemy/sql/__pycache__/ddl.cpython-310.pyc,,
|
||||
sqlalchemy/sql/__pycache__/default_comparator.cpython-310.pyc,,
|
||||
sqlalchemy/sql/__pycache__/dml.cpython-310.pyc,,
|
||||
sqlalchemy/sql/__pycache__/elements.cpython-310.pyc,,
|
||||
sqlalchemy/sql/__pycache__/events.cpython-310.pyc,,
|
||||
sqlalchemy/sql/__pycache__/expression.cpython-310.pyc,,
|
||||
sqlalchemy/sql/__pycache__/functions.cpython-310.pyc,,
|
||||
sqlalchemy/sql/__pycache__/lambdas.cpython-310.pyc,,
|
||||
sqlalchemy/sql/__pycache__/naming.cpython-310.pyc,,
|
||||
sqlalchemy/sql/__pycache__/operators.cpython-310.pyc,,
|
||||
sqlalchemy/sql/__pycache__/roles.cpython-310.pyc,,
|
||||
sqlalchemy/sql/__pycache__/schema.cpython-310.pyc,,
|
||||
sqlalchemy/sql/__pycache__/selectable.cpython-310.pyc,,
|
||||
sqlalchemy/sql/__pycache__/sqltypes.cpython-310.pyc,,
|
||||
sqlalchemy/sql/__pycache__/traversals.cpython-310.pyc,,
|
||||
sqlalchemy/sql/__pycache__/type_api.cpython-310.pyc,,
|
||||
sqlalchemy/sql/__pycache__/util.cpython-310.pyc,,
|
||||
sqlalchemy/sql/__pycache__/visitors.cpython-310.pyc,,
|
||||
sqlalchemy/sql/_dml_constructors.py,sha256=CRI_cxOwcSBPUhouMuNoxBVb3EB0b6zQo6YSjI1d2Vo,4007
|
||||
sqlalchemy/sql/_elements_constructors.py,sha256=gSshp_t_TteDk4mvekbPBDABWDW-umShMPmjCrUrLBs,64764
|
||||
sqlalchemy/sql/_orm_types.py,sha256=WIdXTDALHCd3PuzpAPot2psv505T817wnQ3oXuGH7CU,640
|
||||
sqlalchemy/sql/_py_util.py,sha256=YCkagVa5Ov1OjCfMhUbNa527zX2NsUGPuCLEWmbQQMA,2248
|
||||
sqlalchemy/sql/_selectable_constructors.py,sha256=kHsJViBwydt3l-aiqtI2yLK4BgjEsVB9lpKPuWSWv60,19454
|
||||
sqlalchemy/sql/_typing.py,sha256=WSEwGys86fVA_WAY6Nbu1Qn3Zam25Togi-OxM7Nlu2E,12724
|
||||
sqlalchemy/sql/annotation.py,sha256=2c7wyGxH2gk9TIZELK3x8OSbQBKE4MGAFEWv3hefIbI,18881
|
||||
sqlalchemy/sql/base.py,sha256=d7sIYp0XUH6FPqbehP5VC_8o31PuamwQgBILiotwE3o,76137
|
||||
sqlalchemy/sql/cache_key.py,sha256=AaPpvFDwYMfGixEmEto02flVKyf4UifGdNbu2Z4rfMc,33773
|
||||
sqlalchemy/sql/coercions.py,sha256=YXSGtgtUIJeDPPjtBpFA2PuZ0bP-x8E6N01OIDWJvxw,41959
|
||||
sqlalchemy/sql/compiler.py,sha256=G1Vv0zq1RSjJe0fKE0ih8UXUMCKIljBHqbw-BSoBLqg,276204
|
||||
sqlalchemy/sql/crud.py,sha256=rJ9U55a0oVH5cl4Su5mwCpS6UogZsz1ue6bNvEShrMc,57295
|
||||
sqlalchemy/sql/ddl.py,sha256=J9wtcvAfh2E08Fg-bjr_TWQGmYWWn4vSr5VFJPxThk4,47069
|
||||
sqlalchemy/sql/default_comparator.py,sha256=-sSDLcxHfL4jgIh5f_g2-8wOGazDHhTPyB0pA9Gr_Uk,17212
|
||||
sqlalchemy/sql/dml.py,sha256=vzOpq8TqhdYN7jfla27c4pQITdO0IPcKdV5y92vjONM,67380
|
||||
sqlalchemy/sql/elements.py,sha256=90qmYcYRdwEI_E4dREe98AXIUO7pAoyZ3D4pSq5KEko,176494
|
||||
sqlalchemy/sql/events.py,sha256=ELMw8mkKUvVVeb354hfD_9Fk-KSX28hCfNdnMgnv2zI,18756
|
||||
sqlalchemy/sql/expression.py,sha256=bD-nG9OxLmhgvKkCKFpMtycxq8szzUsxhDeH3E9WmkQ,7748
|
||||
sqlalchemy/sql/functions.py,sha256=YMkPdiSBZnrA4ZrcolA58w1nsoEQbwrL38kMKwiLZP0,56289
|
||||
sqlalchemy/sql/lambdas.py,sha256=lKggh8EyKJThwop8ulclAcMzJh4gydSTwEL8cyMCN10,50759
|
||||
sqlalchemy/sql/naming.py,sha256=3J_KScJBe7dPY4Stj5veIzGfSnaWG9-f7IGbxhc7luE,7077
|
||||
sqlalchemy/sql/operators.py,sha256=nqVC7ggPHfwF4QFMZz7uk0WvPUdxw7s92jjTGkgLJzs,78483
|
||||
sqlalchemy/sql/roles.py,sha256=Rkbx36tMWkq6uu6bl1DMDkLQY8VFpKZtqfmYxooE8MQ,7952
|
||||
sqlalchemy/sql/schema.py,sha256=5oaelPn8oUFB9b7XYJ4OjNqPhWQXvKie3UoReB0KfgY,233840
|
||||
sqlalchemy/sql/selectable.py,sha256=QCeqrL6dU1-ZncSO3z0G5KksghMZyanAm6Egtj_5Xak,239710
|
||||
sqlalchemy/sql/sqltypes.py,sha256=HsFU48oRsD-bLDSHim6LVcyZUpkPxK2eB-MUllRBZyg,129894
|
||||
sqlalchemy/sql/traversals.py,sha256=3s9dNBNwsrpg57x0vxF5J-bdeZpzURGEbfpUhaCt1wU,34622
|
||||
sqlalchemy/sql/type_api.py,sha256=QcUD2j49soLl07LQVHdDilZ1kUkRC_USwJx8xI6Qcyg,87359
|
||||
sqlalchemy/sql/util.py,sha256=5UCudam-vT7lMoYKv2juS483vjdiY2kcFejfjk17KlI,49770
|
||||
sqlalchemy/sql/visitors.py,sha256=1jPX8S1C6pFovW0HWkliVsI0BD0tmEUN8GYZ0F5Di6o,37534
|
||||
sqlalchemy/testing/__init__.py,sha256=I_4C9vgF-GRODJ_IRNxIXXSQHgUDNVggGFFv6v0BJBQ,3221
|
||||
sqlalchemy/testing/__pycache__/__init__.cpython-310.pyc,,
|
||||
sqlalchemy/testing/__pycache__/assertions.cpython-310.pyc,,
|
||||
sqlalchemy/testing/__pycache__/assertsql.cpython-310.pyc,,
|
||||
sqlalchemy/testing/__pycache__/asyncio.cpython-310.pyc,,
|
||||
sqlalchemy/testing/__pycache__/config.cpython-310.pyc,,
|
||||
sqlalchemy/testing/__pycache__/engines.cpython-310.pyc,,
|
||||
sqlalchemy/testing/__pycache__/entities.cpython-310.pyc,,
|
||||
sqlalchemy/testing/__pycache__/exclusions.cpython-310.pyc,,
|
||||
sqlalchemy/testing/__pycache__/pickleable.cpython-310.pyc,,
|
||||
sqlalchemy/testing/__pycache__/profiling.cpython-310.pyc,,
|
||||
sqlalchemy/testing/__pycache__/provision.cpython-310.pyc,,
|
||||
sqlalchemy/testing/__pycache__/requirements.cpython-310.pyc,,
|
||||
sqlalchemy/testing/__pycache__/schema.cpython-310.pyc,,
|
||||
sqlalchemy/testing/__pycache__/util.cpython-310.pyc,,
|
||||
sqlalchemy/testing/__pycache__/warnings.cpython-310.pyc,,
|
||||
sqlalchemy/testing/assertions.py,sha256=K_wIe570kI6xbtZmWMnLiuhBaCFyoQx-iaau9CR1PLI,32428
|
||||
sqlalchemy/testing/assertsql.py,sha256=F8R_LgsUiZ31sgkCigFbxFcVOzt57EI1tRSHgk88CjQ,17290
|
||||
sqlalchemy/testing/asyncio.py,sha256=7LatmcAk09IVuOMWcAdpVltLyhDWVG1cV7iOaR6soz0,3858
|
||||
sqlalchemy/testing/config.py,sha256=L5Z7Inl6th23xpuVDo6Fhn3ItMca0yBT6YofkYRZiKI,11397
|
||||
sqlalchemy/testing/engines.py,sha256=mTwCxPdb6K8S2y-O7dcJwO5kEE4qIAU8Kp6qawiP2TM,13824
|
||||
sqlalchemy/testing/entities.py,sha256=E7IkhsQaziZSOZkKkFnfUvB0165SH5MP1q4QkGKdf98,3471
|
||||
sqlalchemy/testing/exclusions.py,sha256=rWyo1SZpZ-EjNkvr-O085A3XSAqxSL5uqgOE4L5mzM0,12879
|
||||
sqlalchemy/testing/fixtures/__init__.py,sha256=SHlEUIlUaqU2xjziZeBDL1Yd_URWou6dnZqG8s-Ay0g,1226
|
||||
sqlalchemy/testing/fixtures/__pycache__/__init__.cpython-310.pyc,,
|
||||
sqlalchemy/testing/fixtures/__pycache__/base.cpython-310.pyc,,
|
||||
sqlalchemy/testing/fixtures/__pycache__/mypy.cpython-310.pyc,,
|
||||
sqlalchemy/testing/fixtures/__pycache__/orm.cpython-310.pyc,,
|
||||
sqlalchemy/testing/fixtures/__pycache__/sql.cpython-310.pyc,,
|
||||
sqlalchemy/testing/fixtures/base.py,sha256=HQPgUuOnBVNUm9l7dcoT4AuggG0jXW4a23dVZhbSA9k,12622
|
||||
sqlalchemy/testing/fixtures/mypy.py,sha256=TGTo8x02m9Qt10-iRjic_F66-uA2T82QbKYmpUyVzCw,12153
|
||||
sqlalchemy/testing/fixtures/orm.py,sha256=VZBnFHHfQpVdLqV-F9myH886uq3clDIzixMwEe5TjMc,6322
|
||||
sqlalchemy/testing/fixtures/sql.py,sha256=1c2omUuUSRlWNYtKr3ADJFaQeFnq1EnQtKTKn67B-2Q,16196
|
||||
sqlalchemy/testing/pickleable.py,sha256=_E141_vPqtE-H_6cNBnWRDuMtvjeeBKovOI2_P9KJGQ,2988
|
||||
sqlalchemy/testing/plugin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
sqlalchemy/testing/plugin/__pycache__/__init__.cpython-310.pyc,,
|
||||
sqlalchemy/testing/plugin/__pycache__/bootstrap.cpython-310.pyc,,
|
||||
sqlalchemy/testing/plugin/__pycache__/plugin_base.cpython-310.pyc,,
|
||||
sqlalchemy/testing/plugin/__pycache__/pytestplugin.cpython-310.pyc,,
|
||||
sqlalchemy/testing/plugin/bootstrap.py,sha256=3WkvZXQad0oyxg6nJyLEthN30zBuueB33LFxWeMBdKc,1482
|
||||
sqlalchemy/testing/plugin/plugin_base.py,sha256=300KMijfgPiKFbjj1F_6Dj6HAH8T-EuHuX3CQ6Z1m00,22110
|
||||
sqlalchemy/testing/plugin/pytestplugin.py,sha256=IYmAr-FaAODCInWpnKJCNGw03cyRJK5YcZ07hrmtSNg,28151
|
||||
sqlalchemy/testing/profiling.py,sha256=BLknvjemW8oDl0aCSUXQw_ESgoFfx0RoeQYclzhNv0Q,10472
|
||||
sqlalchemy/testing/provision.py,sha256=fnmlxUacztdUcZOPTiSJ-rYe-tpIwyl8O97YRI24FLk,14686
|
||||
sqlalchemy/testing/requirements.py,sha256=qc5zgxUJGmpZjKO4pVSDlKOYs_f8_W7KienkLgoJJZ4,52702
|
||||
sqlalchemy/testing/schema.py,sha256=O76C-woOcc6qjk1csf9yljor-6wjRZzKQNrXdmLLtw8,6737
|
||||
sqlalchemy/testing/suite/__init__.py,sha256=u3lEc0j47s7Dad_2SVWOZ6EU2aOMRWqE_WrQ17HmBsA,489
|
||||
sqlalchemy/testing/suite/__pycache__/__init__.cpython-310.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_cte.cpython-310.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_ddl.cpython-310.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_deprecations.cpython-310.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_dialect.cpython-310.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_insert.cpython-310.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_reflection.cpython-310.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_results.cpython-310.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_rowcount.cpython-310.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_select.cpython-310.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_sequence.cpython-310.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_types.cpython-310.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_unicode_ddl.cpython-310.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_update_delete.cpython-310.pyc,,
|
||||
sqlalchemy/testing/suite/test_cte.py,sha256=RFvWCSmmy3-JEIXIcZEkYpjt_bJQTe7SvfnpXeUFl9o,6410
|
||||
sqlalchemy/testing/suite/test_ddl.py,sha256=VQuejaNUMN484DxwkYL57ZEnPY5UhNGWkQVgquPrWHA,12168
|
||||
sqlalchemy/testing/suite/test_deprecations.py,sha256=8mhjZyrECXiVyE88BJLbyj4Jz_niXOs6ZOXd9rFAWsk,5229
|
||||
sqlalchemy/testing/suite/test_dialect.py,sha256=C3aUUd16iS19WJrqZLGkG4DPhWMAADTCfTDkYYlUz5U,23407
|
||||
sqlalchemy/testing/suite/test_insert.py,sha256=NVPw5lMxNSY99E7p_YZ2bBJG2C4SBCGv2nPyQtrxQnk,17948
|
||||
sqlalchemy/testing/suite/test_reflection.py,sha256=si5nXSHcBWJRrcFC-0sto1m9jXf6hYwlYuQ43YqXiJo,107540
|
||||
sqlalchemy/testing/suite/test_results.py,sha256=fl8qv9YdcEqSQFe1fDf7vrFk7Cam_MzJiETLzx5KYuU,16127
|
||||
sqlalchemy/testing/suite/test_rowcount.py,sha256=ID2Y1jDZ1MjQyZldWQPt40qD2eu0haTaYB2AdWQ1Nnk,6356
|
||||
sqlalchemy/testing/suite/test_select.py,sha256=3SCpfuy-D0q0l4elRNgMc0KlIjo3Nh1nGHo0uEZKAUo,60207
|
||||
sqlalchemy/testing/suite/test_sequence.py,sha256=WqriMWJTPnmCBydP5vehSD2zXxwSyFGug_K2IyEIRSY,9983
|
||||
sqlalchemy/testing/suite/test_types.py,sha256=NObEx25I9LQdMaW470ACm4w9fWRVYf9B0i1VafrOhGQ,63791
|
||||
sqlalchemy/testing/suite/test_unicode_ddl.py,sha256=1n0xf7EyGuLYIMiwc5lANGWvCrmoXf3gtQM93sMcd3c,6070
|
||||
sqlalchemy/testing/suite/test_update_delete.py,sha256=cj9C7U8MFxMSfdckAdODQHsnqwOtU112zqk5U8iyCTM,1710
|
||||
sqlalchemy/testing/util.py,sha256=QH355pEJeqUn4h2LPTleNGinD50hE40R1HRW2LEXF6U,14599
|
||||
sqlalchemy/testing/warnings.py,sha256=ymXClxi_YtysQJZZQzgjT-d3tW63z4pOfKJsTqaBLMQ,1598
|
||||
sqlalchemy/types.py,sha256=bV5WvXIjsG-bWRcwCVACJ6m3tlSMS5XNdtXVXGTMeI8,3244
|
||||
sqlalchemy/util/__init__.py,sha256=NILyXDswLeG8lpxsPh2iIOs4BweaFz7tbn3wQ0-hK5c,8404
|
||||
sqlalchemy/util/__pycache__/__init__.cpython-310.pyc,,
|
||||
sqlalchemy/util/__pycache__/_collections.cpython-310.pyc,,
|
||||
sqlalchemy/util/__pycache__/_concurrency_py3k.cpython-310.pyc,,
|
||||
sqlalchemy/util/__pycache__/_has_cy.cpython-310.pyc,,
|
||||
sqlalchemy/util/__pycache__/_py_collections.cpython-310.pyc,,
|
||||
sqlalchemy/util/__pycache__/compat.cpython-310.pyc,,
|
||||
sqlalchemy/util/__pycache__/concurrency.cpython-310.pyc,,
|
||||
sqlalchemy/util/__pycache__/deprecations.cpython-310.pyc,,
|
||||
sqlalchemy/util/__pycache__/langhelpers.cpython-310.pyc,,
|
||||
sqlalchemy/util/__pycache__/preloaded.cpython-310.pyc,,
|
||||
sqlalchemy/util/__pycache__/queue.cpython-310.pyc,,
|
||||
sqlalchemy/util/__pycache__/tool_support.cpython-310.pyc,,
|
||||
sqlalchemy/util/__pycache__/topological.cpython-310.pyc,,
|
||||
sqlalchemy/util/__pycache__/typing.cpython-310.pyc,,
|
||||
sqlalchemy/util/_collections.py,sha256=yUsrZu5aQh0lK_ht3540KXsoaM-lz4nqK1gHXx29lBc,21124
|
||||
sqlalchemy/util/_concurrency_py3k.py,sha256=LhS5ppk9UGJ2P36rbHaauV3hl57p1Zw_b3SzLkkOiTE,8544
|
||||
sqlalchemy/util/_has_cy.py,sha256=7V8ZfMrlED0bIc6DWsBC_lTEP1JEihWKPdjyLJtxfaE,1268
|
||||
sqlalchemy/util/_py_collections.py,sha256=lwf6V7hnvqP_88eVKZa6GqshDxyBkhPczaIhpfrXE-Y,17217
|
||||
sqlalchemy/util/compat.py,sha256=xwPQl5QoV9Nrje-ZD_csX0penWXrhFi667v5WMdloB8,9416
|
||||
sqlalchemy/util/concurrency.py,sha256=rLb4LbPSTnGaSb381_e3VwHbEjqiF10lkarFjihywTY,2353
|
||||
sqlalchemy/util/deprecations.py,sha256=JMG1QpXb_HhCssZBkm0_vvCSWiCMIbAwIlULtP4DNgs,12514
|
||||
sqlalchemy/util/langhelpers.py,sha256=iJO7hWDKfeAndgu6iDcWRx3tMmbNquzSnxDruMK6130,67147
|
||||
sqlalchemy/util/preloaded.py,sha256=tMuj_6GELLQj1I8YRAKu--VLnxI9kH8Si_IwlDfre4M,6055
|
||||
sqlalchemy/util/queue.py,sha256=-DPCfkQgtqP9znBvm3bRdYjAWs4dysflpL805IMf22A,10529
|
||||
sqlalchemy/util/tool_support.py,sha256=SOfhWXzZXqx5RYX9WM_CeBJGGgV0eaxpv96VFb5KEKs,6167
|
||||
sqlalchemy/util/topological.py,sha256=-i2pX8AD9hjaqpLDu5P3R5w6D8_t5nDWlwLI6xXYyyc,3578
|
||||
sqlalchemy/util/typing.py,sha256=yFUquXzDGUuoyn4idoBkIsqjF_2noHM34DDInh2uERs,16207
|
||||
5
venv/Lib/site-packages/SQLAlchemy-2.0.20.dist-info/WHEEL
Normal file
5
venv/Lib/site-packages/SQLAlchemy-2.0.20.dist-info/WHEEL
Normal file
@@ -0,0 +1,5 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: bdist_wheel (0.41.1)
|
||||
Root-Is-Purelib: false
|
||||
Tag: cp310-cp310-win_amd64
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
sqlalchemy
|
||||
Binary file not shown.
222
venv/Lib/site-packages/_distutils_hack/__init__.py
Normal file
222
venv/Lib/site-packages/_distutils_hack/__init__.py
Normal file
@@ -0,0 +1,222 @@
|
||||
# don't import any costly modules
|
||||
import sys
|
||||
import os
|
||||
|
||||
|
||||
is_pypy = '__pypy__' in sys.builtin_module_names
|
||||
|
||||
|
||||
def warn_distutils_present():
|
||||
if 'distutils' not in sys.modules:
|
||||
return
|
||||
if is_pypy and sys.version_info < (3, 7):
|
||||
# PyPy for 3.6 unconditionally imports distutils, so bypass the warning
|
||||
# https://foss.heptapod.net/pypy/pypy/-/blob/be829135bc0d758997b3566062999ee8b23872b4/lib-python/3/site.py#L250
|
||||
return
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
"Distutils was imported before Setuptools, but importing Setuptools "
|
||||
"also replaces the `distutils` module in `sys.modules`. This may lead "
|
||||
"to undesirable behaviors or errors. To avoid these issues, avoid "
|
||||
"using distutils directly, ensure that setuptools is installed in the "
|
||||
"traditional way (e.g. not an editable install), and/or make sure "
|
||||
"that setuptools is always imported before distutils."
|
||||
)
|
||||
|
||||
|
||||
def clear_distutils():
|
||||
if 'distutils' not in sys.modules:
|
||||
return
|
||||
import warnings
|
||||
|
||||
warnings.warn("Setuptools is replacing distutils.")
|
||||
mods = [
|
||||
name
|
||||
for name in sys.modules
|
||||
if name == "distutils" or name.startswith("distutils.")
|
||||
]
|
||||
for name in mods:
|
||||
del sys.modules[name]
|
||||
|
||||
|
||||
def enabled():
|
||||
"""
|
||||
Allow selection of distutils by environment variable.
|
||||
"""
|
||||
which = os.environ.get('SETUPTOOLS_USE_DISTUTILS', 'local')
|
||||
return which == 'local'
|
||||
|
||||
|
||||
def ensure_local_distutils():
|
||||
import importlib
|
||||
|
||||
clear_distutils()
|
||||
|
||||
# With the DistutilsMetaFinder in place,
|
||||
# perform an import to cause distutils to be
|
||||
# loaded from setuptools._distutils. Ref #2906.
|
||||
with shim():
|
||||
importlib.import_module('distutils')
|
||||
|
||||
# check that submodules load as expected
|
||||
core = importlib.import_module('distutils.core')
|
||||
assert '_distutils' in core.__file__, core.__file__
|
||||
assert 'setuptools._distutils.log' not in sys.modules
|
||||
|
||||
|
||||
def do_override():
|
||||
"""
|
||||
Ensure that the local copy of distutils is preferred over stdlib.
|
||||
|
||||
See https://github.com/pypa/setuptools/issues/417#issuecomment-392298401
|
||||
for more motivation.
|
||||
"""
|
||||
if enabled():
|
||||
warn_distutils_present()
|
||||
ensure_local_distutils()
|
||||
|
||||
|
||||
class _TrivialRe:
|
||||
def __init__(self, *patterns):
|
||||
self._patterns = patterns
|
||||
|
||||
def match(self, string):
|
||||
return all(pat in string for pat in self._patterns)
|
||||
|
||||
|
||||
class DistutilsMetaFinder:
|
||||
def find_spec(self, fullname, path, target=None):
|
||||
# optimization: only consider top level modules and those
|
||||
# found in the CPython test suite.
|
||||
if path is not None and not fullname.startswith('test.'):
|
||||
return
|
||||
|
||||
method_name = 'spec_for_{fullname}'.format(**locals())
|
||||
method = getattr(self, method_name, lambda: None)
|
||||
return method()
|
||||
|
||||
def spec_for_distutils(self):
|
||||
if self.is_cpython():
|
||||
return
|
||||
|
||||
import importlib
|
||||
import importlib.abc
|
||||
import importlib.util
|
||||
|
||||
try:
|
||||
mod = importlib.import_module('setuptools._distutils')
|
||||
except Exception:
|
||||
# There are a couple of cases where setuptools._distutils
|
||||
# may not be present:
|
||||
# - An older Setuptools without a local distutils is
|
||||
# taking precedence. Ref #2957.
|
||||
# - Path manipulation during sitecustomize removes
|
||||
# setuptools from the path but only after the hook
|
||||
# has been loaded. Ref #2980.
|
||||
# In either case, fall back to stdlib behavior.
|
||||
return
|
||||
|
||||
class DistutilsLoader(importlib.abc.Loader):
|
||||
def create_module(self, spec):
|
||||
mod.__name__ = 'distutils'
|
||||
return mod
|
||||
|
||||
def exec_module(self, module):
|
||||
pass
|
||||
|
||||
return importlib.util.spec_from_loader(
|
||||
'distutils', DistutilsLoader(), origin=mod.__file__
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def is_cpython():
|
||||
"""
|
||||
Suppress supplying distutils for CPython (build and tests).
|
||||
Ref #2965 and #3007.
|
||||
"""
|
||||
return os.path.isfile('pybuilddir.txt')
|
||||
|
||||
def spec_for_pip(self):
|
||||
"""
|
||||
Ensure stdlib distutils when running under pip.
|
||||
See pypa/pip#8761 for rationale.
|
||||
"""
|
||||
if self.pip_imported_during_build():
|
||||
return
|
||||
clear_distutils()
|
||||
self.spec_for_distutils = lambda: None
|
||||
|
||||
@classmethod
|
||||
def pip_imported_during_build(cls):
|
||||
"""
|
||||
Detect if pip is being imported in a build script. Ref #2355.
|
||||
"""
|
||||
import traceback
|
||||
|
||||
return any(
|
||||
cls.frame_file_is_setup(frame) for frame, line in traceback.walk_stack(None)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def frame_file_is_setup(frame):
|
||||
"""
|
||||
Return True if the indicated frame suggests a setup.py file.
|
||||
"""
|
||||
# some frames may not have __file__ (#2940)
|
||||
return frame.f_globals.get('__file__', '').endswith('setup.py')
|
||||
|
||||
def spec_for_sensitive_tests(self):
|
||||
"""
|
||||
Ensure stdlib distutils when running select tests under CPython.
|
||||
|
||||
python/cpython#91169
|
||||
"""
|
||||
clear_distutils()
|
||||
self.spec_for_distutils = lambda: None
|
||||
|
||||
sensitive_tests = (
|
||||
[
|
||||
'test.test_distutils',
|
||||
'test.test_peg_generator',
|
||||
'test.test_importlib',
|
||||
]
|
||||
if sys.version_info < (3, 10)
|
||||
else [
|
||||
'test.test_distutils',
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
for name in DistutilsMetaFinder.sensitive_tests:
|
||||
setattr(
|
||||
DistutilsMetaFinder,
|
||||
f'spec_for_{name}',
|
||||
DistutilsMetaFinder.spec_for_sensitive_tests,
|
||||
)
|
||||
|
||||
|
||||
DISTUTILS_FINDER = DistutilsMetaFinder()
|
||||
|
||||
|
||||
def add_shim():
|
||||
DISTUTILS_FINDER in sys.meta_path or insert_shim()
|
||||
|
||||
|
||||
class shim:
|
||||
def __enter__(self):
|
||||
insert_shim()
|
||||
|
||||
def __exit__(self, exc, value, tb):
|
||||
remove_shim()
|
||||
|
||||
|
||||
def insert_shim():
|
||||
sys.meta_path.insert(0, DISTUTILS_FINDER)
|
||||
|
||||
|
||||
def remove_shim():
|
||||
try:
|
||||
sys.meta_path.remove(DISTUTILS_FINDER)
|
||||
except ValueError:
|
||||
pass
|
||||
Binary file not shown.
Binary file not shown.
1
venv/Lib/site-packages/_distutils_hack/override.py
Normal file
1
venv/Lib/site-packages/_distutils_hack/override.py
Normal file
@@ -0,0 +1 @@
|
||||
__import__('_distutils_hack').do_override()
|
||||
1
venv/Lib/site-packages/blinker-1.9.0.dist-info/INSTALLER
Normal file
1
venv/Lib/site-packages/blinker-1.9.0.dist-info/INSTALLER
Normal file
@@ -0,0 +1 @@
|
||||
pip
|
||||
20
venv/Lib/site-packages/blinker-1.9.0.dist-info/LICENSE.txt
Normal file
20
venv/Lib/site-packages/blinker-1.9.0.dist-info/LICENSE.txt
Normal file
@@ -0,0 +1,20 @@
|
||||
Copyright 2010 Jason Kirtland
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
60
venv/Lib/site-packages/blinker-1.9.0.dist-info/METADATA
Normal file
60
venv/Lib/site-packages/blinker-1.9.0.dist-info/METADATA
Normal file
@@ -0,0 +1,60 @@
|
||||
Metadata-Version: 2.3
|
||||
Name: blinker
|
||||
Version: 1.9.0
|
||||
Summary: Fast, simple object-to-object and broadcast signaling
|
||||
Author: Jason Kirtland
|
||||
Maintainer-email: Pallets Ecosystem <contact@palletsprojects.com>
|
||||
Requires-Python: >=3.9
|
||||
Description-Content-Type: text/markdown
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Typing :: Typed
|
||||
Project-URL: Chat, https://discord.gg/pallets
|
||||
Project-URL: Documentation, https://blinker.readthedocs.io
|
||||
Project-URL: Source, https://github.com/pallets-eco/blinker/
|
||||
|
||||
# Blinker
|
||||
|
||||
Blinker provides a fast dispatching system that allows any number of
|
||||
interested parties to subscribe to events, or "signals".
|
||||
|
||||
|
||||
## Pallets Community Ecosystem
|
||||
|
||||
> [!IMPORTANT]\
|
||||
> This project is part of the Pallets Community Ecosystem. Pallets is the open
|
||||
> source organization that maintains Flask; Pallets-Eco enables community
|
||||
> maintenance of related projects. If you are interested in helping maintain
|
||||
> this project, please reach out on [the Pallets Discord server][discord].
|
||||
>
|
||||
> [discord]: https://discord.gg/pallets
|
||||
|
||||
|
||||
## Example
|
||||
|
||||
Signal receivers can subscribe to specific senders or receive signals
|
||||
sent by any sender.
|
||||
|
||||
```pycon
|
||||
>>> from blinker import signal
|
||||
>>> started = signal('round-started')
|
||||
>>> def each(round):
|
||||
... print(f"Round {round}")
|
||||
...
|
||||
>>> started.connect(each)
|
||||
|
||||
>>> def round_two(round):
|
||||
... print("This is round two.")
|
||||
...
|
||||
>>> started.connect(round_two, sender=2)
|
||||
|
||||
>>> for round in range(1, 4):
|
||||
... started.send(round)
|
||||
...
|
||||
Round 1!
|
||||
Round 2!
|
||||
This is round two.
|
||||
Round 3!
|
||||
```
|
||||
|
||||
12
venv/Lib/site-packages/blinker-1.9.0.dist-info/RECORD
Normal file
12
venv/Lib/site-packages/blinker-1.9.0.dist-info/RECORD
Normal file
@@ -0,0 +1,12 @@
|
||||
blinker-1.9.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
blinker-1.9.0.dist-info/LICENSE.txt,sha256=nrc6HzhZekqhcCXSrhvjg5Ykx5XphdTw6Xac4p-spGc,1054
|
||||
blinker-1.9.0.dist-info/METADATA,sha256=uIRiM8wjjbHkCtbCyTvctU37IAZk0kEe5kxAld1dvzA,1633
|
||||
blinker-1.9.0.dist-info/RECORD,,
|
||||
blinker-1.9.0.dist-info/WHEEL,sha256=CpUCUxeHQbRN5UGRQHYRJorO5Af-Qy_fHMctcQ8DSGI,82
|
||||
blinker/__init__.py,sha256=I2EdZqpy4LyjX17Hn1yzJGWCjeLaVaPzsMgHkLfj_cQ,317
|
||||
blinker/__pycache__/__init__.cpython-310.pyc,,
|
||||
blinker/__pycache__/_utilities.cpython-310.pyc,,
|
||||
blinker/__pycache__/base.cpython-310.pyc,,
|
||||
blinker/_utilities.py,sha256=0J7eeXXTUx0Ivf8asfpx0ycVkp0Eqfqnj117x2mYX9E,1675
|
||||
blinker/base.py,sha256=QpDuvXXcwJF49lUBcH5BiST46Rz9wSG7VW_p7N_027M,19132
|
||||
blinker/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
4
venv/Lib/site-packages/blinker-1.9.0.dist-info/WHEEL
Normal file
4
venv/Lib/site-packages/blinker-1.9.0.dist-info/WHEEL
Normal file
@@ -0,0 +1,4 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: flit 3.10.1
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
17
venv/Lib/site-packages/blinker/__init__.py
Normal file
17
venv/Lib/site-packages/blinker/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .base import ANY
|
||||
from .base import default_namespace
|
||||
from .base import NamedSignal
|
||||
from .base import Namespace
|
||||
from .base import Signal
|
||||
from .base import signal
|
||||
|
||||
__all__ = [
|
||||
"ANY",
|
||||
"default_namespace",
|
||||
"NamedSignal",
|
||||
"Namespace",
|
||||
"Signal",
|
||||
"signal",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
BIN
venv/Lib/site-packages/blinker/__pycache__/base.cpython-310.pyc
Normal file
BIN
venv/Lib/site-packages/blinker/__pycache__/base.cpython-310.pyc
Normal file
Binary file not shown.
64
venv/Lib/site-packages/blinker/_utilities.py
Normal file
64
venv/Lib/site-packages/blinker/_utilities.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import collections.abc as c
|
||||
import inspect
|
||||
import typing as t
|
||||
from weakref import ref
|
||||
from weakref import WeakMethod
|
||||
|
||||
T = t.TypeVar("T")
|
||||
|
||||
|
||||
class Symbol:
|
||||
"""A constant symbol, nicer than ``object()``. Repeated calls return the
|
||||
same instance.
|
||||
|
||||
>>> Symbol('foo') is Symbol('foo')
|
||||
True
|
||||
>>> Symbol('foo')
|
||||
foo
|
||||
"""
|
||||
|
||||
symbols: t.ClassVar[dict[str, Symbol]] = {}
|
||||
|
||||
def __new__(cls, name: str) -> Symbol:
|
||||
if name in cls.symbols:
|
||||
return cls.symbols[name]
|
||||
|
||||
obj = super().__new__(cls)
|
||||
cls.symbols[name] = obj
|
||||
return obj
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
self.name = name
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.name
|
||||
|
||||
def __getnewargs__(self) -> tuple[t.Any, ...]:
|
||||
return (self.name,)
|
||||
|
||||
|
||||
def make_id(obj: object) -> c.Hashable:
|
||||
"""Get a stable identifier for a receiver or sender, to be used as a dict
|
||||
key or in a set.
|
||||
"""
|
||||
if inspect.ismethod(obj):
|
||||
# The id of a bound method is not stable, but the id of the unbound
|
||||
# function and instance are.
|
||||
return id(obj.__func__), id(obj.__self__)
|
||||
|
||||
if isinstance(obj, (str, int)):
|
||||
# Instances with the same value always compare equal and have the same
|
||||
# hash, even if the id may change.
|
||||
return obj
|
||||
|
||||
# Assume other types are not hashable but will always be the same instance.
|
||||
return id(obj)
|
||||
|
||||
|
||||
def make_ref(obj: T, callback: c.Callable[[ref[T]], None] | None = None) -> ref[T]:
|
||||
if inspect.ismethod(obj):
|
||||
return WeakMethod(obj, callback) # type: ignore[arg-type, return-value]
|
||||
|
||||
return ref(obj, callback)
|
||||
512
venv/Lib/site-packages/blinker/base.py
Normal file
512
venv/Lib/site-packages/blinker/base.py
Normal file
@@ -0,0 +1,512 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import collections.abc as c
|
||||
import sys
|
||||
import typing as t
|
||||
import weakref
|
||||
from collections import defaultdict
|
||||
from contextlib import contextmanager
|
||||
from functools import cached_property
|
||||
from inspect import iscoroutinefunction
|
||||
|
||||
from ._utilities import make_id
|
||||
from ._utilities import make_ref
|
||||
from ._utilities import Symbol
|
||||
|
||||
F = t.TypeVar("F", bound=c.Callable[..., t.Any])
|
||||
|
||||
ANY = Symbol("ANY")
|
||||
"""Symbol for "any sender"."""
|
||||
|
||||
ANY_ID = 0
|
||||
|
||||
|
||||
class Signal:
|
||||
"""A notification emitter.
|
||||
|
||||
:param doc: The docstring for the signal.
|
||||
"""
|
||||
|
||||
ANY = ANY
|
||||
"""An alias for the :data:`~blinker.ANY` sender symbol."""
|
||||
|
||||
set_class: type[set[t.Any]] = set
|
||||
"""The set class to use for tracking connected receivers and senders.
|
||||
Python's ``set`` is unordered. If receivers must be dispatched in the order
|
||||
they were connected, an ordered set implementation can be used.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
|
||||
@cached_property
|
||||
def receiver_connected(self) -> Signal:
|
||||
"""Emitted at the end of each :meth:`connect` call.
|
||||
|
||||
The signal sender is the signal instance, and the :meth:`connect`
|
||||
arguments are passed through: ``receiver``, ``sender``, and ``weak``.
|
||||
|
||||
.. versionadded:: 1.2
|
||||
"""
|
||||
return Signal(doc="Emitted after a receiver connects.")
|
||||
|
||||
@cached_property
|
||||
def receiver_disconnected(self) -> Signal:
|
||||
"""Emitted at the end of each :meth:`disconnect` call.
|
||||
|
||||
The sender is the signal instance, and the :meth:`disconnect` arguments
|
||||
are passed through: ``receiver`` and ``sender``.
|
||||
|
||||
This signal is emitted **only** when :meth:`disconnect` is called
|
||||
explicitly. This signal cannot be emitted by an automatic disconnect
|
||||
when a weakly referenced receiver or sender goes out of scope, as the
|
||||
instance is no longer be available to be used as the sender for this
|
||||
signal.
|
||||
|
||||
An alternative approach is available by subscribing to
|
||||
:attr:`receiver_connected` and setting up a custom weakref cleanup
|
||||
callback on weak receivers and senders.
|
||||
|
||||
.. versionadded:: 1.2
|
||||
"""
|
||||
return Signal(doc="Emitted after a receiver disconnects.")
|
||||
|
||||
def __init__(self, doc: str | None = None) -> None:
|
||||
if doc:
|
||||
self.__doc__ = doc
|
||||
|
||||
self.receivers: dict[
|
||||
t.Any, weakref.ref[c.Callable[..., t.Any]] | c.Callable[..., t.Any]
|
||||
] = {}
|
||||
"""The map of connected receivers. Useful to quickly check if any
|
||||
receivers are connected to the signal: ``if s.receivers:``. The
|
||||
structure and data is not part of the public API, but checking its
|
||||
boolean value is.
|
||||
"""
|
||||
|
||||
self.is_muted: bool = False
|
||||
self._by_receiver: dict[t.Any, set[t.Any]] = defaultdict(self.set_class)
|
||||
self._by_sender: dict[t.Any, set[t.Any]] = defaultdict(self.set_class)
|
||||
self._weak_senders: dict[t.Any, weakref.ref[t.Any]] = {}
|
||||
|
||||
def connect(self, receiver: F, sender: t.Any = ANY, weak: bool = True) -> F:
|
||||
"""Connect ``receiver`` to be called when the signal is sent by
|
||||
``sender``.
|
||||
|
||||
:param receiver: The callable to call when :meth:`send` is called with
|
||||
the given ``sender``, passing ``sender`` as a positional argument
|
||||
along with any extra keyword arguments.
|
||||
:param sender: Any object or :data:`ANY`. ``receiver`` will only be
|
||||
called when :meth:`send` is called with this sender. If ``ANY``, the
|
||||
receiver will be called for any sender. A receiver may be connected
|
||||
to multiple senders by calling :meth:`connect` multiple times.
|
||||
:param weak: Track the receiver with a :mod:`weakref`. The receiver will
|
||||
be automatically disconnected when it is garbage collected. When
|
||||
connecting a receiver defined within a function, set to ``False``,
|
||||
otherwise it will be disconnected when the function scope ends.
|
||||
"""
|
||||
receiver_id = make_id(receiver)
|
||||
sender_id = ANY_ID if sender is ANY else make_id(sender)
|
||||
|
||||
if weak:
|
||||
self.receivers[receiver_id] = make_ref(
|
||||
receiver, self._make_cleanup_receiver(receiver_id)
|
||||
)
|
||||
else:
|
||||
self.receivers[receiver_id] = receiver
|
||||
|
||||
self._by_sender[sender_id].add(receiver_id)
|
||||
self._by_receiver[receiver_id].add(sender_id)
|
||||
|
||||
if sender is not ANY and sender_id not in self._weak_senders:
|
||||
# store a cleanup for weakref-able senders
|
||||
try:
|
||||
self._weak_senders[sender_id] = make_ref(
|
||||
sender, self._make_cleanup_sender(sender_id)
|
||||
)
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
if "receiver_connected" in self.__dict__ and self.receiver_connected.receivers:
|
||||
try:
|
||||
self.receiver_connected.send(
|
||||
self, receiver=receiver, sender=sender, weak=weak
|
||||
)
|
||||
except TypeError:
|
||||
# TODO no explanation or test for this
|
||||
self.disconnect(receiver, sender)
|
||||
raise
|
||||
|
||||
return receiver
|
||||
|
||||
def connect_via(self, sender: t.Any, weak: bool = False) -> c.Callable[[F], F]:
|
||||
"""Connect the decorated function to be called when the signal is sent
|
||||
by ``sender``.
|
||||
|
||||
The decorated function will be called when :meth:`send` is called with
|
||||
the given ``sender``, passing ``sender`` as a positional argument along
|
||||
with any extra keyword arguments.
|
||||
|
||||
:param sender: Any object or :data:`ANY`. ``receiver`` will only be
|
||||
called when :meth:`send` is called with this sender. If ``ANY``, the
|
||||
receiver will be called for any sender. A receiver may be connected
|
||||
to multiple senders by calling :meth:`connect` multiple times.
|
||||
:param weak: Track the receiver with a :mod:`weakref`. The receiver will
|
||||
be automatically disconnected when it is garbage collected. When
|
||||
connecting a receiver defined within a function, set to ``False``,
|
||||
otherwise it will be disconnected when the function scope ends.=
|
||||
|
||||
.. versionadded:: 1.1
|
||||
"""
|
||||
|
||||
def decorator(fn: F) -> F:
|
||||
self.connect(fn, sender, weak)
|
||||
return fn
|
||||
|
||||
return decorator
|
||||
|
||||
@contextmanager
|
||||
def connected_to(
|
||||
self, receiver: c.Callable[..., t.Any], sender: t.Any = ANY
|
||||
) -> c.Generator[None, None, None]:
|
||||
"""A context manager that temporarily connects ``receiver`` to the
|
||||
signal while a ``with`` block executes. When the block exits, the
|
||||
receiver is disconnected. Useful for tests.
|
||||
|
||||
:param receiver: The callable to call when :meth:`send` is called with
|
||||
the given ``sender``, passing ``sender`` as a positional argument
|
||||
along with any extra keyword arguments.
|
||||
:param sender: Any object or :data:`ANY`. ``receiver`` will only be
|
||||
called when :meth:`send` is called with this sender. If ``ANY``, the
|
||||
receiver will be called for any sender.
|
||||
|
||||
.. versionadded:: 1.1
|
||||
"""
|
||||
self.connect(receiver, sender=sender, weak=False)
|
||||
|
||||
try:
|
||||
yield None
|
||||
finally:
|
||||
self.disconnect(receiver)
|
||||
|
||||
@contextmanager
|
||||
def muted(self) -> c.Generator[None, None, None]:
|
||||
"""A context manager that temporarily disables the signal. No receivers
|
||||
will be called if the signal is sent, until the ``with`` block exits.
|
||||
Useful for tests.
|
||||
"""
|
||||
self.is_muted = True
|
||||
|
||||
try:
|
||||
yield None
|
||||
finally:
|
||||
self.is_muted = False
|
||||
|
||||
def send(
|
||||
self,
|
||||
sender: t.Any | None = None,
|
||||
/,
|
||||
*,
|
||||
_async_wrapper: c.Callable[
|
||||
[c.Callable[..., c.Coroutine[t.Any, t.Any, t.Any]]], c.Callable[..., t.Any]
|
||||
]
|
||||
| None = None,
|
||||
**kwargs: t.Any,
|
||||
) -> list[tuple[c.Callable[..., t.Any], t.Any]]:
|
||||
"""Call all receivers that are connected to the given ``sender``
|
||||
or :data:`ANY`. Each receiver is called with ``sender`` as a positional
|
||||
argument along with any extra keyword arguments. Return a list of
|
||||
``(receiver, return value)`` tuples.
|
||||
|
||||
The order receivers are called is undefined, but can be influenced by
|
||||
setting :attr:`set_class`.
|
||||
|
||||
If a receiver raises an exception, that exception will propagate up.
|
||||
This makes debugging straightforward, with an assumption that correctly
|
||||
implemented receivers will not raise.
|
||||
|
||||
:param sender: Call receivers connected to this sender, in addition to
|
||||
those connected to :data:`ANY`.
|
||||
:param _async_wrapper: Will be called on any receivers that are async
|
||||
coroutines to turn them into sync callables. For example, could run
|
||||
the receiver with an event loop.
|
||||
:param kwargs: Extra keyword arguments to pass to each receiver.
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
Added the ``_async_wrapper`` argument.
|
||||
"""
|
||||
if self.is_muted:
|
||||
return []
|
||||
|
||||
results = []
|
||||
|
||||
for receiver in self.receivers_for(sender):
|
||||
if iscoroutinefunction(receiver):
|
||||
if _async_wrapper is None:
|
||||
raise RuntimeError("Cannot send to a coroutine function.")
|
||||
|
||||
result = _async_wrapper(receiver)(sender, **kwargs)
|
||||
else:
|
||||
result = receiver(sender, **kwargs)
|
||||
|
||||
results.append((receiver, result))
|
||||
|
||||
return results
|
||||
|
||||
async def send_async(
|
||||
self,
|
||||
sender: t.Any | None = None,
|
||||
/,
|
||||
*,
|
||||
_sync_wrapper: c.Callable[
|
||||
[c.Callable[..., t.Any]], c.Callable[..., c.Coroutine[t.Any, t.Any, t.Any]]
|
||||
]
|
||||
| None = None,
|
||||
**kwargs: t.Any,
|
||||
) -> list[tuple[c.Callable[..., t.Any], t.Any]]:
|
||||
"""Await all receivers that are connected to the given ``sender``
|
||||
or :data:`ANY`. Each receiver is called with ``sender`` as a positional
|
||||
argument along with any extra keyword arguments. Return a list of
|
||||
``(receiver, return value)`` tuples.
|
||||
|
||||
The order receivers are called is undefined, but can be influenced by
|
||||
setting :attr:`set_class`.
|
||||
|
||||
If a receiver raises an exception, that exception will propagate up.
|
||||
This makes debugging straightforward, with an assumption that correctly
|
||||
implemented receivers will not raise.
|
||||
|
||||
:param sender: Call receivers connected to this sender, in addition to
|
||||
those connected to :data:`ANY`.
|
||||
:param _sync_wrapper: Will be called on any receivers that are sync
|
||||
callables to turn them into async coroutines. For example,
|
||||
could call the receiver in a thread.
|
||||
:param kwargs: Extra keyword arguments to pass to each receiver.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
if self.is_muted:
|
||||
return []
|
||||
|
||||
results = []
|
||||
|
||||
for receiver in self.receivers_for(sender):
|
||||
if not iscoroutinefunction(receiver):
|
||||
if _sync_wrapper is None:
|
||||
raise RuntimeError("Cannot send to a non-coroutine function.")
|
||||
|
||||
result = await _sync_wrapper(receiver)(sender, **kwargs)
|
||||
else:
|
||||
result = await receiver(sender, **kwargs)
|
||||
|
||||
results.append((receiver, result))
|
||||
|
||||
return results
|
||||
|
||||
def has_receivers_for(self, sender: t.Any) -> bool:
|
||||
"""Check if there is at least one receiver that will be called with the
|
||||
given ``sender``. A receiver connected to :data:`ANY` will always be
|
||||
called, regardless of sender. Does not check if weakly referenced
|
||||
receivers are still live. See :meth:`receivers_for` for a stronger
|
||||
search.
|
||||
|
||||
:param sender: Check for receivers connected to this sender, in addition
|
||||
to those connected to :data:`ANY`.
|
||||
"""
|
||||
if not self.receivers:
|
||||
return False
|
||||
|
||||
if self._by_sender[ANY_ID]:
|
||||
return True
|
||||
|
||||
if sender is ANY:
|
||||
return False
|
||||
|
||||
return make_id(sender) in self._by_sender
|
||||
|
||||
def receivers_for(
|
||||
self, sender: t.Any
|
||||
) -> c.Generator[c.Callable[..., t.Any], None, None]:
|
||||
"""Yield each receiver to be called for ``sender``, in addition to those
|
||||
to be called for :data:`ANY`. Weakly referenced receivers that are not
|
||||
live will be disconnected and skipped.
|
||||
|
||||
:param sender: Yield receivers connected to this sender, in addition
|
||||
to those connected to :data:`ANY`.
|
||||
"""
|
||||
# TODO: test receivers_for(ANY)
|
||||
if not self.receivers:
|
||||
return
|
||||
|
||||
sender_id = make_id(sender)
|
||||
|
||||
if sender_id in self._by_sender:
|
||||
ids = self._by_sender[ANY_ID] | self._by_sender[sender_id]
|
||||
else:
|
||||
ids = self._by_sender[ANY_ID].copy()
|
||||
|
||||
for receiver_id in ids:
|
||||
receiver = self.receivers.get(receiver_id)
|
||||
|
||||
if receiver is None:
|
||||
continue
|
||||
|
||||
if isinstance(receiver, weakref.ref):
|
||||
strong = receiver()
|
||||
|
||||
if strong is None:
|
||||
self._disconnect(receiver_id, ANY_ID)
|
||||
continue
|
||||
|
||||
yield strong
|
||||
else:
|
||||
yield receiver
|
||||
|
||||
def disconnect(self, receiver: c.Callable[..., t.Any], sender: t.Any = ANY) -> None:
|
||||
"""Disconnect ``receiver`` from being called when the signal is sent by
|
||||
``sender``.
|
||||
|
||||
:param receiver: A connected receiver callable.
|
||||
:param sender: Disconnect from only this sender. By default, disconnect
|
||||
from all senders.
|
||||
"""
|
||||
sender_id: c.Hashable
|
||||
|
||||
if sender is ANY:
|
||||
sender_id = ANY_ID
|
||||
else:
|
||||
sender_id = make_id(sender)
|
||||
|
||||
receiver_id = make_id(receiver)
|
||||
self._disconnect(receiver_id, sender_id)
|
||||
|
||||
if (
|
||||
"receiver_disconnected" in self.__dict__
|
||||
and self.receiver_disconnected.receivers
|
||||
):
|
||||
self.receiver_disconnected.send(self, receiver=receiver, sender=sender)
|
||||
|
||||
def _disconnect(self, receiver_id: c.Hashable, sender_id: c.Hashable) -> None:
|
||||
if sender_id == ANY_ID:
|
||||
if self._by_receiver.pop(receiver_id, None) is not None:
|
||||
for bucket in self._by_sender.values():
|
||||
bucket.discard(receiver_id)
|
||||
|
||||
self.receivers.pop(receiver_id, None)
|
||||
else:
|
||||
self._by_sender[sender_id].discard(receiver_id)
|
||||
self._by_receiver[receiver_id].discard(sender_id)
|
||||
|
||||
def _make_cleanup_receiver(
|
||||
self, receiver_id: c.Hashable
|
||||
) -> c.Callable[[weakref.ref[c.Callable[..., t.Any]]], None]:
|
||||
"""Create a callback function to disconnect a weakly referenced
|
||||
receiver when it is garbage collected.
|
||||
"""
|
||||
|
||||
def cleanup(ref: weakref.ref[c.Callable[..., t.Any]]) -> None:
|
||||
# If the interpreter is shutting down, disconnecting can result in a
|
||||
# weird ignored exception. Don't call it in that case.
|
||||
if not sys.is_finalizing():
|
||||
self._disconnect(receiver_id, ANY_ID)
|
||||
|
||||
return cleanup
|
||||
|
||||
def _make_cleanup_sender(
|
||||
self, sender_id: c.Hashable
|
||||
) -> c.Callable[[weakref.ref[t.Any]], None]:
|
||||
"""Create a callback function to disconnect all receivers for a weakly
|
||||
referenced sender when it is garbage collected.
|
||||
"""
|
||||
assert sender_id != ANY_ID
|
||||
|
||||
def cleanup(ref: weakref.ref[t.Any]) -> None:
|
||||
self._weak_senders.pop(sender_id, None)
|
||||
|
||||
for receiver_id in self._by_sender.pop(sender_id, ()):
|
||||
self._by_receiver[receiver_id].discard(sender_id)
|
||||
|
||||
return cleanup
|
||||
|
||||
def _cleanup_bookkeeping(self) -> None:
|
||||
"""Prune unused sender/receiver bookkeeping. Not threadsafe.
|
||||
|
||||
Connecting & disconnecting leaves behind a small amount of bookkeeping
|
||||
data. Typical workloads using Blinker, for example in most web apps,
|
||||
Flask, CLI scripts, etc., are not adversely affected by this
|
||||
bookkeeping.
|
||||
|
||||
With a long-running process performing dynamic signal routing with high
|
||||
volume, e.g. connecting to function closures, senders are all unique
|
||||
object instances. Doing all of this over and over may cause memory usage
|
||||
to grow due to extraneous bookkeeping. (An empty ``set`` for each stale
|
||||
sender/receiver pair.)
|
||||
|
||||
This method will prune that bookkeeping away, with the caveat that such
|
||||
pruning is not threadsafe. The risk is that cleanup of a fully
|
||||
disconnected receiver/sender pair occurs while another thread is
|
||||
connecting that same pair. If you are in the highly dynamic, unique
|
||||
receiver/sender situation that has lead you to this method, that failure
|
||||
mode is perhaps not a big deal for you.
|
||||
"""
|
||||
for mapping in (self._by_sender, self._by_receiver):
|
||||
for ident, bucket in list(mapping.items()):
|
||||
if not bucket:
|
||||
mapping.pop(ident, None)
|
||||
|
||||
def _clear_state(self) -> None:
|
||||
"""Disconnect all receivers and senders. Useful for tests."""
|
||||
self._weak_senders.clear()
|
||||
self.receivers.clear()
|
||||
self._by_sender.clear()
|
||||
self._by_receiver.clear()
|
||||
|
||||
|
||||
class NamedSignal(Signal):
|
||||
"""A named generic notification emitter. The name is not used by the signal
|
||||
itself, but matches the key in the :class:`Namespace` that it belongs to.
|
||||
|
||||
:param name: The name of the signal within the namespace.
|
||||
:param doc: The docstring for the signal.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str, doc: str | None = None) -> None:
|
||||
super().__init__(doc)
|
||||
|
||||
#: The name of this signal.
|
||||
self.name: str = name
|
||||
|
||||
def __repr__(self) -> str:
|
||||
base = super().__repr__()
|
||||
return f"{base[:-1]}; {self.name!r}>" # noqa: E702
|
||||
|
||||
|
||||
class Namespace(dict[str, NamedSignal]):
|
||||
"""A dict mapping names to signals."""
|
||||
|
||||
def signal(self, name: str, doc: str | None = None) -> NamedSignal:
|
||||
"""Return the :class:`NamedSignal` for the given ``name``, creating it
|
||||
if required. Repeated calls with the same name return the same signal.
|
||||
|
||||
:param name: The name of the signal.
|
||||
:param doc: The docstring of the signal.
|
||||
"""
|
||||
if name not in self:
|
||||
self[name] = NamedSignal(name, doc)
|
||||
|
||||
return self[name]
|
||||
|
||||
|
||||
class _PNamespaceSignal(t.Protocol):
|
||||
def __call__(self, name: str, doc: str | None = None) -> NamedSignal: ...
|
||||
|
||||
|
||||
default_namespace: Namespace = Namespace()
|
||||
"""A default :class:`Namespace` for creating named signals. :func:`signal`
|
||||
creates a :class:`NamedSignal` in this namespace.
|
||||
"""
|
||||
|
||||
signal: _PNamespaceSignal = default_namespace.signal
|
||||
"""Return a :class:`NamedSignal` in :data:`default_namespace` with the given
|
||||
``name``, creating it if required. Repeated calls with the same name return the
|
||||
same signal.
|
||||
"""
|
||||
0
venv/Lib/site-packages/blinker/py.typed
Normal file
0
venv/Lib/site-packages/blinker/py.typed
Normal file
1
venv/Lib/site-packages/click-8.2.1.dist-info/INSTALLER
Normal file
1
venv/Lib/site-packages/click-8.2.1.dist-info/INSTALLER
Normal file
@@ -0,0 +1 @@
|
||||
pip
|
||||
82
venv/Lib/site-packages/click-8.2.1.dist-info/METADATA
Normal file
82
venv/Lib/site-packages/click-8.2.1.dist-info/METADATA
Normal file
@@ -0,0 +1,82 @@
|
||||
Metadata-Version: 2.4
|
||||
Name: click
|
||||
Version: 8.2.1
|
||||
Summary: Composable command line interface toolkit
|
||||
Maintainer-email: Pallets <contact@palletsprojects.com>
|
||||
Requires-Python: >=3.10
|
||||
Description-Content-Type: text/markdown
|
||||
License-Expression: BSD-3-Clause
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Typing :: Typed
|
||||
License-File: LICENSE.txt
|
||||
Requires-Dist: colorama; platform_system == 'Windows'
|
||||
Project-URL: Changes, https://click.palletsprojects.com/page/changes/
|
||||
Project-URL: Chat, https://discord.gg/pallets
|
||||
Project-URL: Documentation, https://click.palletsprojects.com/
|
||||
Project-URL: Donate, https://palletsprojects.com/donate
|
||||
Project-URL: Source, https://github.com/pallets/click/
|
||||
|
||||
# $ click_
|
||||
|
||||
Click is a Python package for creating beautiful command line interfaces
|
||||
in a composable way with as little code as necessary. It's the "Command
|
||||
Line Interface Creation Kit". It's highly configurable but comes with
|
||||
sensible defaults out of the box.
|
||||
|
||||
It aims to make the process of writing command line tools quick and fun
|
||||
while also preventing any frustration caused by the inability to
|
||||
implement an intended CLI API.
|
||||
|
||||
Click in three points:
|
||||
|
||||
- Arbitrary nesting of commands
|
||||
- Automatic help page generation
|
||||
- Supports lazy loading of subcommands at runtime
|
||||
|
||||
|
||||
## A Simple Example
|
||||
|
||||
```python
|
||||
import click
|
||||
|
||||
@click.command()
|
||||
@click.option("--count", default=1, help="Number of greetings.")
|
||||
@click.option("--name", prompt="Your name", help="The person to greet.")
|
||||
def hello(count, name):
|
||||
"""Simple program that greets NAME for a total of COUNT times."""
|
||||
for _ in range(count):
|
||||
click.echo(f"Hello, {name}!")
|
||||
|
||||
if __name__ == '__main__':
|
||||
hello()
|
||||
```
|
||||
|
||||
```
|
||||
$ python hello.py --count=3
|
||||
Your name: Click
|
||||
Hello, Click!
|
||||
Hello, Click!
|
||||
Hello, Click!
|
||||
```
|
||||
|
||||
|
||||
## Donate
|
||||
|
||||
The Pallets organization develops and supports Click and other popular
|
||||
packages. In order to grow the community of contributors and users, and
|
||||
allow the maintainers to devote more time to the projects, [please
|
||||
donate today][].
|
||||
|
||||
[please donate today]: https://palletsprojects.com/donate
|
||||
|
||||
## Contributing
|
||||
|
||||
See our [detailed contributing documentation][contrib] for many ways to
|
||||
contribute, including reporting issues, requesting features, asking or answering
|
||||
questions, and making PRs.
|
||||
|
||||
[contrib]: https://palletsprojects.com/contributing/
|
||||
|
||||
38
venv/Lib/site-packages/click-8.2.1.dist-info/RECORD
Normal file
38
venv/Lib/site-packages/click-8.2.1.dist-info/RECORD
Normal file
@@ -0,0 +1,38 @@
|
||||
click-8.2.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
click-8.2.1.dist-info/METADATA,sha256=dI1MbhHTLoKD2tNCCGnx9rK2gok23HDNylFeLKdLSik,2471
|
||||
click-8.2.1.dist-info/RECORD,,
|
||||
click-8.2.1.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
||||
click-8.2.1.dist-info/licenses/LICENSE.txt,sha256=morRBqOU6FO_4h9C9OctWSgZoigF2ZG18ydQKSkrZY0,1475
|
||||
click/__init__.py,sha256=6YyS1aeyknZ0LYweWozNZy0A9nZ_11wmYIhv3cbQrYo,4473
|
||||
click/__pycache__/__init__.cpython-310.pyc,,
|
||||
click/__pycache__/_compat.cpython-310.pyc,,
|
||||
click/__pycache__/_termui_impl.cpython-310.pyc,,
|
||||
click/__pycache__/_textwrap.cpython-310.pyc,,
|
||||
click/__pycache__/_winconsole.cpython-310.pyc,,
|
||||
click/__pycache__/core.cpython-310.pyc,,
|
||||
click/__pycache__/decorators.cpython-310.pyc,,
|
||||
click/__pycache__/exceptions.cpython-310.pyc,,
|
||||
click/__pycache__/formatting.cpython-310.pyc,,
|
||||
click/__pycache__/globals.cpython-310.pyc,,
|
||||
click/__pycache__/parser.cpython-310.pyc,,
|
||||
click/__pycache__/shell_completion.cpython-310.pyc,,
|
||||
click/__pycache__/termui.cpython-310.pyc,,
|
||||
click/__pycache__/testing.cpython-310.pyc,,
|
||||
click/__pycache__/types.cpython-310.pyc,,
|
||||
click/__pycache__/utils.cpython-310.pyc,,
|
||||
click/_compat.py,sha256=v3xBZkFbvA1BXPRkFfBJc6-pIwPI7345m-kQEnpVAs4,18693
|
||||
click/_termui_impl.py,sha256=ASXhLi9IQIc0Js9KQSS-3-SLZcPet3VqysBf9WgbbpI,26712
|
||||
click/_textwrap.py,sha256=BOae0RQ6vg3FkNgSJyOoGzG1meGMxJ_ukWVZKx_v-0o,1400
|
||||
click/_winconsole.py,sha256=_vxUuUaxwBhoR0vUWCNuHY8VUefiMdCIyU2SXPqoF-A,8465
|
||||
click/core.py,sha256=gUhpNS9cFBGdEXXdisGVG-eRvGf49RTyFagxulqwdFw,117343
|
||||
click/decorators.py,sha256=5P7abhJtAQYp_KHgjUvhMv464ERwOzrv2enNknlwHyQ,18461
|
||||
click/exceptions.py,sha256=1rdtXgHJ1b3OjGkN-UpXB9t_HCBihJvh_DtpmLmwn9s,9891
|
||||
click/formatting.py,sha256=Bhqx4QXdKQ9W4WKknIwj5KPKFmtduGOuGq1yw_THLZ8,9726
|
||||
click/globals.py,sha256=gM-Nh6A4M0HB_SgkaF5M4ncGGMDHc_flHXu9_oh4GEU,1923
|
||||
click/parser.py,sha256=nU1Ah2p11q29ul1vNdU9swPo_PUuKrxU6YXToi71q1c,18979
|
||||
click/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
click/shell_completion.py,sha256=CQSGdjgun4ORbOZrXP0CVhEtPx4knsufOkRsDiK64cM,19857
|
||||
click/termui.py,sha256=vAYrKC2a7f_NfEIhAThEVYfa__ib5XQbTSCGtJlABRA,30847
|
||||
click/testing.py,sha256=2eLdAaCJCGToP5Tw-XN8JjrDb3wbJIfARxg3d0crW5M,18702
|
||||
click/types.py,sha256=KBTRxN28cR1VZ5mb9iJX98MQSw_p9SGzljqfEI8z5Tw,38389
|
||||
click/utils.py,sha256=b1Mm-usEDBHtEwcPltPIn3zSK4nw2KTp5GC7_oSTlLo,20245
|
||||
4
venv/Lib/site-packages/click-8.2.1.dist-info/WHEEL
Normal file
4
venv/Lib/site-packages/click-8.2.1.dist-info/WHEEL
Normal file
@@ -0,0 +1,4 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: flit 3.12.0
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
@@ -0,0 +1,28 @@
|
||||
Copyright 2014 Pallets
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
||||
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
123
venv/Lib/site-packages/click/__init__.py
Normal file
123
venv/Lib/site-packages/click/__init__.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""
|
||||
Click is a simple Python module inspired by the stdlib optparse to make
|
||||
writing command line scripts fun. Unlike other modules, it's based
|
||||
around a simple API that does not come with too much magic and is
|
||||
composable.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .core import Argument as Argument
|
||||
from .core import Command as Command
|
||||
from .core import CommandCollection as CommandCollection
|
||||
from .core import Context as Context
|
||||
from .core import Group as Group
|
||||
from .core import Option as Option
|
||||
from .core import Parameter as Parameter
|
||||
from .decorators import argument as argument
|
||||
from .decorators import command as command
|
||||
from .decorators import confirmation_option as confirmation_option
|
||||
from .decorators import group as group
|
||||
from .decorators import help_option as help_option
|
||||
from .decorators import make_pass_decorator as make_pass_decorator
|
||||
from .decorators import option as option
|
||||
from .decorators import pass_context as pass_context
|
||||
from .decorators import pass_obj as pass_obj
|
||||
from .decorators import password_option as password_option
|
||||
from .decorators import version_option as version_option
|
||||
from .exceptions import Abort as Abort
|
||||
from .exceptions import BadArgumentUsage as BadArgumentUsage
|
||||
from .exceptions import BadOptionUsage as BadOptionUsage
|
||||
from .exceptions import BadParameter as BadParameter
|
||||
from .exceptions import ClickException as ClickException
|
||||
from .exceptions import FileError as FileError
|
||||
from .exceptions import MissingParameter as MissingParameter
|
||||
from .exceptions import NoSuchOption as NoSuchOption
|
||||
from .exceptions import UsageError as UsageError
|
||||
from .formatting import HelpFormatter as HelpFormatter
|
||||
from .formatting import wrap_text as wrap_text
|
||||
from .globals import get_current_context as get_current_context
|
||||
from .termui import clear as clear
|
||||
from .termui import confirm as confirm
|
||||
from .termui import echo_via_pager as echo_via_pager
|
||||
from .termui import edit as edit
|
||||
from .termui import getchar as getchar
|
||||
from .termui import launch as launch
|
||||
from .termui import pause as pause
|
||||
from .termui import progressbar as progressbar
|
||||
from .termui import prompt as prompt
|
||||
from .termui import secho as secho
|
||||
from .termui import style as style
|
||||
from .termui import unstyle as unstyle
|
||||
from .types import BOOL as BOOL
|
||||
from .types import Choice as Choice
|
||||
from .types import DateTime as DateTime
|
||||
from .types import File as File
|
||||
from .types import FLOAT as FLOAT
|
||||
from .types import FloatRange as FloatRange
|
||||
from .types import INT as INT
|
||||
from .types import IntRange as IntRange
|
||||
from .types import ParamType as ParamType
|
||||
from .types import Path as Path
|
||||
from .types import STRING as STRING
|
||||
from .types import Tuple as Tuple
|
||||
from .types import UNPROCESSED as UNPROCESSED
|
||||
from .types import UUID as UUID
|
||||
from .utils import echo as echo
|
||||
from .utils import format_filename as format_filename
|
||||
from .utils import get_app_dir as get_app_dir
|
||||
from .utils import get_binary_stream as get_binary_stream
|
||||
from .utils import get_text_stream as get_text_stream
|
||||
from .utils import open_file as open_file
|
||||
|
||||
|
||||
def __getattr__(name: str) -> object:
|
||||
import warnings
|
||||
|
||||
if name == "BaseCommand":
|
||||
from .core import _BaseCommand
|
||||
|
||||
warnings.warn(
|
||||
"'BaseCommand' is deprecated and will be removed in Click 9.0. Use"
|
||||
" 'Command' instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return _BaseCommand
|
||||
|
||||
if name == "MultiCommand":
|
||||
from .core import _MultiCommand
|
||||
|
||||
warnings.warn(
|
||||
"'MultiCommand' is deprecated and will be removed in Click 9.0. Use"
|
||||
" 'Group' instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return _MultiCommand
|
||||
|
||||
if name == "OptionParser":
|
||||
from .parser import _OptionParser
|
||||
|
||||
warnings.warn(
|
||||
"'OptionParser' is deprecated and will be removed in Click 9.0. The"
|
||||
" old parser is available in 'optparse'.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return _OptionParser
|
||||
|
||||
if name == "__version__":
|
||||
import importlib.metadata
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
"The '__version__' attribute is deprecated and will be removed in"
|
||||
" Click 9.1. Use feature detection or"
|
||||
" 'importlib.metadata.version(\"click\")' instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return importlib.metadata.version("click")
|
||||
|
||||
raise AttributeError(name)
|
||||
Binary file not shown.
BIN
venv/Lib/site-packages/click/__pycache__/_compat.cpython-310.pyc
Normal file
BIN
venv/Lib/site-packages/click/__pycache__/_compat.cpython-310.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
venv/Lib/site-packages/click/__pycache__/core.cpython-310.pyc
Normal file
BIN
venv/Lib/site-packages/click/__pycache__/core.cpython-310.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
venv/Lib/site-packages/click/__pycache__/globals.cpython-310.pyc
Normal file
BIN
venv/Lib/site-packages/click/__pycache__/globals.cpython-310.pyc
Normal file
Binary file not shown.
BIN
venv/Lib/site-packages/click/__pycache__/parser.cpython-310.pyc
Normal file
BIN
venv/Lib/site-packages/click/__pycache__/parser.cpython-310.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
venv/Lib/site-packages/click/__pycache__/termui.cpython-310.pyc
Normal file
BIN
venv/Lib/site-packages/click/__pycache__/termui.cpython-310.pyc
Normal file
Binary file not shown.
BIN
venv/Lib/site-packages/click/__pycache__/testing.cpython-310.pyc
Normal file
BIN
venv/Lib/site-packages/click/__pycache__/testing.cpython-310.pyc
Normal file
Binary file not shown.
BIN
venv/Lib/site-packages/click/__pycache__/types.cpython-310.pyc
Normal file
BIN
venv/Lib/site-packages/click/__pycache__/types.cpython-310.pyc
Normal file
Binary file not shown.
BIN
venv/Lib/site-packages/click/__pycache__/utils.cpython-310.pyc
Normal file
BIN
venv/Lib/site-packages/click/__pycache__/utils.cpython-310.pyc
Normal file
Binary file not shown.
622
venv/Lib/site-packages/click/_compat.py
Normal file
622
venv/Lib/site-packages/click/_compat.py
Normal file
@@ -0,0 +1,622 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import codecs
|
||||
import collections.abc as cabc
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import typing as t
|
||||
from types import TracebackType
|
||||
from weakref import WeakKeyDictionary
|
||||
|
||||
CYGWIN = sys.platform.startswith("cygwin")
|
||||
WIN = sys.platform.startswith("win")
|
||||
auto_wrap_for_ansi: t.Callable[[t.TextIO], t.TextIO] | None = None
|
||||
_ansi_re = re.compile(r"\033\[[;?0-9]*[a-zA-Z]")
|
||||
|
||||
|
||||
def _make_text_stream(
|
||||
stream: t.BinaryIO,
|
||||
encoding: str | None,
|
||||
errors: str | None,
|
||||
force_readable: bool = False,
|
||||
force_writable: bool = False,
|
||||
) -> t.TextIO:
|
||||
if encoding is None:
|
||||
encoding = get_best_encoding(stream)
|
||||
if errors is None:
|
||||
errors = "replace"
|
||||
return _NonClosingTextIOWrapper(
|
||||
stream,
|
||||
encoding,
|
||||
errors,
|
||||
line_buffering=True,
|
||||
force_readable=force_readable,
|
||||
force_writable=force_writable,
|
||||
)
|
||||
|
||||
|
||||
def is_ascii_encoding(encoding: str) -> bool:
|
||||
"""Checks if a given encoding is ascii."""
|
||||
try:
|
||||
return codecs.lookup(encoding).name == "ascii"
|
||||
except LookupError:
|
||||
return False
|
||||
|
||||
|
||||
def get_best_encoding(stream: t.IO[t.Any]) -> str:
|
||||
"""Returns the default stream encoding if not found."""
|
||||
rv = getattr(stream, "encoding", None) or sys.getdefaultencoding()
|
||||
if is_ascii_encoding(rv):
|
||||
return "utf-8"
|
||||
return rv
|
||||
|
||||
|
||||
class _NonClosingTextIOWrapper(io.TextIOWrapper):
|
||||
def __init__(
|
||||
self,
|
||||
stream: t.BinaryIO,
|
||||
encoding: str | None,
|
||||
errors: str | None,
|
||||
force_readable: bool = False,
|
||||
force_writable: bool = False,
|
||||
**extra: t.Any,
|
||||
) -> None:
|
||||
self._stream = stream = t.cast(
|
||||
t.BinaryIO, _FixupStream(stream, force_readable, force_writable)
|
||||
)
|
||||
super().__init__(stream, encoding, errors, **extra)
|
||||
|
||||
def __del__(self) -> None:
|
||||
try:
|
||||
self.detach()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def isatty(self) -> bool:
|
||||
# https://bitbucket.org/pypy/pypy/issue/1803
|
||||
return self._stream.isatty()
|
||||
|
||||
|
||||
class _FixupStream:
|
||||
"""The new io interface needs more from streams than streams
|
||||
traditionally implement. As such, this fix-up code is necessary in
|
||||
some circumstances.
|
||||
|
||||
The forcing of readable and writable flags are there because some tools
|
||||
put badly patched objects on sys (one such offender are certain version
|
||||
of jupyter notebook).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
stream: t.BinaryIO,
|
||||
force_readable: bool = False,
|
||||
force_writable: bool = False,
|
||||
):
|
||||
self._stream = stream
|
||||
self._force_readable = force_readable
|
||||
self._force_writable = force_writable
|
||||
|
||||
def __getattr__(self, name: str) -> t.Any:
|
||||
return getattr(self._stream, name)
|
||||
|
||||
def read1(self, size: int) -> bytes:
|
||||
f = getattr(self._stream, "read1", None)
|
||||
|
||||
if f is not None:
|
||||
return t.cast(bytes, f(size))
|
||||
|
||||
return self._stream.read(size)
|
||||
|
||||
def readable(self) -> bool:
|
||||
if self._force_readable:
|
||||
return True
|
||||
x = getattr(self._stream, "readable", None)
|
||||
if x is not None:
|
||||
return t.cast(bool, x())
|
||||
try:
|
||||
self._stream.read(0)
|
||||
except Exception:
|
||||
return False
|
||||
return True
|
||||
|
||||
def writable(self) -> bool:
|
||||
if self._force_writable:
|
||||
return True
|
||||
x = getattr(self._stream, "writable", None)
|
||||
if x is not None:
|
||||
return t.cast(bool, x())
|
||||
try:
|
||||
self._stream.write(b"")
|
||||
except Exception:
|
||||
try:
|
||||
self._stream.write(b"")
|
||||
except Exception:
|
||||
return False
|
||||
return True
|
||||
|
||||
def seekable(self) -> bool:
|
||||
x = getattr(self._stream, "seekable", None)
|
||||
if x is not None:
|
||||
return t.cast(bool, x())
|
||||
try:
|
||||
self._stream.seek(self._stream.tell())
|
||||
except Exception:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _is_binary_reader(stream: t.IO[t.Any], default: bool = False) -> bool:
|
||||
try:
|
||||
return isinstance(stream.read(0), bytes)
|
||||
except Exception:
|
||||
return default
|
||||
# This happens in some cases where the stream was already
|
||||
# closed. In this case, we assume the default.
|
||||
|
||||
|
||||
def _is_binary_writer(stream: t.IO[t.Any], default: bool = False) -> bool:
|
||||
try:
|
||||
stream.write(b"")
|
||||
except Exception:
|
||||
try:
|
||||
stream.write("")
|
||||
return False
|
||||
except Exception:
|
||||
pass
|
||||
return default
|
||||
return True
|
||||
|
||||
|
||||
def _find_binary_reader(stream: t.IO[t.Any]) -> t.BinaryIO | None:
|
||||
# We need to figure out if the given stream is already binary.
|
||||
# This can happen because the official docs recommend detaching
|
||||
# the streams to get binary streams. Some code might do this, so
|
||||
# we need to deal with this case explicitly.
|
||||
if _is_binary_reader(stream, False):
|
||||
return t.cast(t.BinaryIO, stream)
|
||||
|
||||
buf = getattr(stream, "buffer", None)
|
||||
|
||||
# Same situation here; this time we assume that the buffer is
|
||||
# actually binary in case it's closed.
|
||||
if buf is not None and _is_binary_reader(buf, True):
|
||||
return t.cast(t.BinaryIO, buf)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _find_binary_writer(stream: t.IO[t.Any]) -> t.BinaryIO | None:
|
||||
# We need to figure out if the given stream is already binary.
|
||||
# This can happen because the official docs recommend detaching
|
||||
# the streams to get binary streams. Some code might do this, so
|
||||
# we need to deal with this case explicitly.
|
||||
if _is_binary_writer(stream, False):
|
||||
return t.cast(t.BinaryIO, stream)
|
||||
|
||||
buf = getattr(stream, "buffer", None)
|
||||
|
||||
# Same situation here; this time we assume that the buffer is
|
||||
# actually binary in case it's closed.
|
||||
if buf is not None and _is_binary_writer(buf, True):
|
||||
return t.cast(t.BinaryIO, buf)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _stream_is_misconfigured(stream: t.TextIO) -> bool:
|
||||
"""A stream is misconfigured if its encoding is ASCII."""
|
||||
# If the stream does not have an encoding set, we assume it's set
|
||||
# to ASCII. This appears to happen in certain unittest
|
||||
# environments. It's not quite clear what the correct behavior is
|
||||
# but this at least will force Click to recover somehow.
|
||||
return is_ascii_encoding(getattr(stream, "encoding", None) or "ascii")
|
||||
|
||||
|
||||
def _is_compat_stream_attr(stream: t.TextIO, attr: str, value: str | None) -> bool:
|
||||
"""A stream attribute is compatible if it is equal to the
|
||||
desired value or the desired value is unset and the attribute
|
||||
has a value.
|
||||
"""
|
||||
stream_value = getattr(stream, attr, None)
|
||||
return stream_value == value or (value is None and stream_value is not None)
|
||||
|
||||
|
||||
def _is_compatible_text_stream(
|
||||
stream: t.TextIO, encoding: str | None, errors: str | None
|
||||
) -> bool:
|
||||
"""Check if a stream's encoding and errors attributes are
|
||||
compatible with the desired values.
|
||||
"""
|
||||
return _is_compat_stream_attr(
|
||||
stream, "encoding", encoding
|
||||
) and _is_compat_stream_attr(stream, "errors", errors)
|
||||
|
||||
|
||||
def _force_correct_text_stream(
|
||||
text_stream: t.IO[t.Any],
|
||||
encoding: str | None,
|
||||
errors: str | None,
|
||||
is_binary: t.Callable[[t.IO[t.Any], bool], bool],
|
||||
find_binary: t.Callable[[t.IO[t.Any]], t.BinaryIO | None],
|
||||
force_readable: bool = False,
|
||||
force_writable: bool = False,
|
||||
) -> t.TextIO:
|
||||
if is_binary(text_stream, False):
|
||||
binary_reader = t.cast(t.BinaryIO, text_stream)
|
||||
else:
|
||||
text_stream = t.cast(t.TextIO, text_stream)
|
||||
# If the stream looks compatible, and won't default to a
|
||||
# misconfigured ascii encoding, return it as-is.
|
||||
if _is_compatible_text_stream(text_stream, encoding, errors) and not (
|
||||
encoding is None and _stream_is_misconfigured(text_stream)
|
||||
):
|
||||
return text_stream
|
||||
|
||||
# Otherwise, get the underlying binary reader.
|
||||
possible_binary_reader = find_binary(text_stream)
|
||||
|
||||
# If that's not possible, silently use the original reader
|
||||
# and get mojibake instead of exceptions.
|
||||
if possible_binary_reader is None:
|
||||
return text_stream
|
||||
|
||||
binary_reader = possible_binary_reader
|
||||
|
||||
# Default errors to replace instead of strict in order to get
|
||||
# something that works.
|
||||
if errors is None:
|
||||
errors = "replace"
|
||||
|
||||
# Wrap the binary stream in a text stream with the correct
|
||||
# encoding parameters.
|
||||
return _make_text_stream(
|
||||
binary_reader,
|
||||
encoding,
|
||||
errors,
|
||||
force_readable=force_readable,
|
||||
force_writable=force_writable,
|
||||
)
|
||||
|
||||
|
||||
def _force_correct_text_reader(
|
||||
text_reader: t.IO[t.Any],
|
||||
encoding: str | None,
|
||||
errors: str | None,
|
||||
force_readable: bool = False,
|
||||
) -> t.TextIO:
|
||||
return _force_correct_text_stream(
|
||||
text_reader,
|
||||
encoding,
|
||||
errors,
|
||||
_is_binary_reader,
|
||||
_find_binary_reader,
|
||||
force_readable=force_readable,
|
||||
)
|
||||
|
||||
|
||||
def _force_correct_text_writer(
|
||||
text_writer: t.IO[t.Any],
|
||||
encoding: str | None,
|
||||
errors: str | None,
|
||||
force_writable: bool = False,
|
||||
) -> t.TextIO:
|
||||
return _force_correct_text_stream(
|
||||
text_writer,
|
||||
encoding,
|
||||
errors,
|
||||
_is_binary_writer,
|
||||
_find_binary_writer,
|
||||
force_writable=force_writable,
|
||||
)
|
||||
|
||||
|
||||
def get_binary_stdin() -> t.BinaryIO:
|
||||
reader = _find_binary_reader(sys.stdin)
|
||||
if reader is None:
|
||||
raise RuntimeError("Was not able to determine binary stream for sys.stdin.")
|
||||
return reader
|
||||
|
||||
|
||||
def get_binary_stdout() -> t.BinaryIO:
|
||||
writer = _find_binary_writer(sys.stdout)
|
||||
if writer is None:
|
||||
raise RuntimeError("Was not able to determine binary stream for sys.stdout.")
|
||||
return writer
|
||||
|
||||
|
||||
def get_binary_stderr() -> t.BinaryIO:
|
||||
writer = _find_binary_writer(sys.stderr)
|
||||
if writer is None:
|
||||
raise RuntimeError("Was not able to determine binary stream for sys.stderr.")
|
||||
return writer
|
||||
|
||||
|
||||
def get_text_stdin(encoding: str | None = None, errors: str | None = None) -> t.TextIO:
|
||||
rv = _get_windows_console_stream(sys.stdin, encoding, errors)
|
||||
if rv is not None:
|
||||
return rv
|
||||
return _force_correct_text_reader(sys.stdin, encoding, errors, force_readable=True)
|
||||
|
||||
|
||||
def get_text_stdout(encoding: str | None = None, errors: str | None = None) -> t.TextIO:
|
||||
rv = _get_windows_console_stream(sys.stdout, encoding, errors)
|
||||
if rv is not None:
|
||||
return rv
|
||||
return _force_correct_text_writer(sys.stdout, encoding, errors, force_writable=True)
|
||||
|
||||
|
||||
def get_text_stderr(encoding: str | None = None, errors: str | None = None) -> t.TextIO:
|
||||
rv = _get_windows_console_stream(sys.stderr, encoding, errors)
|
||||
if rv is not None:
|
||||
return rv
|
||||
return _force_correct_text_writer(sys.stderr, encoding, errors, force_writable=True)
|
||||
|
||||
|
||||
def _wrap_io_open(
|
||||
file: str | os.PathLike[str] | int,
|
||||
mode: str,
|
||||
encoding: str | None,
|
||||
errors: str | None,
|
||||
) -> t.IO[t.Any]:
|
||||
"""Handles not passing ``encoding`` and ``errors`` in binary mode."""
|
||||
if "b" in mode:
|
||||
return open(file, mode)
|
||||
|
||||
return open(file, mode, encoding=encoding, errors=errors)
|
||||
|
||||
|
||||
def open_stream(
|
||||
filename: str | os.PathLike[str],
|
||||
mode: str = "r",
|
||||
encoding: str | None = None,
|
||||
errors: str | None = "strict",
|
||||
atomic: bool = False,
|
||||
) -> tuple[t.IO[t.Any], bool]:
|
||||
binary = "b" in mode
|
||||
filename = os.fspath(filename)
|
||||
|
||||
# Standard streams first. These are simple because they ignore the
|
||||
# atomic flag. Use fsdecode to handle Path("-").
|
||||
if os.fsdecode(filename) == "-":
|
||||
if any(m in mode for m in ["w", "a", "x"]):
|
||||
if binary:
|
||||
return get_binary_stdout(), False
|
||||
return get_text_stdout(encoding=encoding, errors=errors), False
|
||||
if binary:
|
||||
return get_binary_stdin(), False
|
||||
return get_text_stdin(encoding=encoding, errors=errors), False
|
||||
|
||||
# Non-atomic writes directly go out through the regular open functions.
|
||||
if not atomic:
|
||||
return _wrap_io_open(filename, mode, encoding, errors), True
|
||||
|
||||
# Some usability stuff for atomic writes
|
||||
if "a" in mode:
|
||||
raise ValueError(
|
||||
"Appending to an existing file is not supported, because that"
|
||||
" would involve an expensive `copy`-operation to a temporary"
|
||||
" file. Open the file in normal `w`-mode and copy explicitly"
|
||||
" if that's what you're after."
|
||||
)
|
||||
if "x" in mode:
|
||||
raise ValueError("Use the `overwrite`-parameter instead.")
|
||||
if "w" not in mode:
|
||||
raise ValueError("Atomic writes only make sense with `w`-mode.")
|
||||
|
||||
# Atomic writes are more complicated. They work by opening a file
|
||||
# as a proxy in the same folder and then using the fdopen
|
||||
# functionality to wrap it in a Python file. Then we wrap it in an
|
||||
# atomic file that moves the file over on close.
|
||||
import errno
|
||||
import random
|
||||
|
||||
try:
|
||||
perm: int | None = os.stat(filename).st_mode
|
||||
except OSError:
|
||||
perm = None
|
||||
|
||||
flags = os.O_RDWR | os.O_CREAT | os.O_EXCL
|
||||
|
||||
if binary:
|
||||
flags |= getattr(os, "O_BINARY", 0)
|
||||
|
||||
while True:
|
||||
tmp_filename = os.path.join(
|
||||
os.path.dirname(filename),
|
||||
f".__atomic-write{random.randrange(1 << 32):08x}",
|
||||
)
|
||||
try:
|
||||
fd = os.open(tmp_filename, flags, 0o666 if perm is None else perm)
|
||||
break
|
||||
except OSError as e:
|
||||
if e.errno == errno.EEXIST or (
|
||||
os.name == "nt"
|
||||
and e.errno == errno.EACCES
|
||||
and os.path.isdir(e.filename)
|
||||
and os.access(e.filename, os.W_OK)
|
||||
):
|
||||
continue
|
||||
raise
|
||||
|
||||
if perm is not None:
|
||||
os.chmod(tmp_filename, perm) # in case perm includes bits in umask
|
||||
|
||||
f = _wrap_io_open(fd, mode, encoding, errors)
|
||||
af = _AtomicFile(f, tmp_filename, os.path.realpath(filename))
|
||||
return t.cast(t.IO[t.Any], af), True
|
||||
|
||||
|
||||
class _AtomicFile:
|
||||
def __init__(self, f: t.IO[t.Any], tmp_filename: str, real_filename: str) -> None:
|
||||
self._f = f
|
||||
self._tmp_filename = tmp_filename
|
||||
self._real_filename = real_filename
|
||||
self.closed = False
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._real_filename
|
||||
|
||||
def close(self, delete: bool = False) -> None:
|
||||
if self.closed:
|
||||
return
|
||||
self._f.close()
|
||||
os.replace(self._tmp_filename, self._real_filename)
|
||||
self.closed = True
|
||||
|
||||
def __getattr__(self, name: str) -> t.Any:
|
||||
return getattr(self._f, name)
|
||||
|
||||
def __enter__(self) -> _AtomicFile:
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_value: BaseException | None,
|
||||
tb: TracebackType | None,
|
||||
) -> None:
|
||||
self.close(delete=exc_type is not None)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return repr(self._f)
|
||||
|
||||
|
||||
def strip_ansi(value: str) -> str:
|
||||
return _ansi_re.sub("", value)
|
||||
|
||||
|
||||
def _is_jupyter_kernel_output(stream: t.IO[t.Any]) -> bool:
|
||||
while isinstance(stream, (_FixupStream, _NonClosingTextIOWrapper)):
|
||||
stream = stream._stream
|
||||
|
||||
return stream.__class__.__module__.startswith("ipykernel.")
|
||||
|
||||
|
||||
def should_strip_ansi(
|
||||
stream: t.IO[t.Any] | None = None, color: bool | None = None
|
||||
) -> bool:
|
||||
if color is None:
|
||||
if stream is None:
|
||||
stream = sys.stdin
|
||||
return not isatty(stream) and not _is_jupyter_kernel_output(stream)
|
||||
return not color
|
||||
|
||||
|
||||
# On Windows, wrap the output streams with colorama to support ANSI
|
||||
# color codes.
|
||||
# NOTE: double check is needed so mypy does not analyze this on Linux
|
||||
if sys.platform.startswith("win") and WIN:
|
||||
from ._winconsole import _get_windows_console_stream
|
||||
|
||||
def _get_argv_encoding() -> str:
|
||||
import locale
|
||||
|
||||
return locale.getpreferredencoding()
|
||||
|
||||
_ansi_stream_wrappers: cabc.MutableMapping[t.TextIO, t.TextIO] = WeakKeyDictionary()
|
||||
|
||||
def auto_wrap_for_ansi(stream: t.TextIO, color: bool | None = None) -> t.TextIO:
|
||||
"""Support ANSI color and style codes on Windows by wrapping a
|
||||
stream with colorama.
|
||||
"""
|
||||
try:
|
||||
cached = _ansi_stream_wrappers.get(stream)
|
||||
except Exception:
|
||||
cached = None
|
||||
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
import colorama
|
||||
|
||||
strip = should_strip_ansi(stream, color)
|
||||
ansi_wrapper = colorama.AnsiToWin32(stream, strip=strip)
|
||||
rv = t.cast(t.TextIO, ansi_wrapper.stream)
|
||||
_write = rv.write
|
||||
|
||||
def _safe_write(s: str) -> int:
|
||||
try:
|
||||
return _write(s)
|
||||
except BaseException:
|
||||
ansi_wrapper.reset_all()
|
||||
raise
|
||||
|
||||
rv.write = _safe_write # type: ignore[method-assign]
|
||||
|
||||
try:
|
||||
_ansi_stream_wrappers[stream] = rv
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return rv
|
||||
|
||||
else:
|
||||
|
||||
def _get_argv_encoding() -> str:
|
||||
return getattr(sys.stdin, "encoding", None) or sys.getfilesystemencoding()
|
||||
|
||||
def _get_windows_console_stream(
|
||||
f: t.TextIO, encoding: str | None, errors: str | None
|
||||
) -> t.TextIO | None:
|
||||
return None
|
||||
|
||||
|
||||
def term_len(x: str) -> int:
|
||||
return len(strip_ansi(x))
|
||||
|
||||
|
||||
def isatty(stream: t.IO[t.Any]) -> bool:
|
||||
try:
|
||||
return stream.isatty()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _make_cached_stream_func(
|
||||
src_func: t.Callable[[], t.TextIO | None],
|
||||
wrapper_func: t.Callable[[], t.TextIO],
|
||||
) -> t.Callable[[], t.TextIO | None]:
|
||||
cache: cabc.MutableMapping[t.TextIO, t.TextIO] = WeakKeyDictionary()
|
||||
|
||||
def func() -> t.TextIO | None:
|
||||
stream = src_func()
|
||||
|
||||
if stream is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
rv = cache.get(stream)
|
||||
except Exception:
|
||||
rv = None
|
||||
if rv is not None:
|
||||
return rv
|
||||
rv = wrapper_func()
|
||||
try:
|
||||
cache[stream] = rv
|
||||
except Exception:
|
||||
pass
|
||||
return rv
|
||||
|
||||
return func
|
||||
|
||||
|
||||
_default_text_stdin = _make_cached_stream_func(lambda: sys.stdin, get_text_stdin)
|
||||
_default_text_stdout = _make_cached_stream_func(lambda: sys.stdout, get_text_stdout)
|
||||
_default_text_stderr = _make_cached_stream_func(lambda: sys.stderr, get_text_stderr)
|
||||
|
||||
|
||||
binary_streams: cabc.Mapping[str, t.Callable[[], t.BinaryIO]] = {
|
||||
"stdin": get_binary_stdin,
|
||||
"stdout": get_binary_stdout,
|
||||
"stderr": get_binary_stderr,
|
||||
}
|
||||
|
||||
text_streams: cabc.Mapping[str, t.Callable[[str | None, str | None], t.TextIO]] = {
|
||||
"stdin": get_text_stdin,
|
||||
"stdout": get_text_stdout,
|
||||
"stderr": get_text_stderr,
|
||||
}
|
||||
839
venv/Lib/site-packages/click/_termui_impl.py
Normal file
839
venv/Lib/site-packages/click/_termui_impl.py
Normal file
@@ -0,0 +1,839 @@
|
||||
"""
|
||||
This module contains implementations for the termui module. To keep the
|
||||
import time of Click down, some infrequently used functionality is
|
||||
placed in this module and only imported as needed.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import collections.abc as cabc
|
||||
import contextlib
|
||||
import math
|
||||
import os
|
||||
import shlex
|
||||
import sys
|
||||
import time
|
||||
import typing as t
|
||||
from gettext import gettext as _
|
||||
from io import StringIO
|
||||
from pathlib import Path
|
||||
from shutil import which
|
||||
from types import TracebackType
|
||||
|
||||
from ._compat import _default_text_stdout
|
||||
from ._compat import CYGWIN
|
||||
from ._compat import get_best_encoding
|
||||
from ._compat import isatty
|
||||
from ._compat import open_stream
|
||||
from ._compat import strip_ansi
|
||||
from ._compat import term_len
|
||||
from ._compat import WIN
|
||||
from .exceptions import ClickException
|
||||
from .utils import echo
|
||||
|
||||
V = t.TypeVar("V")
|
||||
|
||||
if os.name == "nt":
|
||||
BEFORE_BAR = "\r"
|
||||
AFTER_BAR = "\n"
|
||||
else:
|
||||
BEFORE_BAR = "\r\033[?25l"
|
||||
AFTER_BAR = "\033[?25h\n"
|
||||
|
||||
|
||||
class ProgressBar(t.Generic[V]):
|
||||
def __init__(
|
||||
self,
|
||||
iterable: cabc.Iterable[V] | None,
|
||||
length: int | None = None,
|
||||
fill_char: str = "#",
|
||||
empty_char: str = " ",
|
||||
bar_template: str = "%(bar)s",
|
||||
info_sep: str = " ",
|
||||
hidden: bool = False,
|
||||
show_eta: bool = True,
|
||||
show_percent: bool | None = None,
|
||||
show_pos: bool = False,
|
||||
item_show_func: t.Callable[[V | None], str | None] | None = None,
|
||||
label: str | None = None,
|
||||
file: t.TextIO | None = None,
|
||||
color: bool | None = None,
|
||||
update_min_steps: int = 1,
|
||||
width: int = 30,
|
||||
) -> None:
|
||||
self.fill_char = fill_char
|
||||
self.empty_char = empty_char
|
||||
self.bar_template = bar_template
|
||||
self.info_sep = info_sep
|
||||
self.hidden = hidden
|
||||
self.show_eta = show_eta
|
||||
self.show_percent = show_percent
|
||||
self.show_pos = show_pos
|
||||
self.item_show_func = item_show_func
|
||||
self.label: str = label or ""
|
||||
|
||||
if file is None:
|
||||
file = _default_text_stdout()
|
||||
|
||||
# There are no standard streams attached to write to. For example,
|
||||
# pythonw on Windows.
|
||||
if file is None:
|
||||
file = StringIO()
|
||||
|
||||
self.file = file
|
||||
self.color = color
|
||||
self.update_min_steps = update_min_steps
|
||||
self._completed_intervals = 0
|
||||
self.width: int = width
|
||||
self.autowidth: bool = width == 0
|
||||
|
||||
if length is None:
|
||||
from operator import length_hint
|
||||
|
||||
length = length_hint(iterable, -1)
|
||||
|
||||
if length == -1:
|
||||
length = None
|
||||
if iterable is None:
|
||||
if length is None:
|
||||
raise TypeError("iterable or length is required")
|
||||
iterable = t.cast("cabc.Iterable[V]", range(length))
|
||||
self.iter: cabc.Iterable[V] = iter(iterable)
|
||||
self.length = length
|
||||
self.pos: int = 0
|
||||
self.avg: list[float] = []
|
||||
self.last_eta: float
|
||||
self.start: float
|
||||
self.start = self.last_eta = time.time()
|
||||
self.eta_known: bool = False
|
||||
self.finished: bool = False
|
||||
self.max_width: int | None = None
|
||||
self.entered: bool = False
|
||||
self.current_item: V | None = None
|
||||
self._is_atty = isatty(self.file)
|
||||
self._last_line: str | None = None
|
||||
|
||||
def __enter__(self) -> ProgressBar[V]:
|
||||
self.entered = True
|
||||
self.render_progress()
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_value: BaseException | None,
|
||||
tb: TracebackType | None,
|
||||
) -> None:
|
||||
self.render_finish()
|
||||
|
||||
def __iter__(self) -> cabc.Iterator[V]:
|
||||
if not self.entered:
|
||||
raise RuntimeError("You need to use progress bars in a with block.")
|
||||
self.render_progress()
|
||||
return self.generator()
|
||||
|
||||
def __next__(self) -> V:
|
||||
# Iteration is defined in terms of a generator function,
|
||||
# returned by iter(self); use that to define next(). This works
|
||||
# because `self.iter` is an iterable consumed by that generator,
|
||||
# so it is re-entry safe. Calling `next(self.generator())`
|
||||
# twice works and does "what you want".
|
||||
return next(iter(self))
|
||||
|
||||
def render_finish(self) -> None:
|
||||
if self.hidden or not self._is_atty:
|
||||
return
|
||||
self.file.write(AFTER_BAR)
|
||||
self.file.flush()
|
||||
|
||||
@property
|
||||
def pct(self) -> float:
|
||||
if self.finished:
|
||||
return 1.0
|
||||
return min(self.pos / (float(self.length or 1) or 1), 1.0)
|
||||
|
||||
@property
|
||||
def time_per_iteration(self) -> float:
|
||||
if not self.avg:
|
||||
return 0.0
|
||||
return sum(self.avg) / float(len(self.avg))
|
||||
|
||||
@property
|
||||
def eta(self) -> float:
|
||||
if self.length is not None and not self.finished:
|
||||
return self.time_per_iteration * (self.length - self.pos)
|
||||
return 0.0
|
||||
|
||||
def format_eta(self) -> str:
|
||||
if self.eta_known:
|
||||
t = int(self.eta)
|
||||
seconds = t % 60
|
||||
t //= 60
|
||||
minutes = t % 60
|
||||
t //= 60
|
||||
hours = t % 24
|
||||
t //= 24
|
||||
if t > 0:
|
||||
return f"{t}d {hours:02}:{minutes:02}:{seconds:02}"
|
||||
else:
|
||||
return f"{hours:02}:{minutes:02}:{seconds:02}"
|
||||
return ""
|
||||
|
||||
def format_pos(self) -> str:
|
||||
pos = str(self.pos)
|
||||
if self.length is not None:
|
||||
pos += f"/{self.length}"
|
||||
return pos
|
||||
|
||||
def format_pct(self) -> str:
|
||||
return f"{int(self.pct * 100): 4}%"[1:]
|
||||
|
||||
def format_bar(self) -> str:
|
||||
if self.length is not None:
|
||||
bar_length = int(self.pct * self.width)
|
||||
bar = self.fill_char * bar_length
|
||||
bar += self.empty_char * (self.width - bar_length)
|
||||
elif self.finished:
|
||||
bar = self.fill_char * self.width
|
||||
else:
|
||||
chars = list(self.empty_char * (self.width or 1))
|
||||
if self.time_per_iteration != 0:
|
||||
chars[
|
||||
int(
|
||||
(math.cos(self.pos * self.time_per_iteration) / 2.0 + 0.5)
|
||||
* self.width
|
||||
)
|
||||
] = self.fill_char
|
||||
bar = "".join(chars)
|
||||
return bar
|
||||
|
||||
def format_progress_line(self) -> str:
|
||||
show_percent = self.show_percent
|
||||
|
||||
info_bits = []
|
||||
if self.length is not None and show_percent is None:
|
||||
show_percent = not self.show_pos
|
||||
|
||||
if self.show_pos:
|
||||
info_bits.append(self.format_pos())
|
||||
if show_percent:
|
||||
info_bits.append(self.format_pct())
|
||||
if self.show_eta and self.eta_known and not self.finished:
|
||||
info_bits.append(self.format_eta())
|
||||
if self.item_show_func is not None:
|
||||
item_info = self.item_show_func(self.current_item)
|
||||
if item_info is not None:
|
||||
info_bits.append(item_info)
|
||||
|
||||
return (
|
||||
self.bar_template
|
||||
% {
|
||||
"label": self.label,
|
||||
"bar": self.format_bar(),
|
||||
"info": self.info_sep.join(info_bits),
|
||||
}
|
||||
).rstrip()
|
||||
|
||||
def render_progress(self) -> None:
|
||||
import shutil
|
||||
|
||||
if self.hidden:
|
||||
return
|
||||
|
||||
if not self._is_atty:
|
||||
# Only output the label once if the output is not a TTY.
|
||||
if self._last_line != self.label:
|
||||
self._last_line = self.label
|
||||
echo(self.label, file=self.file, color=self.color)
|
||||
return
|
||||
|
||||
buf = []
|
||||
# Update width in case the terminal has been resized
|
||||
if self.autowidth:
|
||||
old_width = self.width
|
||||
self.width = 0
|
||||
clutter_length = term_len(self.format_progress_line())
|
||||
new_width = max(0, shutil.get_terminal_size().columns - clutter_length)
|
||||
if new_width < old_width and self.max_width is not None:
|
||||
buf.append(BEFORE_BAR)
|
||||
buf.append(" " * self.max_width)
|
||||
self.max_width = new_width
|
||||
self.width = new_width
|
||||
|
||||
clear_width = self.width
|
||||
if self.max_width is not None:
|
||||
clear_width = self.max_width
|
||||
|
||||
buf.append(BEFORE_BAR)
|
||||
line = self.format_progress_line()
|
||||
line_len = term_len(line)
|
||||
if self.max_width is None or self.max_width < line_len:
|
||||
self.max_width = line_len
|
||||
|
||||
buf.append(line)
|
||||
buf.append(" " * (clear_width - line_len))
|
||||
line = "".join(buf)
|
||||
# Render the line only if it changed.
|
||||
|
||||
if line != self._last_line:
|
||||
self._last_line = line
|
||||
echo(line, file=self.file, color=self.color, nl=False)
|
||||
self.file.flush()
|
||||
|
||||
def make_step(self, n_steps: int) -> None:
|
||||
self.pos += n_steps
|
||||
if self.length is not None and self.pos >= self.length:
|
||||
self.finished = True
|
||||
|
||||
if (time.time() - self.last_eta) < 1.0:
|
||||
return
|
||||
|
||||
self.last_eta = time.time()
|
||||
|
||||
# self.avg is a rolling list of length <= 7 of steps where steps are
|
||||
# defined as time elapsed divided by the total progress through
|
||||
# self.length.
|
||||
if self.pos:
|
||||
step = (time.time() - self.start) / self.pos
|
||||
else:
|
||||
step = time.time() - self.start
|
||||
|
||||
self.avg = self.avg[-6:] + [step]
|
||||
|
||||
self.eta_known = self.length is not None
|
||||
|
||||
def update(self, n_steps: int, current_item: V | None = None) -> None:
|
||||
"""Update the progress bar by advancing a specified number of
|
||||
steps, and optionally set the ``current_item`` for this new
|
||||
position.
|
||||
|
||||
:param n_steps: Number of steps to advance.
|
||||
:param current_item: Optional item to set as ``current_item``
|
||||
for the updated position.
|
||||
|
||||
.. versionchanged:: 8.0
|
||||
Added the ``current_item`` optional parameter.
|
||||
|
||||
.. versionchanged:: 8.0
|
||||
Only render when the number of steps meets the
|
||||
``update_min_steps`` threshold.
|
||||
"""
|
||||
if current_item is not None:
|
||||
self.current_item = current_item
|
||||
|
||||
self._completed_intervals += n_steps
|
||||
|
||||
if self._completed_intervals >= self.update_min_steps:
|
||||
self.make_step(self._completed_intervals)
|
||||
self.render_progress()
|
||||
self._completed_intervals = 0
|
||||
|
||||
def finish(self) -> None:
|
||||
self.eta_known = False
|
||||
self.current_item = None
|
||||
self.finished = True
|
||||
|
||||
def generator(self) -> cabc.Iterator[V]:
|
||||
"""Return a generator which yields the items added to the bar
|
||||
during construction, and updates the progress bar *after* the
|
||||
yielded block returns.
|
||||
"""
|
||||
# WARNING: the iterator interface for `ProgressBar` relies on
|
||||
# this and only works because this is a simple generator which
|
||||
# doesn't create or manage additional state. If this function
|
||||
# changes, the impact should be evaluated both against
|
||||
# `iter(bar)` and `next(bar)`. `next()` in particular may call
|
||||
# `self.generator()` repeatedly, and this must remain safe in
|
||||
# order for that interface to work.
|
||||
if not self.entered:
|
||||
raise RuntimeError("You need to use progress bars in a with block.")
|
||||
|
||||
if not self._is_atty:
|
||||
yield from self.iter
|
||||
else:
|
||||
for rv in self.iter:
|
||||
self.current_item = rv
|
||||
|
||||
# This allows show_item_func to be updated before the
|
||||
# item is processed. Only trigger at the beginning of
|
||||
# the update interval.
|
||||
if self._completed_intervals == 0:
|
||||
self.render_progress()
|
||||
|
||||
yield rv
|
||||
self.update(1)
|
||||
|
||||
self.finish()
|
||||
self.render_progress()
|
||||
|
||||
|
||||
def pager(generator: cabc.Iterable[str], color: bool | None = None) -> None:
|
||||
"""Decide what method to use for paging through text."""
|
||||
stdout = _default_text_stdout()
|
||||
|
||||
# There are no standard streams attached to write to. For example,
|
||||
# pythonw on Windows.
|
||||
if stdout is None:
|
||||
stdout = StringIO()
|
||||
|
||||
if not isatty(sys.stdin) or not isatty(stdout):
|
||||
return _nullpager(stdout, generator, color)
|
||||
|
||||
# Split and normalize the pager command into parts.
|
||||
pager_cmd_parts = shlex.split(os.environ.get("PAGER", ""), posix=False)
|
||||
if pager_cmd_parts:
|
||||
if WIN:
|
||||
if _tempfilepager(generator, pager_cmd_parts, color):
|
||||
return
|
||||
elif _pipepager(generator, pager_cmd_parts, color):
|
||||
return
|
||||
|
||||
if os.environ.get("TERM") in ("dumb", "emacs"):
|
||||
return _nullpager(stdout, generator, color)
|
||||
if (WIN or sys.platform.startswith("os2")) and _tempfilepager(
|
||||
generator, ["more"], color
|
||||
):
|
||||
return
|
||||
if _pipepager(generator, ["less"], color):
|
||||
return
|
||||
|
||||
import tempfile
|
||||
|
||||
fd, filename = tempfile.mkstemp()
|
||||
os.close(fd)
|
||||
try:
|
||||
if _pipepager(generator, ["more"], color):
|
||||
return
|
||||
return _nullpager(stdout, generator, color)
|
||||
finally:
|
||||
os.unlink(filename)
|
||||
|
||||
|
||||
def _pipepager(
|
||||
generator: cabc.Iterable[str], cmd_parts: list[str], color: bool | None
|
||||
) -> bool:
|
||||
"""Page through text by feeding it to another program. Invoking a
|
||||
pager through this might support colors.
|
||||
|
||||
Returns `True` if the command was found, `False` otherwise and thus another
|
||||
pager should be attempted.
|
||||
"""
|
||||
# Split the command into the invoked CLI and its parameters.
|
||||
if not cmd_parts:
|
||||
return False
|
||||
cmd = cmd_parts[0]
|
||||
cmd_params = cmd_parts[1:]
|
||||
|
||||
cmd_filepath = which(cmd)
|
||||
if not cmd_filepath:
|
||||
return False
|
||||
# Resolves symlinks and produces a normalized absolute path string.
|
||||
cmd_path = Path(cmd_filepath).resolve()
|
||||
cmd_name = cmd_path.name
|
||||
|
||||
import subprocess
|
||||
|
||||
# Make a local copy of the environment to not affect the global one.
|
||||
env = dict(os.environ)
|
||||
|
||||
# If we're piping to less and the user hasn't decided on colors, we enable
|
||||
# them by default we find the -R flag in the command line arguments.
|
||||
if color is None and cmd_name == "less":
|
||||
less_flags = f"{os.environ.get('LESS', '')}{' '.join(cmd_params)}"
|
||||
if not less_flags:
|
||||
env["LESS"] = "-R"
|
||||
color = True
|
||||
elif "r" in less_flags or "R" in less_flags:
|
||||
color = True
|
||||
|
||||
c = subprocess.Popen(
|
||||
[str(cmd_path)] + cmd_params,
|
||||
shell=True,
|
||||
stdin=subprocess.PIPE,
|
||||
env=env,
|
||||
errors="replace",
|
||||
text=True,
|
||||
)
|
||||
assert c.stdin is not None
|
||||
try:
|
||||
for text in generator:
|
||||
if not color:
|
||||
text = strip_ansi(text)
|
||||
|
||||
c.stdin.write(text)
|
||||
except BrokenPipeError:
|
||||
# In case the pager exited unexpectedly, ignore the broken pipe error.
|
||||
pass
|
||||
except Exception as e:
|
||||
# In case there is an exception we want to close the pager immediately
|
||||
# and let the caller handle it.
|
||||
# Otherwise the pager will keep running, and the user may not notice
|
||||
# the error message, or worse yet it may leave the terminal in a broken state.
|
||||
c.terminate()
|
||||
raise e
|
||||
finally:
|
||||
# We must close stdin and wait for the pager to exit before we continue
|
||||
try:
|
||||
c.stdin.close()
|
||||
# Close implies flush, so it might throw a BrokenPipeError if the pager
|
||||
# process exited already.
|
||||
except BrokenPipeError:
|
||||
pass
|
||||
|
||||
# Less doesn't respect ^C, but catches it for its own UI purposes (aborting
|
||||
# search or other commands inside less).
|
||||
#
|
||||
# That means when the user hits ^C, the parent process (click) terminates,
|
||||
# but less is still alive, paging the output and messing up the terminal.
|
||||
#
|
||||
# If the user wants to make the pager exit on ^C, they should set
|
||||
# `LESS='-K'`. It's not our decision to make.
|
||||
while True:
|
||||
try:
|
||||
c.wait()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
else:
|
||||
break
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _tempfilepager(
|
||||
generator: cabc.Iterable[str], cmd_parts: list[str], color: bool | None
|
||||
) -> bool:
|
||||
"""Page through text by invoking a program on a temporary file.
|
||||
|
||||
Returns `True` if the command was found, `False` otherwise and thus another
|
||||
pager should be attempted.
|
||||
"""
|
||||
# Split the command into the invoked CLI and its parameters.
|
||||
if not cmd_parts:
|
||||
return False
|
||||
cmd = cmd_parts[0]
|
||||
|
||||
cmd_filepath = which(cmd)
|
||||
if not cmd_filepath:
|
||||
return False
|
||||
# Resolves symlinks and produces a normalized absolute path string.
|
||||
cmd_path = Path(cmd_filepath).resolve()
|
||||
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
fd, filename = tempfile.mkstemp()
|
||||
# TODO: This never terminates if the passed generator never terminates.
|
||||
text = "".join(generator)
|
||||
if not color:
|
||||
text = strip_ansi(text)
|
||||
encoding = get_best_encoding(sys.stdout)
|
||||
with open_stream(filename, "wb")[0] as f:
|
||||
f.write(text.encode(encoding))
|
||||
try:
|
||||
subprocess.call([str(cmd_path), filename])
|
||||
except OSError:
|
||||
# Command not found
|
||||
pass
|
||||
finally:
|
||||
os.close(fd)
|
||||
os.unlink(filename)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _nullpager(
|
||||
stream: t.TextIO, generator: cabc.Iterable[str], color: bool | None
|
||||
) -> None:
|
||||
"""Simply print unformatted text. This is the ultimate fallback."""
|
||||
for text in generator:
|
||||
if not color:
|
||||
text = strip_ansi(text)
|
||||
stream.write(text)
|
||||
|
||||
|
||||
class Editor:
|
||||
def __init__(
|
||||
self,
|
||||
editor: str | None = None,
|
||||
env: cabc.Mapping[str, str] | None = None,
|
||||
require_save: bool = True,
|
||||
extension: str = ".txt",
|
||||
) -> None:
|
||||
self.editor = editor
|
||||
self.env = env
|
||||
self.require_save = require_save
|
||||
self.extension = extension
|
||||
|
||||
def get_editor(self) -> str:
|
||||
if self.editor is not None:
|
||||
return self.editor
|
||||
for key in "VISUAL", "EDITOR":
|
||||
rv = os.environ.get(key)
|
||||
if rv:
|
||||
return rv
|
||||
if WIN:
|
||||
return "notepad"
|
||||
for editor in "sensible-editor", "vim", "nano":
|
||||
if which(editor) is not None:
|
||||
return editor
|
||||
return "vi"
|
||||
|
||||
def edit_files(self, filenames: cabc.Iterable[str]) -> None:
|
||||
import subprocess
|
||||
|
||||
editor = self.get_editor()
|
||||
environ: dict[str, str] | None = None
|
||||
|
||||
if self.env:
|
||||
environ = os.environ.copy()
|
||||
environ.update(self.env)
|
||||
|
||||
exc_filename = " ".join(f'"{filename}"' for filename in filenames)
|
||||
|
||||
try:
|
||||
c = subprocess.Popen(
|
||||
args=f"{editor} {exc_filename}", env=environ, shell=True
|
||||
)
|
||||
exit_code = c.wait()
|
||||
if exit_code != 0:
|
||||
raise ClickException(
|
||||
_("{editor}: Editing failed").format(editor=editor)
|
||||
)
|
||||
except OSError as e:
|
||||
raise ClickException(
|
||||
_("{editor}: Editing failed: {e}").format(editor=editor, e=e)
|
||||
) from e
|
||||
|
||||
@t.overload
|
||||
def edit(self, text: bytes | bytearray) -> bytes | None: ...
|
||||
|
||||
# We cannot know whether or not the type expected is str or bytes when None
|
||||
# is passed, so str is returned as that was what was done before.
|
||||
@t.overload
|
||||
def edit(self, text: str | None) -> str | None: ...
|
||||
|
||||
def edit(self, text: str | bytes | bytearray | None) -> str | bytes | None:
|
||||
import tempfile
|
||||
|
||||
if text is None:
|
||||
data = b""
|
||||
elif isinstance(text, (bytes, bytearray)):
|
||||
data = text
|
||||
else:
|
||||
if text and not text.endswith("\n"):
|
||||
text += "\n"
|
||||
|
||||
if WIN:
|
||||
data = text.replace("\n", "\r\n").encode("utf-8-sig")
|
||||
else:
|
||||
data = text.encode("utf-8")
|
||||
|
||||
fd, name = tempfile.mkstemp(prefix="editor-", suffix=self.extension)
|
||||
f: t.BinaryIO
|
||||
|
||||
try:
|
||||
with os.fdopen(fd, "wb") as f:
|
||||
f.write(data)
|
||||
|
||||
# If the filesystem resolution is 1 second, like Mac OS
|
||||
# 10.12 Extended, or 2 seconds, like FAT32, and the editor
|
||||
# closes very fast, require_save can fail. Set the modified
|
||||
# time to be 2 seconds in the past to work around this.
|
||||
os.utime(name, (os.path.getatime(name), os.path.getmtime(name) - 2))
|
||||
# Depending on the resolution, the exact value might not be
|
||||
# recorded, so get the new recorded value.
|
||||
timestamp = os.path.getmtime(name)
|
||||
|
||||
self.edit_files((name,))
|
||||
|
||||
if self.require_save and os.path.getmtime(name) == timestamp:
|
||||
return None
|
||||
|
||||
with open(name, "rb") as f:
|
||||
rv = f.read()
|
||||
|
||||
if isinstance(text, (bytes, bytearray)):
|
||||
return rv
|
||||
|
||||
return rv.decode("utf-8-sig").replace("\r\n", "\n")
|
||||
finally:
|
||||
os.unlink(name)
|
||||
|
||||
|
||||
def open_url(url: str, wait: bool = False, locate: bool = False) -> int:
|
||||
import subprocess
|
||||
|
||||
def _unquote_file(url: str) -> str:
|
||||
from urllib.parse import unquote
|
||||
|
||||
if url.startswith("file://"):
|
||||
url = unquote(url[7:])
|
||||
|
||||
return url
|
||||
|
||||
if sys.platform == "darwin":
|
||||
args = ["open"]
|
||||
if wait:
|
||||
args.append("-W")
|
||||
if locate:
|
||||
args.append("-R")
|
||||
args.append(_unquote_file(url))
|
||||
null = open("/dev/null", "w")
|
||||
try:
|
||||
return subprocess.Popen(args, stderr=null).wait()
|
||||
finally:
|
||||
null.close()
|
||||
elif WIN:
|
||||
if locate:
|
||||
url = _unquote_file(url)
|
||||
args = ["explorer", f"/select,{url}"]
|
||||
else:
|
||||
args = ["start"]
|
||||
if wait:
|
||||
args.append("/WAIT")
|
||||
args.append("")
|
||||
args.append(url)
|
||||
try:
|
||||
return subprocess.call(args)
|
||||
except OSError:
|
||||
# Command not found
|
||||
return 127
|
||||
elif CYGWIN:
|
||||
if locate:
|
||||
url = _unquote_file(url)
|
||||
args = ["cygstart", os.path.dirname(url)]
|
||||
else:
|
||||
args = ["cygstart"]
|
||||
if wait:
|
||||
args.append("-w")
|
||||
args.append(url)
|
||||
try:
|
||||
return subprocess.call(args)
|
||||
except OSError:
|
||||
# Command not found
|
||||
return 127
|
||||
|
||||
try:
|
||||
if locate:
|
||||
url = os.path.dirname(_unquote_file(url)) or "."
|
||||
else:
|
||||
url = _unquote_file(url)
|
||||
c = subprocess.Popen(["xdg-open", url])
|
||||
if wait:
|
||||
return c.wait()
|
||||
return 0
|
||||
except OSError:
|
||||
if url.startswith(("http://", "https://")) and not locate and not wait:
|
||||
import webbrowser
|
||||
|
||||
webbrowser.open(url)
|
||||
return 0
|
||||
return 1
|
||||
|
||||
|
||||
def _translate_ch_to_exc(ch: str) -> None:
|
||||
if ch == "\x03":
|
||||
raise KeyboardInterrupt()
|
||||
|
||||
if ch == "\x04" and not WIN: # Unix-like, Ctrl+D
|
||||
raise EOFError()
|
||||
|
||||
if ch == "\x1a" and WIN: # Windows, Ctrl+Z
|
||||
raise EOFError()
|
||||
|
||||
return None
|
||||
|
||||
|
||||
if sys.platform == "win32":
|
||||
import msvcrt
|
||||
|
||||
@contextlib.contextmanager
|
||||
def raw_terminal() -> cabc.Iterator[int]:
|
||||
yield -1
|
||||
|
||||
def getchar(echo: bool) -> str:
|
||||
# The function `getch` will return a bytes object corresponding to
|
||||
# the pressed character. Since Windows 10 build 1803, it will also
|
||||
# return \x00 when called a second time after pressing a regular key.
|
||||
#
|
||||
# `getwch` does not share this probably-bugged behavior. Moreover, it
|
||||
# returns a Unicode object by default, which is what we want.
|
||||
#
|
||||
# Either of these functions will return \x00 or \xe0 to indicate
|
||||
# a special key, and you need to call the same function again to get
|
||||
# the "rest" of the code. The fun part is that \u00e0 is
|
||||
# "latin small letter a with grave", so if you type that on a French
|
||||
# keyboard, you _also_ get a \xe0.
|
||||
# E.g., consider the Up arrow. This returns \xe0 and then \x48. The
|
||||
# resulting Unicode string reads as "a with grave" + "capital H".
|
||||
# This is indistinguishable from when the user actually types
|
||||
# "a with grave" and then "capital H".
|
||||
#
|
||||
# When \xe0 is returned, we assume it's part of a special-key sequence
|
||||
# and call `getwch` again, but that means that when the user types
|
||||
# the \u00e0 character, `getchar` doesn't return until a second
|
||||
# character is typed.
|
||||
# The alternative is returning immediately, but that would mess up
|
||||
# cross-platform handling of arrow keys and others that start with
|
||||
# \xe0. Another option is using `getch`, but then we can't reliably
|
||||
# read non-ASCII characters, because return values of `getch` are
|
||||
# limited to the current 8-bit codepage.
|
||||
#
|
||||
# Anyway, Click doesn't claim to do this Right(tm), and using `getwch`
|
||||
# is doing the right thing in more situations than with `getch`.
|
||||
|
||||
if echo:
|
||||
func = t.cast(t.Callable[[], str], msvcrt.getwche)
|
||||
else:
|
||||
func = t.cast(t.Callable[[], str], msvcrt.getwch)
|
||||
|
||||
rv = func()
|
||||
|
||||
if rv in ("\x00", "\xe0"):
|
||||
# \x00 and \xe0 are control characters that indicate special key,
|
||||
# see above.
|
||||
rv += func()
|
||||
|
||||
_translate_ch_to_exc(rv)
|
||||
return rv
|
||||
|
||||
else:
|
||||
import termios
|
||||
import tty
|
||||
|
||||
@contextlib.contextmanager
|
||||
def raw_terminal() -> cabc.Iterator[int]:
|
||||
f: t.TextIO | None
|
||||
fd: int
|
||||
|
||||
if not isatty(sys.stdin):
|
||||
f = open("/dev/tty")
|
||||
fd = f.fileno()
|
||||
else:
|
||||
fd = sys.stdin.fileno()
|
||||
f = None
|
||||
|
||||
try:
|
||||
old_settings = termios.tcgetattr(fd)
|
||||
|
||||
try:
|
||||
tty.setraw(fd)
|
||||
yield fd
|
||||
finally:
|
||||
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
||||
sys.stdout.flush()
|
||||
|
||||
if f is not None:
|
||||
f.close()
|
||||
except termios.error:
|
||||
pass
|
||||
|
||||
def getchar(echo: bool) -> str:
|
||||
with raw_terminal() as fd:
|
||||
ch = os.read(fd, 32).decode(get_best_encoding(sys.stdin), "replace")
|
||||
|
||||
if echo and isatty(sys.stdout):
|
||||
sys.stdout.write(ch)
|
||||
|
||||
_translate_ch_to_exc(ch)
|
||||
return ch
|
||||
51
venv/Lib/site-packages/click/_textwrap.py
Normal file
51
venv/Lib/site-packages/click/_textwrap.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import collections.abc as cabc
|
||||
import textwrap
|
||||
from contextlib import contextmanager
|
||||
|
||||
|
||||
class TextWrapper(textwrap.TextWrapper):
|
||||
def _handle_long_word(
|
||||
self,
|
||||
reversed_chunks: list[str],
|
||||
cur_line: list[str],
|
||||
cur_len: int,
|
||||
width: int,
|
||||
) -> None:
|
||||
space_left = max(width - cur_len, 1)
|
||||
|
||||
if self.break_long_words:
|
||||
last = reversed_chunks[-1]
|
||||
cut = last[:space_left]
|
||||
res = last[space_left:]
|
||||
cur_line.append(cut)
|
||||
reversed_chunks[-1] = res
|
||||
elif not cur_line:
|
||||
cur_line.append(reversed_chunks.pop())
|
||||
|
||||
@contextmanager
|
||||
def extra_indent(self, indent: str) -> cabc.Iterator[None]:
|
||||
old_initial_indent = self.initial_indent
|
||||
old_subsequent_indent = self.subsequent_indent
|
||||
self.initial_indent += indent
|
||||
self.subsequent_indent += indent
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
self.initial_indent = old_initial_indent
|
||||
self.subsequent_indent = old_subsequent_indent
|
||||
|
||||
def indent_only(self, text: str) -> str:
|
||||
rv = []
|
||||
|
||||
for idx, line in enumerate(text.splitlines()):
|
||||
indent = self.initial_indent
|
||||
|
||||
if idx > 0:
|
||||
indent = self.subsequent_indent
|
||||
|
||||
rv.append(f"{indent}{line}")
|
||||
|
||||
return "\n".join(rv)
|
||||
296
venv/Lib/site-packages/click/_winconsole.py
Normal file
296
venv/Lib/site-packages/click/_winconsole.py
Normal file
@@ -0,0 +1,296 @@
|
||||
# This module is based on the excellent work by Adam Bartoš who
|
||||
# provided a lot of what went into the implementation here in
|
||||
# the discussion to issue1602 in the Python bug tracker.
|
||||
#
|
||||
# There are some general differences in regards to how this works
|
||||
# compared to the original patches as we do not need to patch
|
||||
# the entire interpreter but just work in our little world of
|
||||
# echo and prompt.
|
||||
from __future__ import annotations
|
||||
|
||||
import collections.abc as cabc
|
||||
import io
|
||||
import sys
|
||||
import time
|
||||
import typing as t
|
||||
from ctypes import Array
|
||||
from ctypes import byref
|
||||
from ctypes import c_char
|
||||
from ctypes import c_char_p
|
||||
from ctypes import c_int
|
||||
from ctypes import c_ssize_t
|
||||
from ctypes import c_ulong
|
||||
from ctypes import c_void_p
|
||||
from ctypes import POINTER
|
||||
from ctypes import py_object
|
||||
from ctypes import Structure
|
||||
from ctypes.wintypes import DWORD
|
||||
from ctypes.wintypes import HANDLE
|
||||
from ctypes.wintypes import LPCWSTR
|
||||
from ctypes.wintypes import LPWSTR
|
||||
|
||||
from ._compat import _NonClosingTextIOWrapper
|
||||
|
||||
assert sys.platform == "win32"
|
||||
import msvcrt # noqa: E402
|
||||
from ctypes import windll # noqa: E402
|
||||
from ctypes import WINFUNCTYPE # noqa: E402
|
||||
|
||||
c_ssize_p = POINTER(c_ssize_t)
|
||||
|
||||
kernel32 = windll.kernel32
|
||||
GetStdHandle = kernel32.GetStdHandle
|
||||
ReadConsoleW = kernel32.ReadConsoleW
|
||||
WriteConsoleW = kernel32.WriteConsoleW
|
||||
GetConsoleMode = kernel32.GetConsoleMode
|
||||
GetLastError = kernel32.GetLastError
|
||||
GetCommandLineW = WINFUNCTYPE(LPWSTR)(("GetCommandLineW", windll.kernel32))
|
||||
CommandLineToArgvW = WINFUNCTYPE(POINTER(LPWSTR), LPCWSTR, POINTER(c_int))(
|
||||
("CommandLineToArgvW", windll.shell32)
|
||||
)
|
||||
LocalFree = WINFUNCTYPE(c_void_p, c_void_p)(("LocalFree", windll.kernel32))
|
||||
|
||||
STDIN_HANDLE = GetStdHandle(-10)
|
||||
STDOUT_HANDLE = GetStdHandle(-11)
|
||||
STDERR_HANDLE = GetStdHandle(-12)
|
||||
|
||||
PyBUF_SIMPLE = 0
|
||||
PyBUF_WRITABLE = 1
|
||||
|
||||
ERROR_SUCCESS = 0
|
||||
ERROR_NOT_ENOUGH_MEMORY = 8
|
||||
ERROR_OPERATION_ABORTED = 995
|
||||
|
||||
STDIN_FILENO = 0
|
||||
STDOUT_FILENO = 1
|
||||
STDERR_FILENO = 2
|
||||
|
||||
EOF = b"\x1a"
|
||||
MAX_BYTES_WRITTEN = 32767
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
try:
|
||||
# Using `typing_extensions.Buffer` instead of `collections.abc`
|
||||
# on Windows for some reason does not have `Sized` implemented.
|
||||
from collections.abc import Buffer # type: ignore
|
||||
except ImportError:
|
||||
from typing_extensions import Buffer
|
||||
|
||||
try:
|
||||
from ctypes import pythonapi
|
||||
except ImportError:
|
||||
# On PyPy we cannot get buffers so our ability to operate here is
|
||||
# severely limited.
|
||||
get_buffer = None
|
||||
else:
|
||||
|
||||
class Py_buffer(Structure):
|
||||
_fields_ = [ # noqa: RUF012
|
||||
("buf", c_void_p),
|
||||
("obj", py_object),
|
||||
("len", c_ssize_t),
|
||||
("itemsize", c_ssize_t),
|
||||
("readonly", c_int),
|
||||
("ndim", c_int),
|
||||
("format", c_char_p),
|
||||
("shape", c_ssize_p),
|
||||
("strides", c_ssize_p),
|
||||
("suboffsets", c_ssize_p),
|
||||
("internal", c_void_p),
|
||||
]
|
||||
|
||||
PyObject_GetBuffer = pythonapi.PyObject_GetBuffer
|
||||
PyBuffer_Release = pythonapi.PyBuffer_Release
|
||||
|
||||
def get_buffer(obj: Buffer, writable: bool = False) -> Array[c_char]:
|
||||
buf = Py_buffer()
|
||||
flags: int = PyBUF_WRITABLE if writable else PyBUF_SIMPLE
|
||||
PyObject_GetBuffer(py_object(obj), byref(buf), flags)
|
||||
|
||||
try:
|
||||
buffer_type = c_char * buf.len
|
||||
out: Array[c_char] = buffer_type.from_address(buf.buf)
|
||||
return out
|
||||
finally:
|
||||
PyBuffer_Release(byref(buf))
|
||||
|
||||
|
||||
class _WindowsConsoleRawIOBase(io.RawIOBase):
|
||||
def __init__(self, handle: int | None) -> None:
|
||||
self.handle = handle
|
||||
|
||||
def isatty(self) -> t.Literal[True]:
|
||||
super().isatty()
|
||||
return True
|
||||
|
||||
|
||||
class _WindowsConsoleReader(_WindowsConsoleRawIOBase):
|
||||
def readable(self) -> t.Literal[True]:
|
||||
return True
|
||||
|
||||
def readinto(self, b: Buffer) -> int:
|
||||
bytes_to_be_read = len(b)
|
||||
if not bytes_to_be_read:
|
||||
return 0
|
||||
elif bytes_to_be_read % 2:
|
||||
raise ValueError(
|
||||
"cannot read odd number of bytes from UTF-16-LE encoded console"
|
||||
)
|
||||
|
||||
buffer = get_buffer(b, writable=True)
|
||||
code_units_to_be_read = bytes_to_be_read // 2
|
||||
code_units_read = c_ulong()
|
||||
|
||||
rv = ReadConsoleW(
|
||||
HANDLE(self.handle),
|
||||
buffer,
|
||||
code_units_to_be_read,
|
||||
byref(code_units_read),
|
||||
None,
|
||||
)
|
||||
if GetLastError() == ERROR_OPERATION_ABORTED:
|
||||
# wait for KeyboardInterrupt
|
||||
time.sleep(0.1)
|
||||
if not rv:
|
||||
raise OSError(f"Windows error: {GetLastError()}")
|
||||
|
||||
if buffer[0] == EOF:
|
||||
return 0
|
||||
return 2 * code_units_read.value
|
||||
|
||||
|
||||
class _WindowsConsoleWriter(_WindowsConsoleRawIOBase):
|
||||
def writable(self) -> t.Literal[True]:
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _get_error_message(errno: int) -> str:
|
||||
if errno == ERROR_SUCCESS:
|
||||
return "ERROR_SUCCESS"
|
||||
elif errno == ERROR_NOT_ENOUGH_MEMORY:
|
||||
return "ERROR_NOT_ENOUGH_MEMORY"
|
||||
return f"Windows error {errno}"
|
||||
|
||||
def write(self, b: Buffer) -> int:
|
||||
bytes_to_be_written = len(b)
|
||||
buf = get_buffer(b)
|
||||
code_units_to_be_written = min(bytes_to_be_written, MAX_BYTES_WRITTEN) // 2
|
||||
code_units_written = c_ulong()
|
||||
|
||||
WriteConsoleW(
|
||||
HANDLE(self.handle),
|
||||
buf,
|
||||
code_units_to_be_written,
|
||||
byref(code_units_written),
|
||||
None,
|
||||
)
|
||||
bytes_written = 2 * code_units_written.value
|
||||
|
||||
if bytes_written == 0 and bytes_to_be_written > 0:
|
||||
raise OSError(self._get_error_message(GetLastError()))
|
||||
return bytes_written
|
||||
|
||||
|
||||
class ConsoleStream:
|
||||
def __init__(self, text_stream: t.TextIO, byte_stream: t.BinaryIO) -> None:
|
||||
self._text_stream = text_stream
|
||||
self.buffer = byte_stream
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.buffer.name
|
||||
|
||||
def write(self, x: t.AnyStr) -> int:
|
||||
if isinstance(x, str):
|
||||
return self._text_stream.write(x)
|
||||
try:
|
||||
self.flush()
|
||||
except Exception:
|
||||
pass
|
||||
return self.buffer.write(x)
|
||||
|
||||
def writelines(self, lines: cabc.Iterable[t.AnyStr]) -> None:
|
||||
for line in lines:
|
||||
self.write(line)
|
||||
|
||||
def __getattr__(self, name: str) -> t.Any:
|
||||
return getattr(self._text_stream, name)
|
||||
|
||||
def isatty(self) -> bool:
|
||||
return self.buffer.isatty()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<ConsoleStream name={self.name!r} encoding={self.encoding!r}>"
|
||||
|
||||
|
||||
def _get_text_stdin(buffer_stream: t.BinaryIO) -> t.TextIO:
|
||||
text_stream = _NonClosingTextIOWrapper(
|
||||
io.BufferedReader(_WindowsConsoleReader(STDIN_HANDLE)),
|
||||
"utf-16-le",
|
||||
"strict",
|
||||
line_buffering=True,
|
||||
)
|
||||
return t.cast(t.TextIO, ConsoleStream(text_stream, buffer_stream))
|
||||
|
||||
|
||||
def _get_text_stdout(buffer_stream: t.BinaryIO) -> t.TextIO:
|
||||
text_stream = _NonClosingTextIOWrapper(
|
||||
io.BufferedWriter(_WindowsConsoleWriter(STDOUT_HANDLE)),
|
||||
"utf-16-le",
|
||||
"strict",
|
||||
line_buffering=True,
|
||||
)
|
||||
return t.cast(t.TextIO, ConsoleStream(text_stream, buffer_stream))
|
||||
|
||||
|
||||
def _get_text_stderr(buffer_stream: t.BinaryIO) -> t.TextIO:
|
||||
text_stream = _NonClosingTextIOWrapper(
|
||||
io.BufferedWriter(_WindowsConsoleWriter(STDERR_HANDLE)),
|
||||
"utf-16-le",
|
||||
"strict",
|
||||
line_buffering=True,
|
||||
)
|
||||
return t.cast(t.TextIO, ConsoleStream(text_stream, buffer_stream))
|
||||
|
||||
|
||||
_stream_factories: cabc.Mapping[int, t.Callable[[t.BinaryIO], t.TextIO]] = {
|
||||
0: _get_text_stdin,
|
||||
1: _get_text_stdout,
|
||||
2: _get_text_stderr,
|
||||
}
|
||||
|
||||
|
||||
def _is_console(f: t.TextIO) -> bool:
|
||||
if not hasattr(f, "fileno"):
|
||||
return False
|
||||
|
||||
try:
|
||||
fileno = f.fileno()
|
||||
except (OSError, io.UnsupportedOperation):
|
||||
return False
|
||||
|
||||
handle = msvcrt.get_osfhandle(fileno)
|
||||
return bool(GetConsoleMode(handle, byref(DWORD())))
|
||||
|
||||
|
||||
def _get_windows_console_stream(
|
||||
f: t.TextIO, encoding: str | None, errors: str | None
|
||||
) -> t.TextIO | None:
|
||||
if (
|
||||
get_buffer is None
|
||||
or encoding not in {"utf-16-le", None}
|
||||
or errors not in {"strict", None}
|
||||
or not _is_console(f)
|
||||
):
|
||||
return None
|
||||
|
||||
func = _stream_factories.get(f.fileno())
|
||||
if func is None:
|
||||
return None
|
||||
|
||||
b = getattr(f, "buffer", None)
|
||||
|
||||
if b is None:
|
||||
return None
|
||||
|
||||
return func(b)
|
||||
3135
venv/Lib/site-packages/click/core.py
Normal file
3135
venv/Lib/site-packages/click/core.py
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user