Compare commits
65
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a560273c3f | ||
|
|
d95ce1dc7a | ||
|
|
bd8dd3b93d | ||
|
|
b3e246a6ce | ||
|
|
57a5afbe48 | ||
|
|
4320b4cba9 | ||
|
|
a8ce535985 | ||
|
|
817323cb62 | ||
|
|
ab875e8fdb | ||
|
|
233ba727a4 | ||
|
|
08441195fe | ||
|
|
52056282df | ||
|
|
5e009d46e8 | ||
|
|
be7a112038 | ||
|
|
8c145c0d01 | ||
|
|
d4bb3c446e | ||
|
|
12bcc0aa69 | ||
|
|
090fb44600 | ||
|
|
7cc6503b94 | ||
|
|
cca9d94b36 | ||
|
|
2da85c4d84 | ||
|
|
4e94468315 | ||
|
|
ae55f36a0a | ||
|
|
e6937617a1 | ||
|
|
1824c9fe59 | ||
|
|
0bc06358e8 | ||
|
|
77db374418 | ||
|
|
e9d2bef2ae | ||
|
|
f514a577c8 | ||
|
|
3e03fd7e8c | ||
|
|
0fc3ad49ea | ||
|
|
0e3117e408 | ||
|
|
88dcb8a475 | ||
|
|
7bbb46398e | ||
|
|
2fc7e773e0 | ||
|
|
784517e886 | ||
|
|
ca99cd3853 | ||
|
|
a62b936920 | ||
|
|
4e808c3be9 | ||
|
|
80250035e5 | ||
|
|
fb908e00b7 | ||
|
|
9804b6d673 | ||
|
|
4dfe50db93 | ||
|
|
1c43e5ba06 | ||
|
|
a85e06a19b | ||
|
|
12f03e8bc9 | ||
|
|
912ad15c28 | ||
|
|
3b8a01160d | ||
|
|
0f60a31ef8 | ||
|
|
f0e1d79e92 | ||
|
|
9a106d366b | ||
|
|
ae459d2171 | ||
|
|
bef441db88 | ||
|
|
72bac66682 | ||
|
|
60b687305e | ||
|
|
43e19b200d | ||
|
|
7f5210ab57 | ||
|
|
3683a6ffc1 | ||
|
|
aa2dc74ff7 | ||
|
|
9b5d04183d | ||
|
|
83769ce7db | ||
|
|
46dda6166a | ||
|
|
d9e64cf212 | ||
|
|
0c41ac2f84 | ||
|
|
7cd8fe6541 |
No files matched your search
@@ -0,0 +1,83 @@
|
|||||||
|
# studspider Environment Configuration
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Square API Configuration
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Environment: "production" or "sandbox"
|
||||||
|
SQUARE_ENV=sandbox
|
||||||
|
|
||||||
|
# Sandbox credentials
|
||||||
|
SANDBOX_SQUARE_APP_ID=your_sandbox_app_id
|
||||||
|
SANDBOX_SQUARE_TOKEN=your_sandbox_access_token
|
||||||
|
|
||||||
|
# Production credentials
|
||||||
|
PROD_SQUARE_APP_ID=your_production_app_id
|
||||||
|
PROD_SQUARE_TOKEN=your_production_access_token
|
||||||
|
|
||||||
|
# Square LEGO category ID (find this in Square Dashboard)
|
||||||
|
SQUARE_LEGO_CAT_ID=your_lego_category_id
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Brickset API Configuration
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
BRICKSET_USERNAME=your_brickset_username
|
||||||
|
BRICKSET_PASS=your_brickset_password
|
||||||
|
BRICKSET_API_KEY=your_brickset_api_key
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# SSL Certificate Configuration (Optional)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# NOTE: Only needed if running HTTPS directly in Python (not recommended)
|
||||||
|
# For production, use nginx reverse proxy with Let's Encrypt instead
|
||||||
|
# nginx handles SSL/TLS, your Python app runs HTTP internally
|
||||||
|
|
||||||
|
# Path to SSL certificate in PEM format (if running HTTPS directly)
|
||||||
|
# SSL_CERT_PATH=/path/to/cert.pem
|
||||||
|
|
||||||
|
# Path to SSL private key in PEM format (if running HTTPS directly)
|
||||||
|
# SSL_KEY_PATH=/path/to/key.pem
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Square Webhook Signature Verification
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# CRITICAL: Get these from Square Developer Dashboard → Webhooks
|
||||||
|
# 1. Go to https://developer.squareup.com/apps
|
||||||
|
# 2. Select your application
|
||||||
|
# 3. Click "Webhooks" section
|
||||||
|
# 4. Create webhook subscription (Notification URL + Events)
|
||||||
|
# 5. Copy the Signature Key that Square displays
|
||||||
|
|
||||||
|
# Signature key from Square Dashboard (a string like "asdf1234")
|
||||||
|
SQUARE_WEBHOOK_SIGNATURE_KEY=your_webhook_signature_key_from_square
|
||||||
|
|
||||||
|
# Your webhook notification URL (must match EXACTLY what's in Square Dashboard)
|
||||||
|
# This should be your public HTTPS URL (through nginx)
|
||||||
|
# Example: https://yourdomain.com/hook
|
||||||
|
SQUARE_WEBHOOK_URL=https://yourdomain.com/hook
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Server Configuration (Optional)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Default host and port (can override with command line args)
|
||||||
|
# SERVER_HOST=0.0.0.0
|
||||||
|
# SERVER_PORT=7432
|
||||||
|
# SERVER_HOOK_PORT=8080
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Usage Examples
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Basic HTTP server (recommended with nginx):
|
||||||
|
# python src/test_server.py
|
||||||
|
#
|
||||||
|
# With SSL from .env:
|
||||||
|
# Set SSL_CERT_PATH and SSL_KEY_PATH above, then:
|
||||||
|
# python src/test_server.py --hook-port 8443 --hook-ssl-only
|
||||||
|
#
|
||||||
|
# Override SSL from command line:
|
||||||
|
# python src/test_server.py --ssl-cert /custom/cert.pem --ssl-key /custom/key.pem
|
||||||
+15
-1
@@ -25,5 +25,19 @@ square-src.code-workspace
|
|||||||
|
|
||||||
doc/pw-inventory/
|
doc/pw-inventory/
|
||||||
|
|
||||||
edderkop.db-journal
|
studspider.db-journal
|
||||||
.venv
|
.venv
|
||||||
|
*.db
|
||||||
|
*.py[cod]
|
||||||
|
.web
|
||||||
|
.states
|
||||||
|
assets/external/
|
||||||
|
__pycache__/
|
||||||
|
win/
|
||||||
|
.github/
|
||||||
|
bin/
|
||||||
|
activate/
|
||||||
|
build/
|
||||||
|
.pem
|
||||||
|
.p7b
|
||||||
|
nginx/
|
||||||
@@ -1,12 +1,43 @@
|
|||||||
# Edderkop
|
# studspider
|
||||||
|
|
||||||
environment: poetry
|
  
|
||||||
|
|
||||||
code style: black
|
|
||||||
|
|
||||||
built with
|
|
||||||
|
|
||||||
|
is a tool used to talk to brickset.com and a square inventory to pull lego set information for entering in the square db
|
||||||
|
## Run me
|
||||||
|
|
||||||
|
Clone the project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone http://git.traps.moe/ph0sgene/studspider.git
|
||||||
|
git checkout uv
|
||||||
|
```
|
||||||
|
|
||||||
|
Go to the project directory
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd studspider
|
||||||
|
```
|
||||||
|
|
||||||
|
Install dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv sync && uv lock
|
||||||
|
```
|
||||||
|
|
||||||
|
Start the server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvx run studspider.py
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Tech
|
||||||
|
|
||||||
- aiohttp
|
- aiohttp
|
||||||
- squareup
|
- squareup
|
||||||
- apscheduler
|
- apscheduler
|
||||||
- aiosqlite
|
- aiosqlite
|
||||||
|
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
# Understanding PEM vs P7B Certificates
|
||||||
|
|
||||||
|
## Quick Summary
|
||||||
|
|
||||||
|
**You CANNOT use P7B files to run your HTTPS server!**
|
||||||
|
|
||||||
|
- **PEM files** = Run your server with HTTPS ✓
|
||||||
|
- **P7B files** = Upload to Square Dashboard ✓
|
||||||
|
|
||||||
|
## The Complete Picture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Your HTTPS Server │
|
||||||
|
│ │
|
||||||
|
│ Uses: cert.pem + key.pem (PEM format) │
|
||||||
|
│ Command: python src/test_server.py │
|
||||||
|
│ --ssl-cert cert.pem │
|
||||||
|
│ --ssl-key key.pem │
|
||||||
|
│ --hook-port 8443 │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
│ HTTPS traffic
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Square's Servers │
|
||||||
|
│ │
|
||||||
|
│ They verify your server using: cert.p7b │
|
||||||
|
│ (which YOU upload to Square Dashboard) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Why Two Formats?
|
||||||
|
|
||||||
|
### PEM Format (.pem files)
|
||||||
|
- **Contains**: Certificate AND private key (separate files)
|
||||||
|
- **Used by**: Your HTTPS server to encrypt traffic
|
||||||
|
- **Files needed**:
|
||||||
|
- `cert.pem` - Your public certificate
|
||||||
|
- `key.pem` - Your private key (NEVER share this!)
|
||||||
|
|
||||||
|
### P7B Format (.p7b files)
|
||||||
|
- **Contains**: Certificate ONLY (no private key)
|
||||||
|
- **Used by**: Square to verify webhook signatures
|
||||||
|
- **File**: `cert.p7b` - Uploaded to Square Dashboard
|
||||||
|
|
||||||
|
## Step-by-Step Workflow
|
||||||
|
|
||||||
|
### 1. Generate Certificates (PEM format)
|
||||||
|
```powershell
|
||||||
|
python generate_cert.py
|
||||||
|
```
|
||||||
|
**Creates:**
|
||||||
|
- `cert.pem` - Your certificate (public)
|
||||||
|
- `key.pem` - Your private key (secret!)
|
||||||
|
|
||||||
|
### 2. Start Your HTTPS Server (uses PEM)
|
||||||
|
```powershell
|
||||||
|
python src/test_server.py --ssl-cert cert.pem --ssl-key key.pem --hook-port 8443 --hook-ssl-only
|
||||||
|
```
|
||||||
|
**Server runs at:**
|
||||||
|
- `https://localhost:8443/hook`
|
||||||
|
|
||||||
|
### 3. Convert Certificate to P7B for Square
|
||||||
|
```powershell
|
||||||
|
python convert_to_p7b.py
|
||||||
|
```
|
||||||
|
**Creates:**
|
||||||
|
- `cert.p7b` - Certificate in P7B format (for Square)
|
||||||
|
|
||||||
|
### 4. Upload P7B to Square
|
||||||
|
1. Go to: https://developer.squareup.com/apps
|
||||||
|
2. Your App → **Webhooks** → **Manage Notification Settings**
|
||||||
|
3. Upload `cert.p7b`
|
||||||
|
|
||||||
|
### 5. Configure Square Webhook URL
|
||||||
|
- Enter: `https://yourdomain.com:8443/hook`
|
||||||
|
- Square will verify using the P7B you uploaded
|
||||||
|
|
||||||
|
## Common Mistakes
|
||||||
|
|
||||||
|
### ❌ Trying to use P7B with your server
|
||||||
|
```powershell
|
||||||
|
# THIS WON'T WORK!
|
||||||
|
python src/test_server.py --ssl-cert cert.p7b --ssl-key key.p7b
|
||||||
|
```
|
||||||
|
**Error:** P7B doesn't contain the private key needed for HTTPS!
|
||||||
|
|
||||||
|
### ✓ Correct usage
|
||||||
|
```powershell
|
||||||
|
# Server uses PEM files
|
||||||
|
python src/test_server.py --ssl-cert cert.pem --ssl-key key.pem --hook-port 8443
|
||||||
|
|
||||||
|
# P7B file is uploaded to Square Dashboard (not used by your server)
|
||||||
|
```
|
||||||
|
|
||||||
|
## If You Only Have a P7B File
|
||||||
|
|
||||||
|
If someone gave you a P7B file, you can extract the certificate:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python convert_from_p7b.py your_cert.p7b --output cert.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
**BUT:** You still need the private key (`key.pem`) to run HTTPS!
|
||||||
|
|
||||||
|
If you don't have the private key, you must:
|
||||||
|
1. Generate a new certificate pair: `python generate_cert.py`
|
||||||
|
2. Or get the private key from whoever created the P7B
|
||||||
|
|
||||||
|
## File Checklist
|
||||||
|
|
||||||
|
To run your HTTPS server with Square webhooks, you need:
|
||||||
|
|
||||||
|
- [ ] `cert.pem` - Certificate (PEM format) - For your server
|
||||||
|
- [ ] `key.pem` - Private key (PEM format) - For your server
|
||||||
|
- [ ] `cert.p7b` - Certificate (P7B format) - Upload to Square
|
||||||
|
|
||||||
|
Generate all three with:
|
||||||
|
```powershell
|
||||||
|
python generate_cert.py # Creates cert.pem and key.pem
|
||||||
|
python convert_to_p7b.py # Creates cert.p7b from cert.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
- **Never share `key.pem`** - This is your private key!
|
||||||
|
- **`cert.pem` is public** - Safe to share
|
||||||
|
- **`cert.p7b` is public** - You upload this to Square
|
||||||
|
- Self-signed certificates are fine for testing but use Let's Encrypt for production
|
||||||
|
|
||||||
|
## Need Help?
|
||||||
|
|
||||||
|
See the detailed guides:
|
||||||
|
- `HTTPS_SETUP.md` - Complete HTTPS setup instructions
|
||||||
|
- `SQUARE_WEBHOOK_SETUP.md` - Square webhook configuration
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
# HTTPS Webhook Setup Guide
|
||||||
|
|
||||||
|
## Quick Start (Self-Signed Certificate for Testing)
|
||||||
|
|
||||||
|
### 1. Generate Certificate
|
||||||
|
```powershell
|
||||||
|
python generate_cert.py
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates `cert.pem` and `key.pem` files.
|
||||||
|
|
||||||
|
### 1.5. Convert to P7B for Square (Required for Square Webhooks)
|
||||||
|
```powershell
|
||||||
|
python convert_to_p7b.py
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates `cert.p7b` which Square needs for webhook signature verification.
|
||||||
|
Upload this P7B file to Square Developer Dashboard → Webhooks → Manage Notification Settings.
|
||||||
|
|
||||||
|
### 2. Run Server with HTTPS
|
||||||
|
|
||||||
|
#### Option A: HTTPS for webhook only (recommended for development)
|
||||||
|
```powershell
|
||||||
|
python src/test_server.py --ssl-cert cert.pem --ssl-key key.pem --hook-port 8443 --hook-ssl-only
|
||||||
|
```
|
||||||
|
- Main server: `http://localhost:7432/` (HTTP)
|
||||||
|
- Webhook: `https://localhost:8443/hook` (HTTPS)
|
||||||
|
|
||||||
|
#### Option B: HTTPS for everything
|
||||||
|
```powershell
|
||||||
|
python src/test_server.py --ssl-cert cert.pem --ssl-key key.pem --hook-port 8443
|
||||||
|
```
|
||||||
|
- Main server: `https://localhost:7432/` (HTTPS)
|
||||||
|
- Webhook: `https://localhost:8443/hook` (HTTPS)
|
||||||
|
|
||||||
|
#### Option C: Single port with HTTPS
|
||||||
|
```powershell
|
||||||
|
python src/test_server.py --ssl-cert cert.pem --ssl-key key.pem
|
||||||
|
```
|
||||||
|
- Everything on: `https://localhost:7432/`
|
||||||
|
|
||||||
|
### 3. Test the Webhook
|
||||||
|
|
||||||
|
Update `src/test_webhook.py` to use HTTPS:
|
||||||
|
```python
|
||||||
|
url = "https://localhost:8443/hook"
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run:
|
||||||
|
```powershell
|
||||||
|
python src/test_webhook.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production Setup (with Real Certificate)
|
||||||
|
|
||||||
|
### Option 1: Let's Encrypt (Free, Recommended)
|
||||||
|
|
||||||
|
On Linux/WSL:
|
||||||
|
```bash
|
||||||
|
# Install certbot
|
||||||
|
sudo apt-get install certbot
|
||||||
|
|
||||||
|
# Generate certificate (replace yourdomain.com)
|
||||||
|
sudo certbot certonly --standalone -d yourdomain.com
|
||||||
|
|
||||||
|
# Certificates will be at:
|
||||||
|
# /etc/letsencrypt/live/yourdomain.com/fullchain.pem
|
||||||
|
# /etc/letsencrypt/live/yourdomain.com/privkey.pem
|
||||||
|
|
||||||
|
# Run server
|
||||||
|
python src/test_server.py \
|
||||||
|
--ssl-cert /etc/letsencrypt/live/yourdomain.com/fullchain.pem \
|
||||||
|
--ssl-key /etc/letsencrypt/live/yourdomain.com/privkey.pem \
|
||||||
|
--host 0.0.0.0 \
|
||||||
|
--hook-port 443 \
|
||||||
|
--hook-ssl-only
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: Reverse Proxy (nginx/Caddy)
|
||||||
|
|
||||||
|
Run studspider on HTTP internally, use nginx/Caddy for HTTPS:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
# nginx config
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name yourdomain.com;
|
||||||
|
|
||||||
|
ssl_certificate /path/to/cert.pem;
|
||||||
|
ssl_certificate_key /path/to/key.pem;
|
||||||
|
|
||||||
|
location /hook {
|
||||||
|
proxy_pass http://localhost:7432/hook;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run studspider normally:
|
||||||
|
```powershell
|
||||||
|
python src/test_server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "OpenSSL not found" Error
|
||||||
|
- **Windows**: Install from https://slproweb.com/products/Win32OpenSSL.html
|
||||||
|
- **Alternative**: Use Git Bash or WSL which include OpenSSL
|
||||||
|
|
||||||
|
### Self-Signed Certificate Warnings
|
||||||
|
- Normal for development
|
||||||
|
- Use `--insecure` flag with curl/tools for testing
|
||||||
|
- For production, get a proper certificate from Let's Encrypt
|
||||||
|
|
||||||
|
### Port 443 Already in Use
|
||||||
|
- Use a different port like 8443
|
||||||
|
- Or stop the service using port 443
|
||||||
|
|
||||||
|
### Certificate File Not Found
|
||||||
|
- Use absolute paths: `--ssl-cert C:\path\to\cert.pem`
|
||||||
|
- Or run from the directory containing the certificates
|
||||||
|
|
||||||
|
## Square Webhook Configuration
|
||||||
|
|
||||||
|
### Certificate Requirements
|
||||||
|
Square requires a **P7B (PKCS#7)** certificate file for webhook signature verification.
|
||||||
|
|
||||||
|
### Setup Steps:
|
||||||
|
|
||||||
|
1. **Generate your certificate** (if you haven't already):
|
||||||
|
```powershell
|
||||||
|
python generate_cert.py
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Convert PEM to P7B format**:
|
||||||
|
```powershell
|
||||||
|
# Basic conversion
|
||||||
|
python convert_to_p7b.py
|
||||||
|
|
||||||
|
# With intermediate certificates (production)
|
||||||
|
python convert_to_p7b.py --cert fullchain.pem --chain intermediate.pem --output square.p7b
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Upload to Square Dashboard**:
|
||||||
|
- Go to https://developer.squareup.com/
|
||||||
|
- Navigate to your application
|
||||||
|
- Click on "Webhooks"
|
||||||
|
- Click "Manage Notification Settings"
|
||||||
|
- Upload the `cert.p7b` file
|
||||||
|
- Square will use this to verify webhook signatures
|
||||||
|
|
||||||
|
4. **Configure webhook URL in Square**:
|
||||||
|
- Use your public HTTPS URL: `https://yourdomain.com:8443/hook`
|
||||||
|
- Square requires valid SSL (not self-signed for production)
|
||||||
|
|
||||||
|
### Testing with Square Webhooks:
|
||||||
|
|
||||||
|
When setting up webhooks in Square Dashboard:
|
||||||
|
1. Use your HTTPS URL: `https://yourdomain.com:8443/hook`
|
||||||
|
2. Square requires valid SSL (not self-signed)
|
||||||
|
3. Consider using ngrok for local testing:
|
||||||
|
```powershell
|
||||||
|
ngrok http 8443
|
||||||
|
```
|
||||||
|
Then use the ngrok HTTPS URL in Square Dashboard
|
||||||
@@ -0,0 +1,356 @@
|
|||||||
|
# Nginx Reverse Proxy Configuration for studspider
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
When using nginx as a reverse proxy:
|
||||||
|
- **Nginx handles HTTPS** (SSL/TLS termination)
|
||||||
|
- **studspider runs on HTTP** internally (no SSL needed)
|
||||||
|
- **nginx forwards requests** to studspider
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Internet (HTTPS) → nginx (443) → studspider (7432/HTTP)
|
||||||
|
↓
|
||||||
|
Handles SSL certificates
|
||||||
|
Handles HTTPS traffic
|
||||||
|
Forwards to HTTP backend
|
||||||
|
```
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
✓ **No SSL certificates needed in studspider**
|
||||||
|
✓ **nginx handles SSL efficiently**
|
||||||
|
✓ **Easy certificate renewal** (Let's Encrypt)
|
||||||
|
✓ **Better performance** (nginx is optimized for this)
|
||||||
|
✓ **Additional security** (rate limiting, IP filtering, etc.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration Examples
|
||||||
|
|
||||||
|
### Basic Configuration (HTTP → HTTPS redirect + webhook proxy)
|
||||||
|
|
||||||
|
Create `/etc/nginx/sites-available/studspider`:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
# Redirect HTTP to HTTPS
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name studspider.sh www.studspider.sh;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTTPS server
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
server_name studspider.sh www.studspider.sh;
|
||||||
|
|
||||||
|
# SSL certificates (Let's Encrypt)
|
||||||
|
ssl_certificate /etc/letsencrypt/live/studspider.sh/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/studspider.sh/privkey.pem;
|
||||||
|
|
||||||
|
# SSL configuration (modern, secure)
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||||
|
ssl_prefer_server_ciphers on;
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
access_log /var/log/nginx/studspider_access.log;
|
||||||
|
error_log /var/log/nginx/studspider_error.log;
|
||||||
|
|
||||||
|
# Webhook endpoint (Square webhooks)
|
||||||
|
location /hook {
|
||||||
|
proxy_pass http://localhost:7432/hook;
|
||||||
|
|
||||||
|
# Forward headers
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
# Timeouts
|
||||||
|
proxy_connect_timeout 60s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
|
||||||
|
# Disable buffering for real-time responses
|
||||||
|
proxy_buffering off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main API endpoint (optional)
|
||||||
|
location / {
|
||||||
|
proxy_pass http://localhost:7432/;
|
||||||
|
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Separate Port Configuration (webhook on different port)
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
server_name studspider.sh;
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/studspider.sh/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/studspider.sh/privkey.pem;
|
||||||
|
|
||||||
|
# Main API
|
||||||
|
location / {
|
||||||
|
proxy_pass http://localhost:7432/;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto https;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Separate server for webhooks on different port
|
||||||
|
server {
|
||||||
|
listen 8443 ssl http2;
|
||||||
|
server_name studspider.sh;
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/studspider.sh/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/studspider.sh/privkey.pem;
|
||||||
|
|
||||||
|
# Webhook only
|
||||||
|
location /hook {
|
||||||
|
proxy_pass http://localhost:8080/hook; # studspider webhook port
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto https;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Rate Limiting (recommended for webhooks)
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
# Define rate limit zone (outside server block)
|
||||||
|
limit_req_zone $binary_remote_addr zone=webhook_limit:10m rate=10r/s;
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
server_name studspider.sh;
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/studspider.sh/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/studspider.sh/privkey.pem;
|
||||||
|
|
||||||
|
# Webhook with rate limiting
|
||||||
|
location /hook {
|
||||||
|
# Rate limit: 10 requests per second per IP
|
||||||
|
limit_req zone=webhook_limit burst=20 nodelay;
|
||||||
|
|
||||||
|
# Only allow POST requests
|
||||||
|
limit_except POST {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy_pass http://localhost:7432/hook;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto https;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Setup Steps
|
||||||
|
|
||||||
|
### 1. Install nginx (if not already installed)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ubuntu/Debian
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install nginx
|
||||||
|
|
||||||
|
# CentOS/RHEL
|
||||||
|
sudo yum install nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Get SSL Certificate (Let's Encrypt - Free!)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install certbot
|
||||||
|
sudo apt install certbot python3-certbot-nginx
|
||||||
|
|
||||||
|
# Get certificate for your domain
|
||||||
|
sudo certbot --nginx -d studspider.sh -d www.studspider.sh
|
||||||
|
|
||||||
|
# Certbot will automatically:
|
||||||
|
# 1. Generate certificate
|
||||||
|
# 2. Configure nginx
|
||||||
|
# 3. Set up auto-renewal
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Create nginx Configuration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create config file
|
||||||
|
sudo nano /etc/nginx/sites-available/studspider
|
||||||
|
|
||||||
|
# Paste one of the configurations above
|
||||||
|
|
||||||
|
# Create symbolic link to enable site
|
||||||
|
sudo ln -s /etc/nginx/sites-available/studspider /etc/nginx/sites-enabled/
|
||||||
|
|
||||||
|
# Test configuration
|
||||||
|
sudo nginx -t
|
||||||
|
|
||||||
|
# Reload nginx
|
||||||
|
sudo systemctl reload nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Start studspider (HTTP only, no SSL needed!)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Simple - single port
|
||||||
|
python src/test_server.py --port 7432
|
||||||
|
|
||||||
|
# Or with separate webhook port
|
||||||
|
python src/test_server.py --port 7432 --hook-port 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Configure Square Webhook URL
|
||||||
|
|
||||||
|
In Square Developer Dashboard:
|
||||||
|
- **Webhook URL**: `https://studspider.sh/hook`
|
||||||
|
- **Certificate**: Upload the P7B file (see below)
|
||||||
|
|
||||||
|
### 6. Generate P7B for Square
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Convert your Let's Encrypt certificate to P7B for Square
|
||||||
|
python convert_to_p7b.py --cert /etc/letsencrypt/live/studspider.sh/fullchain.pem --output square.p7b
|
||||||
|
|
||||||
|
# Upload square.p7b to Square Dashboard
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Systemd Service (Run studspider at boot)
|
||||||
|
|
||||||
|
Create `/etc/systemd/system/studspider.service`:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=studspider - LEGO Inventory Sync Service
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=www-data
|
||||||
|
WorkingDirectory=/opt/studspider
|
||||||
|
Environment="PATH=/opt/studspider/.venv/bin"
|
||||||
|
ExecStart=/opt/studspider/.venv/bin/python src/test_server.py --port 7432
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
StandardOutput=append:/var/log/studspider/stdout.log
|
||||||
|
StandardError=append:/var/log/studspider/stderr.log
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
Enable and start:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create log directory
|
||||||
|
sudo mkdir -p /var/log/studspider
|
||||||
|
sudo chown www-data:www-data /var/log/studspider
|
||||||
|
|
||||||
|
# Enable service
|
||||||
|
sudo systemctl enable studspider
|
||||||
|
|
||||||
|
# Start service
|
||||||
|
sudo systemctl start studspider
|
||||||
|
|
||||||
|
# Check status
|
||||||
|
sudo systemctl status studspider
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
sudo journalctl -u studspider -f
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Test nginx configuration
|
||||||
|
```bash
|
||||||
|
sudo nginx -t
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test studspider locally
|
||||||
|
```bash
|
||||||
|
curl http://localhost:7432/hook -X POST -H "Content-Type: application/json" -d '{"test":"data"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test through nginx
|
||||||
|
```bash
|
||||||
|
curl https://studspider.sh/hook -X POST -H "Content-Type: application/json" -d '{"test":"data"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Monitor logs
|
||||||
|
```bash
|
||||||
|
# nginx logs
|
||||||
|
sudo tail -f /var/log/nginx/studspider_access.log
|
||||||
|
sudo tail -f /var/log/nginx/studspider_error.log
|
||||||
|
|
||||||
|
# studspider logs
|
||||||
|
tail -f ./logs/studspider.log
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### 502 Bad Gateway
|
||||||
|
- Check if studspider is running: `ps aux | grep test_server.py`
|
||||||
|
- Check the port matches nginx config
|
||||||
|
- Check firewall isn't blocking internal connections
|
||||||
|
|
||||||
|
### Certificate Errors
|
||||||
|
- Ensure Let's Encrypt renewal is working: `sudo certbot renew --dry-run`
|
||||||
|
- Check certificate files exist and have correct permissions
|
||||||
|
|
||||||
|
### Webhook Not Receiving
|
||||||
|
- Verify nginx is forwarding: Check nginx access logs
|
||||||
|
- Verify studspider is receiving: Check studspider logs
|
||||||
|
- Test locally first, then through nginx
|
||||||
|
|
||||||
|
### Connection Refused
|
||||||
|
- Check studspider is listening: `sudo netstat -tlnp | grep 7432`
|
||||||
|
- Check nginx proxy_pass points to correct host:port
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
### With nginx reverse proxy:
|
||||||
|
|
||||||
|
✓ **studspider runs simple HTTP** (no SSL certificates needed)
|
||||||
|
```bash
|
||||||
|
python src/test_server.py --port 7432
|
||||||
|
```
|
||||||
|
|
||||||
|
✓ **nginx handles HTTPS** (manages certificates automatically)
|
||||||
|
|
||||||
|
✓ **Square gets HTTPS webhook** (`https://studspider.sh/hook`)
|
||||||
|
|
||||||
|
✓ **You upload P7B to Square** (converted from nginx's certificate)
|
||||||
|
|
||||||
|
### This is the recommended production setup!
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
# Square Webhook Setup - Quick Reference
|
||||||
|
|
||||||
|
## Complete Setup Process
|
||||||
|
|
||||||
|
### Step 1: Generate SSL Certificate
|
||||||
|
```powershell
|
||||||
|
# Generate PEM certificate
|
||||||
|
python generate_cert.py
|
||||||
|
# Creates: cert.pem, key.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Convert to P7B for Square
|
||||||
|
```powershell
|
||||||
|
# Convert PEM to P7B (PKCS#7) format
|
||||||
|
python convert_to_p7b.py
|
||||||
|
# Creates: cert.p7b
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Start HTTPS Server
|
||||||
|
```powershell
|
||||||
|
# Start webhook server with HTTPS
|
||||||
|
python src/test_server.py --ssl-cert cert.pem --ssl-key key.pem --hook-port 8443 --hook-ssl-only
|
||||||
|
|
||||||
|
# Server URLs:
|
||||||
|
# Main: http://localhost:7432/
|
||||||
|
# Webhook: https://localhost:8443/hook (or https://yourdomain.com:8443/hook)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Configure Square Webhook
|
||||||
|
|
||||||
|
1. **Upload P7B Certificate to Square:**
|
||||||
|
- Go to: https://developer.squareup.com/apps
|
||||||
|
- Select your application
|
||||||
|
- Navigate to: **Webhooks** → **Manage Notification Settings**
|
||||||
|
- Upload `cert.p7b` file
|
||||||
|
- This allows Square to verify webhook signatures
|
||||||
|
|
||||||
|
2. **Add Webhook Subscription:**
|
||||||
|
- Still in Webhooks section
|
||||||
|
- Click **"Add Subscription"**
|
||||||
|
- Enter URL: `https://yourdomain.com:8443/hook`
|
||||||
|
- Select events to subscribe to (e.g., `inventory.count.updated`)
|
||||||
|
- Click **Save**
|
||||||
|
|
||||||
|
### Step 5: Test the Webhook
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Test from command line
|
||||||
|
python src/test_webhook.py
|
||||||
|
|
||||||
|
# Or manually with curl
|
||||||
|
curl -X POST https://localhost:8443/hook \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"type":"inventory.count.updated","merchant_id":"test"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production Deployment
|
||||||
|
|
||||||
|
### Using Let's Encrypt (Recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Get Let's Encrypt certificate (Linux/WSL)
|
||||||
|
sudo certbot certonly --standalone -d yourdomain.com
|
||||||
|
|
||||||
|
# 2. Convert to P7B for Square
|
||||||
|
python convert_to_p7b.py \
|
||||||
|
--cert /etc/letsencrypt/live/yourdomain.com/fullchain.pem \
|
||||||
|
--output square.p7b
|
||||||
|
|
||||||
|
# 3. Upload square.p7b to Square Dashboard
|
||||||
|
|
||||||
|
# 4. Run server
|
||||||
|
python src/test_server.py \
|
||||||
|
--ssl-cert /etc/letsencrypt/live/yourdomain.com/fullchain.pem \
|
||||||
|
--ssl-key /etc/letsencrypt/live/yourdomain.com/privkey.pem \
|
||||||
|
--host 0.0.0.0 \
|
||||||
|
--hook-port 443 \
|
||||||
|
--hook-ssl-only
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using ngrok (Easy Testing)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Terminal 1: Start your server (HTTP is fine)
|
||||||
|
python src/test_server.py --hook-port 8080
|
||||||
|
|
||||||
|
# Terminal 2: Expose with ngrok (provides HTTPS automatically)
|
||||||
|
ngrok http 8080
|
||||||
|
|
||||||
|
# Use the ngrok HTTPS URL in Square:
|
||||||
|
# Example: https://abc123.ngrok.io/hook
|
||||||
|
|
||||||
|
# For P7B certificate with ngrok:
|
||||||
|
# ngrok provides its own certificate, so you don't need to upload one to Square
|
||||||
|
```
|
||||||
|
|
||||||
|
## Webhook Event Types
|
||||||
|
|
||||||
|
Common Square webhook events you can subscribe to:
|
||||||
|
|
||||||
|
- `inventory.count.updated` - Inventory quantity changes
|
||||||
|
- `catalog.version.updated` - Catalog item updates
|
||||||
|
- `payment.created` - New payment received
|
||||||
|
- `payment.updated` - Payment status changes
|
||||||
|
- `order.created` - New order created
|
||||||
|
- `order.updated` - Order status changes
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Certificate Upload Fails
|
||||||
|
- Ensure file is in P7B format (not PEM)
|
||||||
|
- File must be valid PKCS#7 structure
|
||||||
|
- Check file isn't corrupted: `openssl pkcs7 -print_certs -in cert.p7b`
|
||||||
|
|
||||||
|
### Webhook Not Receiving Data
|
||||||
|
1. Check server is running and accessible from internet
|
||||||
|
2. Verify URL in Square Dashboard is correct
|
||||||
|
3. Check firewall allows incoming traffic on webhook port
|
||||||
|
4. Review server logs: `./logs/studspider.log`
|
||||||
|
|
||||||
|
### SSL Certificate Errors
|
||||||
|
- Self-signed certificates won't work with Square in production
|
||||||
|
- Use Let's Encrypt or commercial CA for production
|
||||||
|
- Use ngrok for quick testing (provides valid HTTPS)
|
||||||
|
|
||||||
|
### Testing Locally
|
||||||
|
```powershell
|
||||||
|
# Square can't reach localhost - use one of these:
|
||||||
|
# 1. ngrok (easiest)
|
||||||
|
ngrok http 8443
|
||||||
|
|
||||||
|
# 2. Cloudflare Tunnel
|
||||||
|
cloudflared tunnel --url http://localhost:8443
|
||||||
|
|
||||||
|
# 3. Deploy to cloud (AWS, Azure, DigitalOcean, etc.)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verifying Webhook Signatures
|
||||||
|
|
||||||
|
Square signs webhook requests. Your server should verify signatures:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# TODO: Add to hook_handler in test_server.py
|
||||||
|
import hmac
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
def verify_square_signature(payload: bytes, signature: str, webhook_signature_key: str) -> bool:
|
||||||
|
"""Verify Square webhook signature"""
|
||||||
|
computed_signature = hmac.new(
|
||||||
|
webhook_signature_key.encode('utf-8'),
|
||||||
|
payload,
|
||||||
|
hashlib.sha256
|
||||||
|
).digest()
|
||||||
|
|
||||||
|
expected_signature = base64.b64decode(signature)
|
||||||
|
return hmac.compare_digest(computed_signature, expected_signature)
|
||||||
|
```
|
||||||
|
|
||||||
|
Get your webhook signature key from Square Dashboard → Webhooks → Signature Key.
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Add to your `.env` file:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Square Webhook Configuration
|
||||||
|
SQUARE_WEBHOOK_SIGNATURE_KEY=your_signature_key_from_square_dashboard
|
||||||
|
|
||||||
|
# SSL Certificate Paths (production)
|
||||||
|
SSL_CERT_PATH=/path/to/fullchain.pem
|
||||||
|
SSL_KEY_PATH=/path/to/privkey.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands Quick Reference
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Generate certificate
|
||||||
|
python generate_cert.py
|
||||||
|
|
||||||
|
# Convert to P7B
|
||||||
|
python convert_to_p7b.py
|
||||||
|
|
||||||
|
# Start HTTPS server
|
||||||
|
python src/test_server.py --ssl-cert cert.pem --ssl-key key.pem --hook-port 8443 --hook-ssl-only
|
||||||
|
|
||||||
|
# Test webhook
|
||||||
|
python src/test_webhook.py
|
||||||
|
|
||||||
|
# Start with ngrok
|
||||||
|
ngrok http 8443
|
||||||
|
```
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
+5
-1
@@ -1,5 +1,5 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "edderkop"
|
name = "studpider"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = ""
|
description = ""
|
||||||
authors = [{ name = "CF", email = "ph0sgene@pm.me" }]
|
authors = [{ name = "CF", email = "ph0sgene@pm.me" }]
|
||||||
@@ -17,6 +17,10 @@ dependencies = [
|
|||||||
"apscheduler (>=3.11.0,<4.0.0)",
|
"apscheduler (>=3.11.0,<4.0.0)",
|
||||||
"aiosqlite (>=0.21.0,<0.22.0)",
|
"aiosqlite (>=0.21.0,<0.22.0)",
|
||||||
"bs4>=0.0.2",
|
"bs4>=0.0.2",
|
||||||
|
"reflex>=0.8.14.post1",
|
||||||
|
"fastapi>=0.128.0",
|
||||||
|
"uvicorn>=0.40.0",
|
||||||
|
"gunicorn>=23.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='Edderkop',
|
name="studspider",
|
||||||
version='0.1',
|
version="0.1",
|
||||||
description='Bricket + Square integration application',
|
description="Bricket + Square integration application",
|
||||||
author='CF',
|
author="CF",
|
||||||
author_email='chris-forst@pm.me',
|
author_email="chris-forst@pm.me",
|
||||||
packages=['src'],
|
packages=["src"],
|
||||||
package_dir={'': 'src'},
|
package_dir={"": "src"},
|
||||||
install_requires=[
|
install_requires=[
|
||||||
# List your project dependencies here
|
# List your project dependencies here
|
||||||
],
|
],
|
||||||
entry_points={
|
entry_points={
|
||||||
'console_scripts': [
|
"console_scripts": [
|
||||||
'main=main:main', # Adjust this line based on your main function
|
"main=main:main", # Adjust this line based on your main function
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
+4
-2
@@ -9,7 +9,9 @@ import aiohttp
|
|||||||
import json
|
import json
|
||||||
import dotenv
|
import dotenv
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
from asyncio_throttle.throttler import Throttler
|
import throttler
|
||||||
|
|
||||||
|
Throttler = throttler.GlobalThrottler
|
||||||
|
|
||||||
dotenv.load_dotenv()
|
dotenv.load_dotenv()
|
||||||
|
|
||||||
@@ -22,7 +24,7 @@ class AsyncBricksetAPI:
|
|||||||
self.password = os.getenv("BRICKSET_PASS")
|
self.password = os.getenv("BRICKSET_PASS")
|
||||||
self.api_key = os.getenv("BRICKSET_API_KEY")
|
self.api_key = os.getenv("BRICKSET_API_KEY")
|
||||||
self.debug = debug
|
self.debug = debug
|
||||||
self.headers = {"UserAgent": "Edderkop/0.1"}
|
self.headers = {"UserAgent": "studspider/0.1"}
|
||||||
self.throttler = Throttler(rate_limit=500, period=60)
|
self.throttler = Throttler(rate_limit=500, period=60)
|
||||||
self.user_hash = None
|
self.user_hash = None
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
|
|||||||
+11
-12
@@ -5,9 +5,6 @@ import asyncio
|
|||||||
import uuid
|
import uuid
|
||||||
import re
|
import re
|
||||||
from time import time
|
from time import time
|
||||||
import re
|
|
||||||
# we dont need no stinkin floats
|
|
||||||
time = int(time())
|
|
||||||
|
|
||||||
import db
|
import db
|
||||||
|
|
||||||
@@ -109,11 +106,17 @@ async def acat2id(cat_id: str):
|
|||||||
{
|
{
|
||||||
"id": obj["id"],
|
"id": obj["id"],
|
||||||
"name": obj.get("item_data", {}).get("name", "Unknown"),
|
"name": obj.get("item_data", {}).get("name", "Unknown"),
|
||||||
"number": None if not obj.get("item_data", {}).get("name", "") else (
|
"number": (
|
||||||
|
None
|
||||||
|
if not obj.get("item_data", {}).get("name", "")
|
||||||
|
else (
|
||||||
get_set_number(obj.get("item_data", {}).get("name", ""))
|
get_set_number(obj.get("item_data", {}).get("name", ""))
|
||||||
if re.findall(r"\d{5}", obj.get("item_data", {}).get("name", ""))
|
if re.findall(r"\d{5}", obj.get("item_data", {}).get("name", ""))
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
),
|
||||||
|
"updated_at": obj.get("updated_at"),
|
||||||
|
"version": obj.get("version"),
|
||||||
}
|
}
|
||||||
for obj in result_json.get("objects", [])
|
for obj in result_json.get("objects", [])
|
||||||
]
|
]
|
||||||
@@ -122,14 +125,10 @@ async def acat2id(cat_id: str):
|
|||||||
|
|
||||||
|
|
||||||
async def test() -> None:
|
async def test() -> None:
|
||||||
task = asyncio.create_task(acat2id(LEGO_CAT_ID)) # type: ignore
|
result = await acat2id(LEGO_CAT_ID) # type: ignore
|
||||||
await do_review()
|
print("OBJECTS IN LEGO CATEGORY:")
|
||||||
await asyncio.sleep(1) # Give the task some time to complete
|
for item in result:
|
||||||
result = await task
|
print(f"ID: {item['id']}, Name: {item['name']}, Updated: {item['updated_at']}")
|
||||||
#print("OBJECTS IN LEGO CATEGORY:")
|
|
||||||
#print(f"ID: {item['id']}, Name: {item['name']}")
|
|
||||||
return True
|
|
||||||
# pass
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|||||||
+2
-2
@@ -14,9 +14,9 @@ debug = False # Set to True to enable debug output
|
|||||||
|
|
||||||
def login(username, password):
|
def login(username, password):
|
||||||
url = "https://brickset.com/api/v3.asmx/login"
|
url = "https://brickset.com/api/v3.asmx/login"
|
||||||
headers = {"UserAgent": "Edderkop/0.1"}
|
headers = {"UserAgent": "studspider/0.1"}
|
||||||
params = {
|
params = {
|
||||||
"UserAgent": "Edderkop/0.1",
|
"UserAgent": "studspider/0.1",
|
||||||
"username": username,
|
"username": username,
|
||||||
"password": password,
|
"password": password,
|
||||||
"apiKey": BRICKSET_API_KEY,
|
"apiKey": BRICKSET_API_KEY,
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import aiosqlite
|
import aiosqlite
|
||||||
from time import time
|
from time import time
|
||||||
|
|
||||||
time = int(time()) # we dont need no stinkin floats
|
time = int(time()) # we dont need no stinkin floats
|
||||||
|
|
||||||
# db = await aiosqlite.connect("edderkop.db") # type: ignore
|
# db = await aiosqlite.connect("studspider.db") # type: ignore
|
||||||
|
|
||||||
|
|
||||||
async def close(db):
|
async def close(db):
|
||||||
await db.close()
|
await db.close()
|
||||||
|
|
||||||
|
|
||||||
async def fetch(table, sqobj):
|
async def fetch(table, sqobj):
|
||||||
"""
|
"""
|
||||||
Asynchronously fetch a record from the REVIEW table by sq_object.
|
Asynchronously fetch a record from the REVIEW table by sq_object.
|
||||||
@@ -16,13 +19,16 @@ async def fetch(table, sqobj):
|
|||||||
Returns:
|
Returns:
|
||||||
dict or None: The fetched record as a dictionary, or None if not found.
|
dict or None: The fetched record as a dictionary, or None if not found.
|
||||||
"""
|
"""
|
||||||
async with aiosqlite.connect("edderkop.db") as db: # type: ignore
|
async with aiosqlite.connect("studspider.db") as db: # type: ignore
|
||||||
async with db.execute(f"SELECT * FROM {table} WHERE sq_object = {sqobj}") as cursor:
|
async with db.execute(
|
||||||
|
f"SELECT * FROM {table} WHERE sq_object = {sqobj}"
|
||||||
|
) as cursor:
|
||||||
async for row in cursor:
|
async for row in cursor:
|
||||||
return row
|
return row
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def upsert(table: str, data: dict, unique_key: str) -> bool:
|
async def upsert(table: str, data: dict, unique_key: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Asynchronously insert or update a record in the specified table.
|
Asynchronously insert or update a record in the specified table.
|
||||||
@@ -37,9 +43,11 @@ async def upsert(table: str, data: dict, unique_key: str) -> bool:
|
|||||||
"""
|
"""
|
||||||
print(f"Upserting into {table}: {data}")
|
print(f"Upserting into {table}: {data}")
|
||||||
|
|
||||||
columns = ', '.join(data.keys())
|
columns = ", ".join(data.keys())
|
||||||
placeholders = ', '.join('?' * len(data))
|
placeholders = ", ".join("?" * len(data))
|
||||||
updates = ', '.join(f"{col}=excluded.{col}" for col in data.keys() if col != unique_key)
|
updates = ", ".join(
|
||||||
|
f"{col}=excluded.{col}" for col in data.keys() if col != unique_key
|
||||||
|
)
|
||||||
|
|
||||||
sql = f"""
|
sql = f"""
|
||||||
INSERT INTO {table} ({columns})
|
INSERT INTO {table} ({columns})
|
||||||
@@ -47,7 +55,7 @@ async def upsert(table: str, data: dict, unique_key: str) -> bool:
|
|||||||
ON CONFLICT({unique_key}) DO UPDATE SET
|
ON CONFLICT({unique_key}) DO UPDATE SET
|
||||||
{updates};
|
{updates};
|
||||||
"""
|
"""
|
||||||
async with aiosqlite.connect("edderkop.db") as db: # type: ignore
|
async with aiosqlite.connect("studspider.db") as db: # type: ignore
|
||||||
async with db.execute(sql, tuple(data.values())) as cursor:
|
async with db.execute(sql, tuple(data.values())) as cursor:
|
||||||
await db.commit()
|
await db.commit()
|
||||||
return True
|
return True
|
||||||
+1
-1
@@ -15,7 +15,7 @@ class BricksetAPI:
|
|||||||
self.password = os.getenv("BRICKSET_PASS")
|
self.password = os.getenv("BRICKSET_PASS")
|
||||||
self.api_key = os.getenv("BRICKSET_API_KEY")
|
self.api_key = os.getenv("BRICKSET_API_KEY")
|
||||||
self.debug = debug
|
self.debug = debug
|
||||||
self.headers = {"UserAgent": "Edderkop/0.1"}
|
self.headers = {"UserAgent": "studspider/0.1"}
|
||||||
self.user_hash = None
|
self.user_hash = None
|
||||||
|
|
||||||
def login(self) -> Dict[str, Any]:
|
def login(self) -> Dict[str, Any]:
|
||||||
|
|||||||
@@ -6,6 +6,15 @@ dotenv.load_dotenv()
|
|||||||
|
|
||||||
LEGO_CAT_ID = os.getenv("SQUARE_LEGO_CAT_ID")
|
LEGO_CAT_ID = os.getenv("SQUARE_LEGO_CAT_ID")
|
||||||
|
|
||||||
|
# SSL Certificate paths (optional - for direct HTTPS, not needed with nginx)
|
||||||
|
SSL_CERT_PATH = os.getenv("SSL_CERT_PATH")
|
||||||
|
SSL_KEY_PATH = os.getenv("SSL_KEY_PATH")
|
||||||
|
|
||||||
|
# Square Webhook Configuration
|
||||||
|
# Get signature key from Square Developer Dashboard → Webhooks
|
||||||
|
SQUARE_WEBHOOK_SIGNATURE_KEY = os.getenv("SQUARE_WEBHOOK_SIGNATURE_KEY")
|
||||||
|
SQUARE_WEBHOOK_URL = os.getenv("SQUARE_WEBHOOK_URL")
|
||||||
|
|
||||||
if os.getenv("SQUARE_ENV") == "production":
|
if os.getenv("SQUARE_ENV") == "production":
|
||||||
SQUARE_APP_ID = os.getenv("PROD_SQUARE_APP_ID")
|
SQUARE_APP_ID = os.getenv("PROD_SQUARE_APP_ID")
|
||||||
SQUARE_TOKEN = os.getenv("PROD_SQUARE_TOKEN")
|
SQUARE_TOKEN = os.getenv("PROD_SQUARE_TOKEN")
|
||||||
|
|||||||
+23
-14
@@ -1,20 +1,27 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import get_sq_env
|
import re
|
||||||
|
from . import get_sq_env
|
||||||
import aiosqlite
|
import aiosqlite
|
||||||
|
from time import time
|
||||||
|
|
||||||
import asquare
|
from . import asquare
|
||||||
import db
|
from . import db
|
||||||
import abricksetapi
|
from . import abricksetapi
|
||||||
from . throttler import GlobalThrottler
|
from . import throttler
|
||||||
|
Throttler = throttler.GlobalThrottler
|
||||||
|
|
||||||
|
|
||||||
|
LEGO_CAT_ID = get_sq_env.LEGO_CAT_ID
|
||||||
|
|
||||||
|
# big glue boy
|
||||||
async def do_review() -> bool:
|
async def do_review() -> bool:
|
||||||
result = await asquare.acat2id(LEGO_CAT_ID)
|
result = await asquare.acat2id(LEGO_CAT_ID) # type: ignore
|
||||||
for item in result:
|
for item in result:
|
||||||
if not re.match(r".*\d{5}.*", item["name"]):
|
if not re.match(r".*\d{5}.*", item["name"]):
|
||||||
missing = []
|
missing = []
|
||||||
print(f"{item} is missing its set number!")
|
print(f"{item} is missing its set number!")
|
||||||
print(f"[@DEBUG] Upserting {item['id']} into REVIEW table")
|
#print(f"[@DEBUG] Upserting {item['id']} into REVIEW table")
|
||||||
item_sanitized = {
|
item_sanitized = {
|
||||||
"sq_object": item["id"],
|
"sq_object": item["id"],
|
||||||
"set_name": item["name"],
|
"set_name": item["name"],
|
||||||
@@ -22,11 +29,13 @@ async def do_review() -> bool:
|
|||||||
"time_added": time
|
"time_added": time
|
||||||
}
|
}
|
||||||
missing.append(item_sanitized)
|
missing.append(item_sanitized)
|
||||||
for item in missing:
|
|
||||||
sq_item = await db.fetch(item["id"]) #
|
|
||||||
if item["id"] != sq_item["sq_object"]:
|
|
||||||
await db.upsert("REVIEW", item_sanitized, "sq_object")
|
|
||||||
print(f"[@DEBUG] Upserted {item['id']} into REVIEW table")
|
|
||||||
print(f"[@DEBUG] {item['id']} already in REVIEW table")
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
for item in missing:
|
||||||
|
print(item)
|
||||||
|
# sq_item = await db.fetch(item["id"])
|
||||||
|
if item["id"] != sq_item["sq_object"]:
|
||||||
|
print(f"[@DEBUG] {item['id']} is missing)")
|
||||||
|
#await db.upsert("MISSING", item_sanitized, "sq_object")
|
||||||
|
# #print(f"[@DEBUG] Upserted {item['id']} into REVIEW table")
|
||||||
|
#print(f"[@DEBUG] {item['id']} already in REVIEW table")
|
||||||
|
return True
|
||||||
+5
-5
@@ -6,21 +6,21 @@ import os
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Get the absolute path to edderkop.db in the project root
|
# Get the absolute path to studspider.db in the project root
|
||||||
DEFAULT_DB_PATH = os.path.join(
|
DEFAULT_DB_PATH = os.path.join(
|
||||||
os.path.dirname(os.path.dirname(__file__)), "edderkop.db"
|
os.path.dirname(os.path.dirname(__file__)), "studspider.db"
|
||||||
)
|
)
|
||||||
|
|
||||||
if not os.path.exists(DEFAULT_DB_PATH):
|
if not os.path.exists(DEFAULT_DB_PATH):
|
||||||
raise FileNotFoundError(f"Database file not found at {DEFAULT_DB_PATH}")
|
raise FileNotFoundError(f"Database file not found at {DEFAULT_DB_PATH}")
|
||||||
print('Please run "src/scripts/EDDERKOP.SQL" to setup the database.')
|
print('Please run "src/scripts/studspider.SQL" to setup the database.')
|
||||||
os.exit(0)
|
os.exit(0)
|
||||||
|
|
||||||
|
|
||||||
class AsyncSchedulerDB:
|
class AsyncSchedulerDB:
|
||||||
"""
|
"""
|
||||||
Asynchronous SQLite database manager for APScheduler.
|
Asynchronous SQLite database manager for APScheduler.
|
||||||
Handles job storage, retrieval, and database operations using the main edderkop.db database.
|
Handles job storage, retrieval, and database operations using the main studspider.db database.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, db_path: str = DEFAULT_DB_PATH):
|
def __init__(self, db_path: str = DEFAULT_DB_PATH):
|
||||||
@@ -29,7 +29,7 @@ class AsyncSchedulerDB:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
db_path (str, optional): Path to the SQLite database file.
|
db_path (str, optional): Path to the SQLite database file.
|
||||||
Defaults to edderkop.db in project root.
|
Defaults to studspider.db in project root.
|
||||||
"""
|
"""
|
||||||
self.db_path = db_path
|
self.db_path = db_path
|
||||||
self._initialized = False
|
self._initialized = False
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
"""
|
||||||
|
Convert P7B (PKCS#7) certificate to PEM format for use with the server
|
||||||
|
Use this if you have a P7B file and need to extract PEM certificates
|
||||||
|
"""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def convert_p7b_to_pem(p7b_file, cert_output="cert.pem", extract_key=False):
|
||||||
|
"""
|
||||||
|
Convert P7B certificate to PEM format
|
||||||
|
|
||||||
|
Args:
|
||||||
|
p7b_file: Input P7B (PKCS#7) file
|
||||||
|
cert_output: Output PEM certificate file
|
||||||
|
extract_key: If True, try to extract private key (rarely works with P7B)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Check if OpenSSL is available
|
||||||
|
try:
|
||||||
|
subprocess.run(["openssl", "version"], capture_output=True, check=True)
|
||||||
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||||
|
print("ERROR: OpenSSL not found!")
|
||||||
|
print("\nFor Windows, you can:")
|
||||||
|
print(
|
||||||
|
"1. Install OpenSSL from: https://slproweb.com/products/Win32OpenSSL.html"
|
||||||
|
)
|
||||||
|
print("2. Or use Git Bash which includes OpenSSL")
|
||||||
|
print("3. Or use WSL (Windows Subsystem for Linux)")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check if input file exists
|
||||||
|
if not os.path.exists(p7b_file):
|
||||||
|
print(f"ERROR: P7B file not found: {p7b_file}")
|
||||||
|
print(f"Current directory: {os.getcwd()}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f"Converting {p7b_file} to PEM format...")
|
||||||
|
|
||||||
|
# Convert P7B to PEM
|
||||||
|
cmd = ["openssl", "pkcs7", "-print_certs", "-in", p7b_file, "-out", cert_output]
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(cmd, check=True, capture_output=True, text=True)
|
||||||
|
|
||||||
|
if os.path.exists(cert_output):
|
||||||
|
print(f"\n✓ PEM certificate created: {os.path.abspath(cert_output)}")
|
||||||
|
print(f" File size: {os.path.getsize(cert_output)} bytes")
|
||||||
|
|
||||||
|
# Check what was extracted
|
||||||
|
with open(cert_output, "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
cert_count = content.count("BEGIN CERTIFICATE")
|
||||||
|
print(f" Certificates found: {cert_count}")
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("IMPORTANT:")
|
||||||
|
print("=" * 60)
|
||||||
|
print("⚠️ P7B files usually contain ONLY certificates, not private keys!")
|
||||||
|
print("\nTo run an HTTPS server, you need:")
|
||||||
|
print("1. Certificate (cert.pem) - ✓ Extracted")
|
||||||
|
print("2. Private Key (key.pem) - ✗ NOT in P7B file")
|
||||||
|
print("\nIf you don't have the private key:")
|
||||||
|
print("- Generate a new certificate pair: python generate_cert.py")
|
||||||
|
print("- Or obtain the private key from your certificate provider")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print("ERROR: Output file was not created")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"ERROR: Conversion failed: {e}")
|
||||||
|
if e.stderr:
|
||||||
|
print(f"OpenSSL error: {e.stderr}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Convert P7B (PKCS#7) certificate to PEM format"
|
||||||
|
)
|
||||||
|
parser.add_argument("p7b_file", help="Input P7B certificate file")
|
||||||
|
parser.add_argument(
|
||||||
|
"--output",
|
||||||
|
default="cert.pem",
|
||||||
|
help="Output PEM certificate file (default: cert.pem)",
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print("P7B to PEM Certificate Converter")
|
||||||
|
print("=" * 60)
|
||||||
|
print()
|
||||||
|
|
||||||
|
success = convert_p7b_to_pem(args.p7b_file, args.output)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print("\n✓ Conversion complete!")
|
||||||
|
print("\nNext steps:")
|
||||||
|
print("1. If you have the private key separately, use it with --ssl-key")
|
||||||
|
print("2. If not, generate a new certificate: python generate_cert.py")
|
||||||
|
else:
|
||||||
|
sys.exit(1)
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
"""
|
||||||
|
Convert PEM certificate to P7B (PKCS#7) format for Square webhooks
|
||||||
|
Square requires P7B format for webhook signature verification
|
||||||
|
"""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def convert_pem_to_p7b(cert_file="cert.pem", output_file="cert.p7b"):
|
||||||
|
"""
|
||||||
|
Convert PEM certificate to P7B (PKCS#7) format
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cert_file: Input PEM certificate file
|
||||||
|
output_file: Output P7B file
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Check if OpenSSL is available
|
||||||
|
try:
|
||||||
|
subprocess.run(["openssl", "version"], capture_output=True, check=True)
|
||||||
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||||
|
print("ERROR: OpenSSL not found!")
|
||||||
|
print("\nFor Windows, you can:")
|
||||||
|
print(
|
||||||
|
"1. Install OpenSSL from: https://slproweb.com/products/Win32OpenSSL.html"
|
||||||
|
)
|
||||||
|
print("2. Or use Git Bash which includes OpenSSL")
|
||||||
|
print("3. Or use WSL (Windows Subsystem for Linux)")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check if input file exists
|
||||||
|
if not os.path.exists(cert_file):
|
||||||
|
print(f"ERROR: Certificate file not found: {cert_file}")
|
||||||
|
print(f"Current directory: {os.getcwd()}")
|
||||||
|
print("\nMake sure you've generated the certificate first:")
|
||||||
|
print(" python generate_cert.py")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f"Converting {cert_file} to P7B format...")
|
||||||
|
|
||||||
|
# Convert PEM to P7B (PKCS#7)
|
||||||
|
# -certfile can include intermediate certificates if needed
|
||||||
|
cmd = [
|
||||||
|
"openssl",
|
||||||
|
"crl2pkcs7",
|
||||||
|
"-nocrl",
|
||||||
|
"-certfile",
|
||||||
|
cert_file,
|
||||||
|
"-out",
|
||||||
|
output_file,
|
||||||
|
]
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(cmd, check=True, capture_output=True, text=True)
|
||||||
|
|
||||||
|
if os.path.exists(output_file):
|
||||||
|
print(f"\n✓ P7B certificate created: {os.path.abspath(output_file)}")
|
||||||
|
print(f" File size: {os.path.getsize(output_file)} bytes")
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("Square Webhook Configuration:")
|
||||||
|
print("=" * 60)
|
||||||
|
print(f"1. Upload this file to Square Developer Dashboard:")
|
||||||
|
print(f" {os.path.abspath(output_file)}")
|
||||||
|
print("\n2. In Square Dashboard:")
|
||||||
|
print(" - Go to Webhooks settings")
|
||||||
|
print(" - Click 'Manage Notification Settings'")
|
||||||
|
print(" - Upload the P7B file for signature verification")
|
||||||
|
print("\n3. Square will use this to verify webhook signatures")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Show file contents preview
|
||||||
|
print("\nP7B file preview (first 500 chars):")
|
||||||
|
with open(output_file, "r") as f:
|
||||||
|
content = f.read(500)
|
||||||
|
print(content)
|
||||||
|
if len(content) == 500:
|
||||||
|
print("...")
|
||||||
|
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print("ERROR: Output file was not created")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"ERROR: Conversion failed: {e}")
|
||||||
|
if e.stderr:
|
||||||
|
print(f"OpenSSL error: {e.stderr}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def convert_with_chain(cert_file="cert.pem", chain_file=None, output_file="cert.p7b"):
|
||||||
|
"""
|
||||||
|
Convert PEM certificate with certificate chain to P7B format
|
||||||
|
Use this if you have intermediate certificates
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cert_file: Your certificate file
|
||||||
|
chain_file: Intermediate/chain certificate file (optional)
|
||||||
|
output_file: Output P7B file
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not chain_file:
|
||||||
|
return convert_pem_to_p7b(cert_file, output_file)
|
||||||
|
|
||||||
|
# Check files exist
|
||||||
|
for f in [cert_file, chain_file]:
|
||||||
|
if not os.path.exists(f):
|
||||||
|
print(f"ERROR: File not found: {f}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f"Converting {cert_file} with chain {chain_file} to P7B format...")
|
||||||
|
|
||||||
|
# Create combined cert file
|
||||||
|
combined = "combined_cert.pem"
|
||||||
|
with open(combined, "w") as out:
|
||||||
|
with open(cert_file, "r") as f:
|
||||||
|
out.write(f.read())
|
||||||
|
with open(chain_file, "r") as f:
|
||||||
|
out.write(f.read())
|
||||||
|
|
||||||
|
result = convert_pem_to_p7b(combined, output_file)
|
||||||
|
|
||||||
|
# Clean up combined file
|
||||||
|
if os.path.exists(combined):
|
||||||
|
os.remove(combined)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Convert PEM certificate to P7B (PKCS#7) for Square webhooks"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--cert",
|
||||||
|
default="cert.pem",
|
||||||
|
help="Input PEM certificate file (default: cert.pem)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--chain", default=None, help="Optional: Intermediate certificate chain file"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--output", default="cert.p7b", help="Output P7B file (default: cert.p7b)"
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print("PEM to P7B Certificate Converter for Square Webhooks")
|
||||||
|
print("=" * 60)
|
||||||
|
print()
|
||||||
|
|
||||||
|
if args.chain:
|
||||||
|
success = convert_with_chain(args.cert, args.chain, args.output)
|
||||||
|
else:
|
||||||
|
success = convert_pem_to_p7b(args.cert, args.output)
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("\n✓ Conversion complete!")
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
CREATE TABLE "REVIEW" (
|
|
||||||
"sq_object" TEXT NOT NULL UNIQUE,
|
|
||||||
"time_added" INTEGER,
|
|
||||||
"set_number" INTEGER,
|
|
||||||
"time_modified" INTEGER,
|
|
||||||
PRIMARY KEY("sq_object")
|
|
||||||
)
|
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
"""
|
||||||
|
Generate self-signed SSL certificate for testing HTTPS webhooks
|
||||||
|
Run this script to create cert.pem and key.pem files
|
||||||
|
"""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def generate_certificate():
|
||||||
|
"""Generate self-signed certificate using OpenSSL"""
|
||||||
|
|
||||||
|
cert_file = "cert.pem"
|
||||||
|
key_file = "key.pem"
|
||||||
|
|
||||||
|
# Check if OpenSSL is available
|
||||||
|
try:
|
||||||
|
subprocess.run(["openssl", "version"], capture_output=True, check=True)
|
||||||
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||||
|
print("ERROR: OpenSSL not found!")
|
||||||
|
print("\nFor Windows, you can:")
|
||||||
|
print(
|
||||||
|
"1. Install OpenSSL from: https://slproweb.com/products/Win32OpenSSL.html"
|
||||||
|
)
|
||||||
|
print("2. Or use Git Bash which includes OpenSSL")
|
||||||
|
print("3. Or use WSL (Windows Subsystem for Linux)")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print("Generating self-signed certificate...")
|
||||||
|
print("This will create cert.pem and key.pem files")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Generate certificate valid for 365 days
|
||||||
|
cmd = [
|
||||||
|
"openssl",
|
||||||
|
"req",
|
||||||
|
"-x509",
|
||||||
|
"-newkey",
|
||||||
|
"rsa:4096",
|
||||||
|
"-keyout",
|
||||||
|
key_file,
|
||||||
|
"-out",
|
||||||
|
cert_file,
|
||||||
|
"-days",
|
||||||
|
"365",
|
||||||
|
"-nodes", # No passphrase
|
||||||
|
"-subj",
|
||||||
|
"/C=US/ST=State/L=City/O=Organization/CN=localhost",
|
||||||
|
]
|
||||||
|
|
||||||
|
try:
|
||||||
|
subprocess.run(cmd, check=True)
|
||||||
|
print(f"\n✓ Certificate created: {os.path.abspath(cert_file)}")
|
||||||
|
print(f"✓ Private key created: {os.path.abspath(key_file)}")
|
||||||
|
print("\nYou can now run the server with HTTPS:")
|
||||||
|
print(
|
||||||
|
f" python src/test_server.py --ssl-cert {cert_file} --ssl-key {key_file} --hook-port 8443"
|
||||||
|
)
|
||||||
|
print("\nOr webhook-only HTTPS:")
|
||||||
|
print(
|
||||||
|
f" python src/test_server.py --ssl-cert {cert_file} --ssl-key {key_file} --hook-port 8443 --hook-ssl-only"
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
"\nNote: Self-signed certificates will show security warnings in browsers."
|
||||||
|
)
|
||||||
|
print("For production, use certificates from a trusted CA like Let's Encrypt.")
|
||||||
|
return True
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"ERROR: Failed to generate certificate: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if not generate_certificate():
|
||||||
|
sys.exit(1)
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
CREATE TABLE "REVIEW" (
|
||||||
|
"sq_object" TEXT NOT NULL UNIQUE,
|
||||||
|
"price" INTEGER NOT NULL,
|
||||||
|
"date_modified" INTEGER,
|
||||||
|
PRIMARY KEY("sq_object")
|
||||||
|
)
|
||||||
|
|
||||||
|
CREATE TABLE "MISSING" (
|
||||||
|
"sq_object" TEXT NOT NULL UNIQUE,
|
||||||
|
"time_added" INTEGER,
|
||||||
|
"set_number" INTEGER,
|
||||||
|
"time_modified" INTEGER,
|
||||||
|
"set_name" TEXT,
|
||||||
|
PRIMARY KEY("sq_object")
|
||||||
|
)
|
||||||
+2
-2
@@ -74,7 +74,7 @@ async def handle_post(request):
|
|||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return aiohttp.web.Response(status=400, text="400 Bad Request.")
|
return aiohttp.web.Response(status=400, text="400 Bad Request.")
|
||||||
result = {"message": f"Edderkop/0.1 received command: {reqdata}"}
|
result = {"message": f"studspider/0.1 received command: {reqdata}"}
|
||||||
return aiohttp.web.json_response(status=200, data=result)
|
return aiohttp.web.json_response(status=200, data=result)
|
||||||
|
|
||||||
|
|
||||||
@@ -108,6 +108,6 @@ async def main():
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
print(generate_toilet_banner("Edderkop v0.1"))
|
print(generate_toilet_banner("studspider v0.1"))
|
||||||
print("Built for PLAY-WELL TEKNOLOGIES by contact@traps.moe\n\n")
|
print("Built for PLAY-WELL TEKNOLOGIES by contact@traps.moe\n\n")
|
||||||
aiohttp.web.run_app(app, port=7432)
|
aiohttp.web.run_app(app, port=7432)
|
||||||
+322
-18
@@ -1,44 +1,52 @@
|
|||||||
|
import argparse
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
import ssl
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from base64 import b64decode
|
from base64 import b64decode
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
import uuid
|
import uuid
|
||||||
from time import time
|
|
||||||
import dotenv
|
import dotenv
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import logging
|
import logging
|
||||||
from logging.handlers import RotatingFileHandler
|
from logging.handlers import RotatingFileHandler
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
#print("Creating log directory if it doesn't exist...")
|
|
||||||
#Path("./logs/").mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
import banner
|
import banner
|
||||||
import get_sq_env
|
import get_sq_env
|
||||||
|
from time import time
|
||||||
|
|
||||||
|
time = int(time())
|
||||||
import asquare as sq
|
import asquare as sq
|
||||||
|
|
||||||
|
# Square SDK webhook validation
|
||||||
|
from square.utils.webhooks_helper import verify_signature
|
||||||
|
|
||||||
# from asquareclass import AsyncSquareClient # type: ignore
|
# from asquareclass import AsyncSquareClient # type: ignore
|
||||||
from abricksetapi import AsyncBricksetAPI
|
from abricksetapi import AsyncBricksetAPI
|
||||||
|
|
||||||
print("Loading environment variables...")
|
print("Loading environment variables...")
|
||||||
dotenv.load_dotenv()
|
dotenv.load_dotenv()
|
||||||
SQUARE_LEGO_CAT_ID = os.environ.get("SQUARE_LEGO_CAT_ID")
|
SQUARE_LEGO_CAT_ID = os.environ.get("SQUARE_LEGO_CAT_ID")
|
||||||
|
SQUARE_WEBHOOK_SIGNATURE_KEY = os.environ.get("SQUARE_WEBHOOK_SIGNATURE_KEY")
|
||||||
|
SQUARE_WEBHOOK_URL = os.environ.get("SQUARE_WEBHOOK_URL")
|
||||||
|
|
||||||
from asyncio_throttle.throttler import Throttler
|
from asyncio_throttle.throttler import Throttler
|
||||||
|
|
||||||
# set up logger
|
# set up logger
|
||||||
print("Setting up logger...")
|
print("Setting up logger...")
|
||||||
logger = logging.getLogger("edderkop")
|
logger = logging.getLogger("studspider")
|
||||||
logger.setLevel(logging.DEBUG)
|
logger.setLevel(logging.DEBUG)
|
||||||
handler = RotatingFileHandler("./logs/edderkop.log", maxBytes=125000, backupCount=5)
|
handler = RotatingFileHandler("./logs/studspider.log", maxBytes=125000, backupCount=5)
|
||||||
logger.addHandler(handler)
|
logger.addHandler(handler)
|
||||||
|
|
||||||
lego = AsyncBricksetAPI(debug=True, logger=logger)
|
lego = AsyncBricksetAPI(debug=True, logger=logger)
|
||||||
# sq = AsyncSquareClient(logger=logger)
|
# sq = AsyncSquareClient(logger=logger)
|
||||||
# logging.basicConfig(
|
# logging.basicConfig(
|
||||||
# filename="./logs/edderkop.log", encoding="utf-8", level=logging.DEBUG
|
# filename="./logs/studspider.log", encoding="utf-8", level=logging.DEBUG
|
||||||
# )
|
# )
|
||||||
print("Setting up rate limiter...")
|
print("Setting up rate limiter...")
|
||||||
print("Doing RE things...")
|
print("Doing RE things...")
|
||||||
@@ -46,7 +54,7 @@ UA = re.compile(r"^kopling\/?", re.IGNORECASE)
|
|||||||
|
|
||||||
|
|
||||||
async def run_update():
|
async def run_update():
|
||||||
set_list = await sq.acat2id(SQUARE_LEGO_CAT_ID)
|
set_list = await sq.acat2id(SQUARE_LEGO_CAT_ID) # type: ignore
|
||||||
# print(f"[@DEBUG]\t {set_list}") # type: ignore
|
# print(f"[@DEBUG]\t {set_list}") # type: ignore
|
||||||
for l_set in set_list:
|
for l_set in set_list:
|
||||||
# print(f"[@DEBUG]{l_set}")
|
# print(f"[@DEBUG]{l_set}")
|
||||||
@@ -79,14 +87,162 @@ def valid_uuid(val):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# In-memory store for processed event IDs (idempotency)
|
||||||
|
processed_event_ids = set()
|
||||||
|
MAX_STORED_EVENT_IDS = 10000 # Prevent memory growth
|
||||||
|
|
||||||
|
|
||||||
|
async def hook_handler(request: web.Request) -> web.Response:
|
||||||
|
"""Handle incoming JSON webhook POSTs on /hook endpoint"""
|
||||||
|
try:
|
||||||
|
current_time = int(asyncio.get_event_loop().time())
|
||||||
|
|
||||||
|
# Verify request method
|
||||||
|
if request.method != "POST":
|
||||||
|
logger.error(
|
||||||
|
f"{current_time}\tInvalid request method to /hook from %s",
|
||||||
|
request.remote,
|
||||||
|
)
|
||||||
|
return web.Response(status=405, text="Method not allowed. Use POST.")
|
||||||
|
|
||||||
|
# Read raw body for signature verification
|
||||||
|
body = await request.read()
|
||||||
|
body_str = body.decode("utf-8")
|
||||||
|
|
||||||
|
# Verify Square webhook signature using Square SDK
|
||||||
|
signature_header = request.headers.get("x-square-hmacsha256-signature", "")
|
||||||
|
|
||||||
|
if SQUARE_WEBHOOK_SIGNATURE_KEY and SQUARE_WEBHOOK_URL:
|
||||||
|
if not signature_header:
|
||||||
|
logger.error(
|
||||||
|
f"{current_time}\tMissing signature header from {request.remote}"
|
||||||
|
)
|
||||||
|
print(f"[@DEBUG] Missing x-square-hmacsha256-signature header")
|
||||||
|
return web.Response(status=401, text="Unauthorized: Missing signature")
|
||||||
|
|
||||||
|
# Use Square SDK's official validation function
|
||||||
|
is_valid = verify_signature(
|
||||||
|
request_body=body_str,
|
||||||
|
signature_header=signature_header,
|
||||||
|
signature_key=SQUARE_WEBHOOK_SIGNATURE_KEY,
|
||||||
|
notification_url=SQUARE_WEBHOOK_URL,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not is_valid:
|
||||||
|
logger.error(f"{current_time}\tInvalid signature from {request.remote}")
|
||||||
|
print(f"[@DEBUG] Invalid webhook signature!")
|
||||||
|
return web.Response(status=401, text="Unauthorized: Invalid signature")
|
||||||
|
|
||||||
|
print(f"[@DEBUG] ✓ Webhook signature verified")
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
f"[@DEBUG] ⚠️ Webhook signature verification DISABLED (set SQUARE_WEBHOOK_SIGNATURE_KEY and SQUARE_WEBHOOK_URL in .env)"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Parse JSON data
|
||||||
|
try:
|
||||||
|
data = json.loads(body_str)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
print(f"[@DEBUG] Invalid JSON to /hook from {request.remote}: {str(e)}")
|
||||||
|
logger.error(f"Invalid JSON to /hook from {request.remote}: {str(e)}")
|
||||||
|
return web.Response(status=400, text="Invalid JSON in request body")
|
||||||
|
|
||||||
|
# Check for idempotency (event_id)
|
||||||
|
event_id = data.get("event_id")
|
||||||
|
if event_id:
|
||||||
|
if event_id in processed_event_ids:
|
||||||
|
print(
|
||||||
|
f"[@DEBUG] Duplicate event {event_id} - already processed (idempotency)"
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
f"{current_time}\tDuplicate event {event_id} from {request.remote}"
|
||||||
|
)
|
||||||
|
return web.json_response(
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"message": "Event already processed (idempotency)",
|
||||||
|
"event_id": event_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add to processed set
|
||||||
|
processed_event_ids.add(event_id)
|
||||||
|
|
||||||
|
# Prevent memory growth - remove oldest if too many
|
||||||
|
if len(processed_event_ids) > MAX_STORED_EVENT_IDS:
|
||||||
|
processed_event_ids.pop()
|
||||||
|
|
||||||
|
# Get event info for quick response
|
||||||
|
webhook_type = data.get("type", "unknown")
|
||||||
|
event_id_val = data.get("event_id", "none")
|
||||||
|
|
||||||
|
# RESPOND IMMEDIATELY - Square requires fast response (< 2 seconds)
|
||||||
|
# Don't do any processing before responding!
|
||||||
|
response_data = {
|
||||||
|
"status": "success",
|
||||||
|
"message": "Webhook received",
|
||||||
|
"event_id": event_id_val,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Log and process AFTER sending response (async background tasks)
|
||||||
|
async def log_and_process():
|
||||||
|
# Log the received webhook with detailed output
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print(f"[@DEBUG] WEBHOOK RECEIVED at {current_time}")
|
||||||
|
print(f"[@DEBUG] From: {request.remote}")
|
||||||
|
print(f"[@DEBUG] Headers: {dict(request.headers)}")
|
||||||
|
print(f"[@DEBUG] Data type: {webhook_type}")
|
||||||
|
print(f"[@DEBUG] Event ID: {event_id_val}")
|
||||||
|
print(f"[@DEBUG] Full payload:")
|
||||||
|
print(json.dumps(data, indent=2))
|
||||||
|
print(f"{'='*60}\n")
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"{current_time}\tWebhook received from {request.remote}: {json.dumps(data)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Process the webhook data
|
||||||
|
if webhook_type == "inventory.count.updated":
|
||||||
|
print(f"[@DEBUG] Processing inventory count update...")
|
||||||
|
# Extract inventory data
|
||||||
|
inventory_counts = (
|
||||||
|
data.get("data", {}).get("object", {}).get("inventory_counts", [])
|
||||||
|
)
|
||||||
|
for count in inventory_counts:
|
||||||
|
print(
|
||||||
|
f"[@DEBUG] Catalog Object: {count.get('catalog_object_id')}"
|
||||||
|
)
|
||||||
|
print(f"[@DEBUG] Quantity: {count.get('quantity')}")
|
||||||
|
print(f"[@DEBUG] State: {count.get('state')}")
|
||||||
|
|
||||||
|
# Start background processing (don't wait for it)
|
||||||
|
asyncio.create_task(log_and_process())
|
||||||
|
|
||||||
|
# Return response immediately
|
||||||
|
return web.json_response(response_data)
|
||||||
|
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
error_msg = f"JSON decode error in webhook: {str(e)}"
|
||||||
|
print(f"[@DEBUG] {error_msg}")
|
||||||
|
logger.error(error_msg)
|
||||||
|
return web.Response(status=400, text=error_msg)
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"Webhook handler error: {str(e)}"
|
||||||
|
print(f"[@DEBUG] {error_msg}")
|
||||||
|
print(f"[@DEBUG] Exception type: {type(e).__name__}")
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
print(f"[@DEBUG] Traceback:\n{traceback.format_exc()}")
|
||||||
|
logger.error(f"{error_msg}\n{traceback.format_exc()}")
|
||||||
|
return web.Response(status=500, text="Internal server error")
|
||||||
|
|
||||||
|
|
||||||
async def request_handler(request: web.Request) -> web.Response:
|
async def request_handler(request: web.Request) -> web.Response:
|
||||||
"""Handle incoming POST requests with command processing"""
|
"""Handle incoming POST requests with command processing"""
|
||||||
try:
|
try:
|
||||||
# Verify request method
|
# Verify request method
|
||||||
if request.method != "POST":
|
if request.method != "POST":
|
||||||
logger.error(
|
logger.error(f"{time}\tInvalid request method from %s at ", request.remote)
|
||||||
f"{time()}\tInvalid request method from %s at ", request.remote
|
|
||||||
)
|
|
||||||
return web.Response(status=405, text="Method not allowed. Use POST.")
|
return web.Response(status=405, text="Method not allowed. Use POST.")
|
||||||
|
|
||||||
# Get JSON data from request
|
# Get JSON data from request
|
||||||
@@ -114,17 +270,17 @@ async def request_handler(request: web.Request) -> web.Response:
|
|||||||
try:
|
try:
|
||||||
decoded_cmd = b64decode(data["CMD"]).decode("utf-8")
|
decoded_cmd = b64decode(data["CMD"]).decode("utf-8")
|
||||||
print(
|
print(
|
||||||
f"""At: {time()}\nReceived command: {decoded_cmd}\nUser-Agent: {request.headers.get('User-Agent')}\nIdempotency-Key: {request.headers.get('Idempotency-Key')}"""
|
f"""At: {time}\nReceived command: {decoded_cmd}\nUser-Agent: {request.headers.get('User-Agent')}\nIdempotency-Key: {request.headers.get('Idempotency-Key')}"""
|
||||||
)
|
)
|
||||||
logging.info(
|
logging.info(
|
||||||
f"{time()}\tCommand: {decoded_cmd}\tFrom: {request.remote}\tUA: {request.headers.get('User-Agent')}\tIdempotency-Key: {request.headers.get('Idempotency-Key')}"
|
f"{time}\tCommand: {decoded_cmd}\tFrom: {request.remote}\tUA: {request.headers.get('User-Agent')}\tIdempotency-Key: {request.headers.get('Idempotency-Key')}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Process the command
|
# Process the command
|
||||||
if decoded_cmd == "RUNUPDATE":
|
if decoded_cmd == "RUNUPDATE":
|
||||||
response_data = {"status": "success", "message": "Update started"}
|
response_data = {"status": "success", "message": "Update started"}
|
||||||
await run_update()
|
await run_update()
|
||||||
logging.info(f"{time()}\tUpdate command processed successfully")
|
logging.info(f"{time}\tUpdate command processed successfully")
|
||||||
return web.json_response(response_data)
|
return web.json_response(response_data)
|
||||||
else:
|
else:
|
||||||
return web.Response(status=400, text=f"Unknown command: {decoded_cmd}")
|
return web.Response(status=400, text=f"Unknown command: {decoded_cmd}")
|
||||||
@@ -137,12 +293,160 @@ async def request_handler(request: web.Request) -> web.Response:
|
|||||||
print(f"Request handler error: {str(e)}")
|
print(f"Request handler error: {str(e)}")
|
||||||
return web.Response(status=500, text="Internal server error")
|
return web.Response(status=500, text="Internal server error")
|
||||||
|
|
||||||
|
|
||||||
print("Starting web server...")
|
print("Starting web server...")
|
||||||
os.system('cls' if os.name == 'nt' else 'clear')
|
os.system("cls" if os.name == "nt" else "clear")
|
||||||
app = web.Application()
|
app = web.Application()
|
||||||
app.router.add_post("/", request_handler)
|
app.router.add_post("/api", request_handler)
|
||||||
|
app.router.add_post("/hook", hook_handler)
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="studspider - LEGO Inventory Sync Service for Square POS and Brickset",
|
||||||
|
epilog="""
|
||||||
|
Examples:
|
||||||
|
# Basic HTTP server (recommended with nginx reverse proxy)
|
||||||
|
python src/test_server.py --host 0.0.0.0 --port 7432
|
||||||
|
|
||||||
|
# With separate webhook port
|
||||||
|
python src/test_server.py --port 7432 --hook-port 8080
|
||||||
|
|
||||||
|
# HTTPS using certificates from .env (set SSL_CERT_PATH and SSL_KEY_PATH)
|
||||||
|
python src/test_server.py --hook-port 8443 --hook-ssl-only
|
||||||
|
|
||||||
|
# HTTPS with command line override (self-signed cert for testing)
|
||||||
|
python src/test_server.py --ssl-cert cert.pem --ssl-key key.pem --hook-port 8443 --hook-ssl-only
|
||||||
|
|
||||||
|
# Full HTTPS (both endpoints)
|
||||||
|
python src/test_server.py --ssl-cert cert.pem --ssl-key key.pem --hook-port 8443
|
||||||
|
|
||||||
|
# Production behind nginx (no SSL needed in app)
|
||||||
|
python src/test_server.py --host 127.0.0.1 --port 7432
|
||||||
|
|
||||||
|
Environment Variables (.env):
|
||||||
|
SSL_CERT_PATH - Default SSL certificate path
|
||||||
|
SSL_KEY_PATH - Default SSL key path
|
||||||
|
|
||||||
|
For more information, see doc/
|
||||||
|
""",
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--host",
|
||||||
|
type=str,
|
||||||
|
default="localhost",
|
||||||
|
help="IP address to listen on (default: localhost, use 0.0.0.0 for all interfaces)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--port", type=int, default=7432, help="Port for main API endpoint (default: 7432)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--hook-port",
|
||||||
|
type=int,
|
||||||
|
default=None,
|
||||||
|
help="Separate port for webhook endpoint (default: None - uses same port as --port)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--ssl-cert",
|
||||||
|
type=str,
|
||||||
|
default=get_sq_env.SSL_CERT_PATH,
|
||||||
|
help="Path to SSL certificate file in PEM format (default: from .env SSL_CERT_PATH)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--ssl-key",
|
||||||
|
type=str,
|
||||||
|
default=get_sq_env.SSL_KEY_PATH,
|
||||||
|
help="Path to SSL private key file in PEM format (default: from .env SSL_KEY_PATH)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--hook-ssl-only",
|
||||||
|
action="store_true",
|
||||||
|
help="Enable HTTPS only for webhook endpoint, main API stays HTTP (requires --ssl-cert and --ssl-key)",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
print(banner.generate("Edderkop v0.1"))
|
print("Creating log directory if it doesn't exist...")
|
||||||
print("Built for PLAY-WELL TEKNOLOGIES by contact@traps.moe\n\n")
|
os.makedirs("./logs/", exist_ok=True)
|
||||||
web.run_app(app, host="localhost", port=7432)
|
print(banner.generate("studspider v0.1"))
|
||||||
|
print("Built for PLAY-WELL TEKNOLOGIES by contact@studspider.sh\n\n")
|
||||||
|
|
||||||
|
# Setup SSL context if certificates provided
|
||||||
|
ssl_context = None
|
||||||
|
if args.ssl_cert and args.ssl_key:
|
||||||
|
# Check if trying to use P7B file
|
||||||
|
if args.ssl_cert.endswith(".p7b") or args.ssl_key.endswith(".p7b"):
|
||||||
|
print("ERROR: Cannot use P7B files directly for HTTPS server!")
|
||||||
|
print("\nP7B files are for Square's webhook signature verification only.")
|
||||||
|
print("Your server needs PEM format files (.pem)")
|
||||||
|
print("\nWorkflow:")
|
||||||
|
print("1. Generate PEM files: python generate_cert.py")
|
||||||
|
print("2. Convert to P7B for Square: python convert_to_p7b.py")
|
||||||
|
print("3. Upload P7B to Square Dashboard")
|
||||||
|
print("4. Run server with PEM files:")
|
||||||
|
print(
|
||||||
|
" python src/test_server.py --ssl-cert cert.pem --ssl-key key.pem --hook-port 8443"
|
||||||
|
)
|
||||||
|
print("\nIf you have a P7B and need PEM:")
|
||||||
|
print(" python convert_from_p7b.py your_cert.p7b")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
||||||
|
ssl_context.load_cert_chain(args.ssl_cert, args.ssl_key)
|
||||||
|
print(f"✓ SSL enabled with cert: {args.ssl_cert}")
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
print(f"ERROR: Certificate file not found: {e}")
|
||||||
|
exit(1)
|
||||||
|
except ssl.SSLError as e:
|
||||||
|
print(f"ERROR: Invalid certificate or key file: {e}")
|
||||||
|
print("\nMake sure you're using PEM format files.")
|
||||||
|
exit(1)
|
||||||
|
elif args.ssl_cert or args.ssl_key:
|
||||||
|
print("ERROR: Both --ssl-cert and --ssl-key must be provided together")
|
||||||
|
exit(1)
|
||||||
|
else:
|
||||||
|
print("ℹ️ Running without SSL (HTTP only)")
|
||||||
|
print(" For production with nginx reverse proxy, this is recommended.")
|
||||||
|
print(" Nginx will handle HTTPS, forward HTTP to this server.")
|
||||||
|
|
||||||
|
# If hook-port is specified, run a separate server for webhooks
|
||||||
|
if args.hook_port:
|
||||||
|
protocol_main = "https" if (ssl_context and not args.hook_ssl_only) else "http"
|
||||||
|
protocol_hook = "https" if ssl_context else "http"
|
||||||
|
|
||||||
|
# Create separate app for webhook
|
||||||
|
hook_app = web.Application()
|
||||||
|
hook_app.router.add_post("/hook", hook_handler)
|
||||||
|
|
||||||
|
# Run both servers concurrently
|
||||||
|
async def run_servers():
|
||||||
|
runner1 = web.AppRunner(app)
|
||||||
|
await runner1.setup()
|
||||||
|
# Main server uses SSL only if not hook-ssl-only mode
|
||||||
|
main_ssl = None if args.hook_ssl_only else ssl_context
|
||||||
|
site1 = web.TCPSite(runner1, args.host, args.port, ssl_context=main_ssl)
|
||||||
|
|
||||||
|
runner2 = web.AppRunner(hook_app)
|
||||||
|
await runner2.setup()
|
||||||
|
# Webhook server always uses SSL if certificates provided
|
||||||
|
site2 = web.TCPSite(
|
||||||
|
runner2, args.host, args.hook_port, ssl_context=ssl_context
|
||||||
|
)
|
||||||
|
|
||||||
|
await site1.start()
|
||||||
|
await site2.start()
|
||||||
|
|
||||||
|
print(f"Starting main server on {args.host}:{args.port} ({protocol_main})")
|
||||||
|
print(
|
||||||
|
f"Starting webhook server on {args.host}:{args.hook_port} ({protocol_hook})"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Keep running
|
||||||
|
await asyncio.Event().wait()
|
||||||
|
|
||||||
|
asyncio.run(run_servers())
|
||||||
|
else:
|
||||||
|
protocol = "https" if ssl_context else "http"
|
||||||
|
print(f"Server running on {args.host}:{args.port} ({protocol})")
|
||||||
|
print(f"Webhook endpoint: {protocol}://{args.host}:{args.port}/hook")
|
||||||
|
web.run_app(app, host=args.host, port=args.port, ssl_context=ssl_context)
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
"""
|
||||||
|
Test script for webhook endpoint
|
||||||
|
Sends the Square inventory.count.updated webhook payload
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import aiohttp
|
||||||
|
import json
|
||||||
|
import ssl
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
async def test_webhook(verify_ssl=True):
|
||||||
|
url = "https://studspider.sh:8443/hook"
|
||||||
|
|
||||||
|
# Your Square webhook payload
|
||||||
|
payload = {
|
||||||
|
"merchant_id": "6SSW7HV8K2ST5",
|
||||||
|
"type": "inventory.count.updated",
|
||||||
|
"event_id": "df5f3813-a913-45a1-94e9-fdc3f7d5e3b6",
|
||||||
|
"created_at": "2019-10-29T18:38:45.455006797Z",
|
||||||
|
"data": {
|
||||||
|
"type": "inventory_counts",
|
||||||
|
"id": "84e4ac73-d605-4dbd-a9e5-ffff794ddb9d",
|
||||||
|
"object": {
|
||||||
|
"inventory_counts": [
|
||||||
|
{
|
||||||
|
"calculated_at": "2019-10-29T18:38:45.10296Z",
|
||||||
|
"catalog_object_id": "FGQ5JJWT2PYTHF35CKZ2DSKP",
|
||||||
|
"catalog_object_type": "ITEM_VARIATION",
|
||||||
|
"location_id": "YYQR03DGCTXA4",
|
||||||
|
"quantity": "10",
|
||||||
|
"state": "IN_STOCK",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
print("Sending webhook test payload...")
|
||||||
|
print(f"URL: {url}")
|
||||||
|
print(
|
||||||
|
f"SSL Verification: {'Enabled' if verify_ssl else 'Disabled (accepting self-signed certs)'}"
|
||||||
|
)
|
||||||
|
print(json.dumps(payload, indent=2))
|
||||||
|
print("\n" + "=" * 60 + "\n")
|
||||||
|
|
||||||
|
# Create SSL context that allows self-signed certificates if verify_ssl is False
|
||||||
|
ssl_context = None
|
||||||
|
if not verify_ssl:
|
||||||
|
ssl_context = ssl.create_default_context()
|
||||||
|
ssl_context.check_hostname = False
|
||||||
|
ssl_context.verify_mode = ssl.CERT_NONE
|
||||||
|
|
||||||
|
try:
|
||||||
|
connector = aiohttp.TCPConnector(ssl=ssl_context)
|
||||||
|
async with aiohttp.ClientSession(connector=connector) as session:
|
||||||
|
async with session.post(url, json=payload) as resp:
|
||||||
|
print(f"Status: {resp.status}")
|
||||||
|
print(f"Response headers: {dict(resp.headers)}")
|
||||||
|
|
||||||
|
response_text = await resp.text()
|
||||||
|
print(f"\nResponse body:")
|
||||||
|
|
||||||
|
try:
|
||||||
|
response_json = json.loads(response_text)
|
||||||
|
print(json.dumps(response_json, indent=2))
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print(response_text)
|
||||||
|
|
||||||
|
except aiohttp.ClientConnectorError as e:
|
||||||
|
print(f"Connection error: {e}")
|
||||||
|
print("\nTroubleshooting:")
|
||||||
|
print("1. Make sure the server is running")
|
||||||
|
print("2. Check if the hostname and port are correct")
|
||||||
|
print("3. For self-signed certificates, the script now accepts them by default")
|
||||||
|
print("\nServer start command:")
|
||||||
|
print(
|
||||||
|
" python src/test_server.py --ssl-cert cert.pem --ssl-key key.pem --hook-port 8443 --hook-ssl-only"
|
||||||
|
)
|
||||||
|
except aiohttp.ServerTimeoutError as e:
|
||||||
|
print(f"Timeout error: {e}")
|
||||||
|
print("The server took too long to respond")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {type(e).__name__}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Check command line arguments
|
||||||
|
verify_ssl = True
|
||||||
|
if len(sys.argv) > 1 and sys.argv[1] in ["--no-verify", "--insecure", "-k"]:
|
||||||
|
verify_ssl = False
|
||||||
|
print("⚠️ SSL verification disabled - accepting self-signed certificates\n")
|
||||||
|
elif len(sys.argv) > 1:
|
||||||
|
print("Usage: python src/test_webhook.py [--no-verify]")
|
||||||
|
print(
|
||||||
|
" --no-verify: Disable SSL certificate verification (for self-signed certs)"
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Default to no verification since we're likely using self-signed certs
|
||||||
|
asyncio.run(test_webhook(verify_ssl=False))
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
"""
|
||||||
|
Gunicorn configuration for StudSpider webhook server
|
||||||
|
Production-ready settings for running with nginx reverse proxy
|
||||||
|
"""
|
||||||
|
|
||||||
|
import multiprocessing
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Server socket
|
||||||
|
bind = "127.0.0.1:7433" # nginx will proxy to this
|
||||||
|
backlog = 2048
|
||||||
|
|
||||||
|
# Worker processes
|
||||||
|
workers = multiprocessing.cpu_count() * 2 + 1 # Recommended formula
|
||||||
|
worker_class = "uvicorn.workers.UvicornWorker" # Async worker for FastAPI
|
||||||
|
worker_connections = 1000
|
||||||
|
max_requests = 1000 # Restart workers after this many requests
|
||||||
|
max_requests_jitter = 50 # Add randomness to prevent all workers restarting at once
|
||||||
|
|
||||||
|
# Timeouts
|
||||||
|
timeout = 30 # Workers silent for more than this are killed
|
||||||
|
keepalive = 2 # Wait this long for requests on Keep-Alive connections
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
accesslog = "./logs/webhook_access.log"
|
||||||
|
errorlog = "./logs/webhook_error.log"
|
||||||
|
loglevel = "info"
|
||||||
|
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(D)s'
|
||||||
|
|
||||||
|
# Process naming
|
||||||
|
proc_name = "studspider-webhook"
|
||||||
|
|
||||||
|
# Server mechanics
|
||||||
|
daemon = False # Run in foreground (systemd will handle daemonization)
|
||||||
|
pidfile = "./logs/webhook.pid"
|
||||||
|
umask = 0
|
||||||
|
user = None # Run as current user (systemd will set user)
|
||||||
|
group = None
|
||||||
|
tmp_upload_dir = None
|
||||||
|
|
||||||
|
# SSL (not needed with nginx, but here for reference)
|
||||||
|
# keyfile = None
|
||||||
|
# certfile = None
|
||||||
|
|
||||||
|
# Security
|
||||||
|
limit_request_line = 4096
|
||||||
|
limit_request_fields = 100
|
||||||
|
limit_request_field_size = 8190
|
||||||
|
|
||||||
|
# Debugging (disable in production)
|
||||||
|
reload = os.getenv("GUNICORN_RELOAD", "false").lower() == "true"
|
||||||
|
reload_engine = "auto"
|
||||||
|
|
||||||
|
|
||||||
|
# Hooks
|
||||||
|
def on_starting(server):
|
||||||
|
"""Called just before the master process is initialized."""
|
||||||
|
print("Studspider WEB starting...")
|
||||||
|
|
||||||
|
|
||||||
|
def on_reload(server):
|
||||||
|
"""Called to recycle workers during a reload via SIGHUP."""
|
||||||
|
print("🔄 Reloading workers...")
|
||||||
|
|
||||||
|
|
||||||
|
def when_ready(server):
|
||||||
|
"""Called just after the server is started."""
|
||||||
|
print(f"STUDSPIDER HOOK v0.1 RUNNING ON {bind}")
|
||||||
|
print(f"WORKERS: {workers}")
|
||||||
|
print(f"WORKING CLASS: {worker_class}\n")
|
||||||
|
|
||||||
|
|
||||||
|
def worker_int(worker):
|
||||||
|
"""Called just after a worker exited on SIGINT or SIGQUIT."""
|
||||||
|
print(f"⚠️ Worker {worker.pid} received INT or QUIT signal")
|
||||||
|
|
||||||
|
|
||||||
|
def worker_abort(worker):
|
||||||
|
"""Called when a worker received the SIGABRT signal."""
|
||||||
|
print(f"❌ Worker {worker.pid} aborted")
|
||||||
@@ -0,0 +1,278 @@
|
|||||||
|
"""
|
||||||
|
Production webhook server using FastAPI + Gunicorn/Uvicorn
|
||||||
|
Receives Square webhooks with signature verification
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import asyncio
|
||||||
|
from typing import Dict, Any
|
||||||
|
from fastapi import FastAPI, Request, Response, status
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
import dotenv
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
dotenv.load_dotenv()
|
||||||
|
|
||||||
|
# Square webhook validation
|
||||||
|
from square.utils.webhooks_helper import verify_signature
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
SQUARE_WEBHOOK_SIGNATURE_KEY = os.getenv("SANDBOX_SQUARE_WEBHOOK_INVENTORY_ID")
|
||||||
|
SQUARE_WEBHOOK_URL = os.getenv("SQUARE_WEBHOOK_URL")
|
||||||
|
|
||||||
|
# Debug: Print configuration (redact key)
|
||||||
|
if SQUARE_WEBHOOK_SIGNATURE_KEY:
|
||||||
|
print(f"[CONFIG] Signature key loaded: {SQUARE_WEBHOOK_SIGNATURE_KEY[:10]}...")
|
||||||
|
else:
|
||||||
|
print("[CONFIG] WARNING: SQUARE_WEBHOOK_SIGNATURE_KEY not set!")
|
||||||
|
|
||||||
|
if SQUARE_WEBHOOK_URL:
|
||||||
|
print(f"[CONFIG] Webhook URL: {SQUARE_WEBHOOK_URL}")
|
||||||
|
else:
|
||||||
|
print("[CONFIG] WARNING: SQUARE_WEBHOOK_URL not set!")
|
||||||
|
|
||||||
|
# In-memory store for processed event IDs (idempotency)
|
||||||
|
processed_event_ids = set()
|
||||||
|
MAX_STORED_EVENT_IDS = 10000
|
||||||
|
|
||||||
|
# Create FastAPI app
|
||||||
|
app = FastAPI(title="Studspider")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def root():
|
||||||
|
"""Landing page"""
|
||||||
|
return {
|
||||||
|
"service": "Studspider",
|
||||||
|
"status": "running",
|
||||||
|
"endpoints": {"webhook": "/hook (POST)", "health": "/health (GET)"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
async def health_check():
|
||||||
|
"""Health check endpoint for monitoring"""
|
||||||
|
return {
|
||||||
|
"status": "healthy",
|
||||||
|
"signature_verification": (
|
||||||
|
"enabled" if SQUARE_WEBHOOK_SIGNATURE_KEY else "disabled"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/hook")
|
||||||
|
async def webhook_handler(request: Request):
|
||||||
|
"""
|
||||||
|
Handle Square webhook events - MUST respond within 2 seconds!
|
||||||
|
|
||||||
|
Square sends POST requests with:
|
||||||
|
- x-square-hmacsha256-signature header
|
||||||
|
- JSON body with event data
|
||||||
|
- event_id for idempotency
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Log incoming request
|
||||||
|
print(f"\n[WEBHOOK] Incoming request from {request.client.host}")
|
||||||
|
print(f"[WEBHOOK] Method: {request.method}")
|
||||||
|
print(f"[WEBHOOK] URL: {request.url}")
|
||||||
|
print(f"[WEBHOOK] Headers: {dict(request.headers)}")
|
||||||
|
|
||||||
|
# Read raw body for signature verification
|
||||||
|
body = await request.body()
|
||||||
|
body_str = body.decode("utf-8")
|
||||||
|
print(f"[WEBHOOK] Body length: {len(body_str)} bytes")
|
||||||
|
|
||||||
|
# Get signature header
|
||||||
|
signature_header = request.headers.get("x-square-hmacsha256-signature", "")
|
||||||
|
|
||||||
|
# Verify Square webhook signature (if configured)
|
||||||
|
if SQUARE_WEBHOOK_SIGNATURE_KEY and SQUARE_WEBHOOK_URL:
|
||||||
|
if not signature_header:
|
||||||
|
print(
|
||||||
|
f"[ERROR] Missing x-square-hmacsha256-signature header from {request.client.host}"
|
||||||
|
)
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
content={"error": "Missing signature header"},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Debug signature verification inputs
|
||||||
|
print(f"[DEBUG] Signature header: {signature_header[:20]}...")
|
||||||
|
print(f"[DEBUG] Signature key (first 10 chars): {SQUARE_WEBHOOK_SIGNATURE_KEY[:10]}...")
|
||||||
|
print(f"[DEBUG] Notification URL: {SQUARE_WEBHOOK_URL}")
|
||||||
|
print(f"[DEBUG] Body (first 100 chars): {body_str[:100]}...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
is_valid = verify_signature(
|
||||||
|
request_body=body_str,
|
||||||
|
signature_header=signature_header,
|
||||||
|
signature_key=SQUARE_WEBHOOK_SIGNATURE_KEY,
|
||||||
|
notification_url=SQUARE_WEBHOOK_URL,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not is_valid:
|
||||||
|
print(
|
||||||
|
f"[ERROR] Invalid webhook signature from {request.client.host}"
|
||||||
|
)
|
||||||
|
print(f"[ERROR] This usually means:")
|
||||||
|
print(f"[ERROR] 1. SQUARE_WEBHOOK_URL doesn't match Square Dashboard exactly")
|
||||||
|
print(f"[ERROR] 2. SQUARE_WEBHOOK_SIGNATURE_KEY is incorrect")
|
||||||
|
print(f"[ERROR] 3. Request body was modified in transit")
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
content={"error": "Invalid signature"},
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"[INFO] ✓ Webhook signature verified from {request.client.host}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Signature verification exception: {e}")
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
content={"error": f"Signature verification failed: {str(e)}"},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
f"[WARN] Webhook signature verification DISABLED - configure SQUARE_WEBHOOK_SIGNATURE_KEY"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Parse JSON data
|
||||||
|
try:
|
||||||
|
data = json.loads(body_str)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
print(f"[ERROR] Invalid JSON from {request.client.host}: {e}")
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
content={"error": "Invalid JSON"},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check idempotency (event_id)
|
||||||
|
event_id = data.get("event_id")
|
||||||
|
if event_id:
|
||||||
|
if event_id in processed_event_ids:
|
||||||
|
print(
|
||||||
|
f"[INFO] Duplicate event {event_id} - already processed (idempotency)"
|
||||||
|
)
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status.HTTP_200_OK,
|
||||||
|
content={
|
||||||
|
"status": "success",
|
||||||
|
"message": "Event already processed",
|
||||||
|
"event_id": event_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add to processed set
|
||||||
|
processed_event_ids.add(event_id)
|
||||||
|
|
||||||
|
# Prevent memory growth
|
||||||
|
if len(processed_event_ids) > MAX_STORED_EVENT_IDS:
|
||||||
|
processed_event_ids.pop()
|
||||||
|
|
||||||
|
# Get event info for quick response
|
||||||
|
webhook_type = data.get("type", "unknown")
|
||||||
|
event_id_val = data.get("event_id", "none")
|
||||||
|
merchant_id = data.get("merchant_id", "unknown")
|
||||||
|
|
||||||
|
# RESPOND IMMEDIATELY - Square requires fast response (< 2 seconds)
|
||||||
|
response_data = {
|
||||||
|
"status": "success",
|
||||||
|
"message": "Webhook received",
|
||||||
|
"event_id": event_id_val,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Process webhook in background (async task)
|
||||||
|
asyncio.create_task(
|
||||||
|
process_webhook(
|
||||||
|
webhook_type, event_id_val, merchant_id, data, request.client.host
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Return immediate response
|
||||||
|
print(f"[INFO] 200 OK response sent for event {event_id_val}")
|
||||||
|
return JSONResponse(status_code=status.HTTP_200_OK, content=response_data)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Catch-all for any unexpected errors - still respond quickly
|
||||||
|
print(f"[ERROR] Unexpected error in webhook handler: {e}")
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
traceback.print_exc()
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
content={"error": "Internal server error"},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Return immediate response
|
||||||
|
return JSONResponse(status_code=status.HTTP_200_OK, content=response_data)
|
||||||
|
|
||||||
|
|
||||||
|
async def process_webhook(
|
||||||
|
webhook_type: str,
|
||||||
|
event_id: str,
|
||||||
|
merchant_id: str,
|
||||||
|
data: Dict[str, Any],
|
||||||
|
client_ip: str,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Process webhook data asynchronously after responding to Square
|
||||||
|
|
||||||
|
This runs in the background and doesn't block the HTTP response
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print(f"[WEBHOOK] Received at {asyncio.get_event_loop().time()}")
|
||||||
|
print(f"[WEBHOOK] From: {client_ip}")
|
||||||
|
print(f"[WEBHOOK] Type: {webhook_type}")
|
||||||
|
print(f"[WEBHOOK] Event ID: {event_id}")
|
||||||
|
print(f"[WEBHOOK] Merchant: {merchant_id}")
|
||||||
|
print(f"[WEBHOOK] Full payload:")
|
||||||
|
print(json.dumps(data, indent=2))
|
||||||
|
print("=" * 60 + "\n")
|
||||||
|
|
||||||
|
# Process based on webhook type
|
||||||
|
if webhook_type == "inventory.count.updated":
|
||||||
|
print(f"[PROCESS] Processing inventory count update...")
|
||||||
|
inventory_counts = (
|
||||||
|
data.get("data", {}).get("object", {}).get("inventory_counts", [])
|
||||||
|
)
|
||||||
|
for count in inventory_counts:
|
||||||
|
catalog_id = count.get("catalog_object_id")
|
||||||
|
quantity = count.get("quantity")
|
||||||
|
state = count.get("state")
|
||||||
|
print(
|
||||||
|
f"[PROCESS] Catalog: {catalog_id}, Qty: {quantity}, State: {state}"
|
||||||
|
)
|
||||||
|
|
||||||
|
elif webhook_type == "catalog.version.updated":
|
||||||
|
print(f"[PROCESS] Processing catalog version update...")
|
||||||
|
# Add your catalog processing logic here
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(f"[PROCESS] Webhook type '{webhook_type}' - no handler configured")
|
||||||
|
|
||||||
|
print(f"[PROCESS] ✓ Webhook processing complete for {event_id}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Failed to process webhook {event_id}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# For development/testing - use uvicorn directly
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("Studspider webhook boot dev")
|
||||||
|
print("=" * 60)
|
||||||
|
print(
|
||||||
|
f"Signature verification: {'ENABLED' if SQUARE_WEBHOOK_SIGNATURE_KEY else 'DISABLED'}"
|
||||||
|
)
|
||||||
|
print(f"Webhook URL: {SQUARE_WEBHOOK_URL or 'NOT CONFIGURED'}")
|
||||||
|
print("\nFor production, use Gunicorn:")
|
||||||
|
print(
|
||||||
|
" gunicorn webhook_server:app -w 4 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:7433"
|
||||||
|
)
|
||||||
|
print("=" * 60 + "\n")
|
||||||
|
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=7433, log_level="info")
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
"""
|
||||||
|
Simple test script to verify Square webhook signature verification works correctly.
|
||||||
|
|
||||||
|
This uses the same test data from Square's documentation to ensure our implementation
|
||||||
|
matches their expected behavior.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, "src")
|
||||||
|
|
||||||
|
from square.utils.webhooks_helper import verify_signature
|
||||||
|
|
||||||
|
# Test data from Square's documentation
|
||||||
|
TEST_BODY = '{"hello":"world"}'
|
||||||
|
TEST_SIGNATURE = "2kRE5qRU2tR+tBGlDwMEw2avJ7QM4ikPYD/PJ3bd9Og="
|
||||||
|
TEST_SIGNATURE_KEY = "asdf1234"
|
||||||
|
TEST_NOTIFICATION_URL = "https://example.com/webhook"
|
||||||
|
|
||||||
|
|
||||||
|
def test_signature_validation():
|
||||||
|
"""Test that signature validation works with Square SDK"""
|
||||||
|
|
||||||
|
print("Testing Square webhook signature validation...")
|
||||||
|
print(f"Body: {TEST_BODY}")
|
||||||
|
print(f"Signature: {TEST_SIGNATURE}")
|
||||||
|
print(f"Key: {TEST_SIGNATURE_KEY}")
|
||||||
|
print(f"URL: {TEST_NOTIFICATION_URL}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# This should return True with Square's test data
|
||||||
|
is_valid = verify_signature(
|
||||||
|
request_body=TEST_BODY,
|
||||||
|
signature_header=TEST_SIGNATURE,
|
||||||
|
signature_key=TEST_SIGNATURE_KEY,
|
||||||
|
notification_url=TEST_NOTIFICATION_URL,
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_valid:
|
||||||
|
print("✅ SUCCESS: Signature validation passed!")
|
||||||
|
print("The webhook implementation is using Square SDK correctly.")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print("❌ FAILURE: Signature validation failed!")
|
||||||
|
print("Something is wrong with the signature verification.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
success = test_signature_validation()
|
||||||
|
sys.exit(0 if success else 1)
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import src.test_server
|
import src.test_server
|
||||||
|
|
||||||
|
|
||||||
class edderkop:
|
class Studspider:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.name = "Edderkop"
|
self.name = "Studspider"
|
||||||
self.version = "0.0.1"
|
self.version = "0.0.1"
|
||||||
self.description = """
|
self.description = """
|
||||||
Edderkop is an application designed to interact with the Brickset API and directly with Square.
|
studspider is an application designed to interact with the Brickset API and directly with Square.
|
||||||
It allows users to search for items, manage collections, and perform various operations related to bricksets.
|
It allows users to search for items, manage collections, and perform various operations related to bricksets.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -69,6 +69,20 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/f5/10/6c25ed6de94c49f88a91fa5018cb4c0f3625f31d5be9f771ebe5cc7cd506/aiosqlite-0.21.0-py3-none-any.whl", hash = "sha256:2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0", size = 15792, upload-time = "2025-02-03T07:30:13.6Z" },
|
{ url = "https://files.pythonhosted.org/packages/f5/10/6c25ed6de94c49f88a91fa5018cb4c0f3625f31d5be9f771ebe5cc7cd506/aiosqlite-0.21.0-py3-none-any.whl", hash = "sha256:2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0", size = 15792, upload-time = "2025-02-03T07:30:13.6Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "alembic"
|
||||||
|
version = "1.17.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "mako" },
|
||||||
|
{ name = "sqlalchemy" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/6b/45/6f4555f2039f364c3ce31399529dcf48dd60726ff3715ad67f547d87dfd2/alembic-1.17.0.tar.gz", hash = "sha256:4652a0b3e19616b57d652b82bfa5e38bf5dbea0813eed971612671cb9e90c0fe", size = 1975526, upload-time = "2025-10-11T18:40:13.585Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/1f/38e29b06bfed7818ebba1f84904afdc8153ef7b6c7e0d8f3bc6643f5989c/alembic-1.17.0-py3-none-any.whl", hash = "sha256:80523bc437d41b35c5db7e525ad9d908f79de65c27d6a5a5eab6df348a352d99", size = 247449, upload-time = "2025-10-11T18:40:16.288Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "altgraph"
|
name = "altgraph"
|
||||||
version = "0.17.4"
|
version = "0.17.4"
|
||||||
@@ -78,6 +92,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/4d/3f/3bc3f1d83f6e4a7fcb834d3720544ca597590425be5ba9db032b2bf322a2/altgraph-0.17.4-py2.py3-none-any.whl", hash = "sha256:642743b4750de17e655e6711601b077bc6598dbfa3ba5fa2b2a35ce12b508dff", size = 21212, upload-time = "2023-09-25T09:04:50.691Z" },
|
{ url = "https://files.pythonhosted.org/packages/4d/3f/3bc3f1d83f6e4a7fcb834d3720544ca597590425be5ba9db032b2bf322a2/altgraph-0.17.4-py2.py3-none-any.whl", hash = "sha256:642743b4750de17e655e6711601b077bc6598dbfa3ba5fa2b2a35ce12b508dff", size = 21212, upload-time = "2023-09-25T09:04:50.691Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "annotated-doc"
|
||||||
|
version = "0.0.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "annotated-types"
|
name = "annotated-types"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
@@ -143,6 +166,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/94/fe/3aed5d0be4d404d12d36ab97e2f1791424d9ca39c2f754a6285d59a3b01d/beautifulsoup4-4.14.2-py3-none-any.whl", hash = "sha256:5ef6fa3a8cbece8488d66985560f97ed091e22bbc4e9c2338508a9d5de6d4515", size = 106392, upload-time = "2025-09-29T10:05:43.771Z" },
|
{ url = "https://files.pythonhosted.org/packages/94/fe/3aed5d0be4d404d12d36ab97e2f1791424d9ca39c2f754a6285d59a3b01d/beautifulsoup4-4.14.2-py3-none-any.whl", hash = "sha256:5ef6fa3a8cbece8488d66985560f97ed091e22bbc4e9c2338508a9d5de6d4515", size = 106392, upload-time = "2025-09-29T10:05:43.771Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bidict"
|
||||||
|
version = "0.23.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9a/6e/026678aa5a830e07cd9498a05d3e7e650a4f56a42f267a53d22bcda1bdc9/bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71", size = 29093, upload-time = "2024-02-18T19:09:05.748Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5", size = 32764, upload-time = "2024-02-18T19:09:04.156Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bs4"
|
name = "bs4"
|
||||||
version = "0.0.2"
|
version = "0.0.2"
|
||||||
@@ -184,6 +216,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" },
|
{ url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "click"
|
||||||
|
version = "8.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "colorama"
|
name = "colorama"
|
||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
@@ -205,34 +249,18 @@ wheels = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "edderkop"
|
name = "fastapi"
|
||||||
version = "0.1.0"
|
version = "0.128.0"
|
||||||
source = { virtual = "." }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiohttp" },
|
{ name = "annotated-doc" },
|
||||||
{ name = "aiosqlite" },
|
{ name = "pydantic" },
|
||||||
{ name = "apscheduler" },
|
{ name = "starlette" },
|
||||||
{ name = "asyncio-throttle" },
|
{ name = "typing-extensions" },
|
||||||
{ name = "bs4" },
|
|
||||||
{ name = "dotenv" },
|
|
||||||
{ name = "pyinstaller" },
|
|
||||||
{ name = "pytest" },
|
|
||||||
{ name = "requests" },
|
|
||||||
{ name = "squareup" },
|
|
||||||
]
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/52/08/8c8508db6c7b9aae8f7175046af41baad690771c9bcde676419965e338c7/fastapi-0.128.0.tar.gz", hash = "sha256:1cc179e1cef10a6be60ffe429f79b829dce99d8de32d7acb7e6c8dfdf7f2645a", size = 365682, upload-time = "2025-12-27T15:21:13.714Z" }
|
||||||
[package.metadata]
|
wheels = [
|
||||||
requires-dist = [
|
{ url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094, upload-time = "2025-12-27T15:21:12.154Z" },
|
||||||
{ name = "aiohttp", specifier = ">=3.12.13,<4.0.0" },
|
|
||||||
{ name = "aiosqlite", specifier = ">=0.21.0,<0.22.0" },
|
|
||||||
{ name = "apscheduler", specifier = ">=3.11.0,<4.0.0" },
|
|
||||||
{ name = "asyncio-throttle", specifier = ">=1.0.2,<2.0.0" },
|
|
||||||
{ name = "bs4", specifier = ">=0.0.2" },
|
|
||||||
{ name = "dotenv", specifier = ">=0.9.9,<0.10.0" },
|
|
||||||
{ name = "pyinstaller", specifier = ">=6.14.1,<7.0.0" },
|
|
||||||
{ name = "pytest", specifier = ">=8.4.2,<9.0.0" },
|
|
||||||
{ name = "requests", specifier = ">=2.32.4,<3.0.0" },
|
|
||||||
{ name = "squareup", specifier = ">=43.0.0.20250618,<44.0.0.0" },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -278,6 +306,71 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106, upload-time = "2025-06-09T23:02:34.204Z" },
|
{ url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106, upload-time = "2025-06-09T23:02:34.204Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "granian"
|
||||||
|
version = "2.5.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "click" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/07/85/3f5a1258567718c75719f5206d33457f7bd2b091b0fee0a618a395fda758/granian-2.5.5.tar.gz", hash = "sha256:da785fae71cb45e92ce3fbb8633dc48b12f6a5055a7358226d78176967a5d2c9", size = 112143, upload-time = "2025-10-07T17:39:38.751Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/fc/8024558e038477531cdbf996f94ff9d64c008a33ffd33077b38d43451193/granian-2.5.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:49fd6a3f5d9302b8c8da56fcbf29faa7edc5847a460635356c5052584fa7c4b2", size = 2833726, upload-time = "2025-10-07T17:37:56.296Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/6e/baae1148dc834bbdf07ca45920641c23ff167b2af338cfcd3551e063aee1/granian-2.5.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:62e7d9dd5b21d7637e062f7ce20d1887069d64d06e16c7bac40e01de4cb54b63", size = 2517287, upload-time = "2025-10-07T17:37:57.949Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/77/23/65398e16a121cdab95ac6a31c48172f86ff333ac01dbc8d57c2e9496c668/granian-2.5.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:307e09e1bf5b7f869b9dccfca7af253bbb74bf4cb0ba2d972043334a889b048f", size = 3009557, upload-time = "2025-10-07T17:37:59.664Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/01/1ec3bb03528cf5330a5b1d2c77f1a4d224a34a3c73ea47a860ef4d72146b/granian-2.5.5-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a847903664275e6bede3ef178642379c6e1c43767e3980b2b6d68c62e6b14b5", size = 2833794, upload-time = "2025-10-07T17:38:00.993Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1e/a5/fe976a36945cea22b7c4a9eb8ddd9866644c12f766b9f3ab8bd575e9d611/granian-2.5.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e21cd8ee95cd2017328043ec31c455ef69c9466617a9d51e4b1ca0ff0203d873", size = 3117180, upload-time = "2025-10-07T17:38:02.573Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a2/f2/0a7450990a5ec23a3dc5c464886ece21593dc4edd027e3a6f5367fbb0cd4/granian-2.5.5-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:f47b9ecf635ef727b1f399cd2e65329010ab2c48a5381be0d928a538ed8e9158", size = 2917766, upload-time = "2025-10-07T17:38:04.327Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/93/81/1238e871ef9e609e12b9ada411793c87343c46905f8f56c8a6d4088d9ae6/granian-2.5.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:59e868dcc7ca469aa099ca9456204c2ff5e6c7e98bbafb66921b911c2b5e4c15", size = 2981663, upload-time = "2025-10-07T17:38:05.722Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b0/3c/0ad5915c0a96d2c4de13e51f107d11013ed9a48e01524ec8cc096afdc639/granian-2.5.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:c0a1ef4bac9a59d52a80f81153a15c82a6876591e9ab8e330e096f339d06211d", size = 3146507, upload-time = "2025-10-07T17:38:07.078Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/75/f7/2d3d3372cf81775e72196a1a7b26cf563dbe35ec5cc197dd4f9e3df5d050/granian-2.5.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:748efbb431d46aff3b9ef88b68fddfc7b29e7738292fca841b1b925f67e328a4", size = 3207738, upload-time = "2025-10-07T17:38:08.345Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8d/1b/d663e071cac94f3be515c3650aa6675432d3a9ccbb4c419a20bb31083d92/granian-2.5.5-cp313-cp313-win_amd64.whl", hash = "sha256:fa87dd2d4b914e6d539bf18968daad3d34bb6877ab90b1408c009c3714a0213c", size = 2178462, upload-time = "2025-10-07T17:38:09.708Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/9f/edec61a604d919042b435eb6f40217c7ff565fde91e9670183c895eaf4e1/granian-2.5.5-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:647cb53789cbf369e895341152a9822f8238830fc939b85d2844151d1f0da32e", size = 2759098, upload-time = "2025-10-07T17:38:10.962Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/76/f96df41fbc72b4c9a9f0994f2ae274080a3b47b8209989140a1f98ba27a9/granian-2.5.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:39f1d5398d31df3375e10524aa00f33d65b7d051551c4876251e9eec4ddc2c11", size = 2477953, upload-time = "2025-10-07T17:38:12.329Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/70/0bfa32321501756357efebc45668c9ba054f8b8af1c63590d5af2aaed59c/granian-2.5.5-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61308acf1943d06c4c4b48f79697a97f233ce85a07801e95e01c85647fd10eb5", size = 2997992, upload-time = "2025-10-07T17:38:13.627Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3a/a1/9a961b05c6cde2b6b27abae5da9e5efdfe3c7e0fc8b6f63433473861c2ff/granian-2.5.5-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:dfd11cb106d9c68c6682a0554142f7383ff99789e1ecef3087c3e13ac50fde24", size = 2784260, upload-time = "2025-10-07T17:38:15.32Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e2/1a/9eada34cb30c4cd17e8201082b2a5f4d159ed275fdbc072355bc8ab32753/granian-2.5.5-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:d05801e405f4000837bba9eea7cdef0486a6c8a2aff073e75be0a7055c2c07c6", size = 2973232, upload-time = "2025-10-07T17:38:16.717Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5d/24/bbdc6dcb4197da663b2e8f4442f234ccfcd6fd883a9448a0fa3629cd6fe1/granian-2.5.5-cp313-cp313t-musllinux_1_1_armv7l.whl", hash = "sha256:28c61a87c766fc22f5d1ad640e6a4f2a4c14ff86c629c7fa8c9cc0bbc756a18c", size = 3132458, upload-time = "2025-10-07T17:38:18.134Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0f/b9/a44efa4771859bd65ff1e103b57f1c010b8d3434b20120fc0e44d0f28b41/granian-2.5.5-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:1cf9431bc5096e2f38aa8b8c762ad4e5e2f1e119ca7381a3414e1ce64690ab5c", size = 3194088, upload-time = "2025-10-07T17:38:19.408Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/03/3d33933328a2003ebd45c5d781387df59b60bc2465c9c5ea308930509ca7/granian-2.5.5-cp313-cp313t-win_amd64.whl", hash = "sha256:63a4910032589d25ce09bc2d1e5164db941710c17fe91a543f8bd3e3b9a5d522", size = 2170457, upload-time = "2025-10-07T17:38:21.077Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.optional-dependencies]
|
||||||
|
reload = [
|
||||||
|
{ name = "watchfiles" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "greenlet"
|
||||||
|
version = "3.2.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/03/b8/704d753a5a45507a7aab61f18db9509302ed3d0a27ac7e0359ec2905b1a6/greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", size = 188260, upload-time = "2025-08-07T13:24:33.51Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516, upload-time = "2025-08-07T13:53:16.314Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169, upload-time = "2025-08-07T13:18:32.861Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1c/53/f9c440463b3057485b8594d7a638bed53ba531165ef0ca0e6c364b5cc807/greenlet-3.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e343822feb58ac4d0a1211bd9399de2b3a04963ddeec21530fc426cc121f19b", size = 1564759, upload-time = "2025-11-04T12:42:19.395Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/e4/3bb4240abdd0a8d23f4f88adec746a3099f0d86bfedb623f063b2e3b4df0/greenlet-3.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca7f6f1f2649b89ce02f6f229d7c19f680a6238af656f61e0115b24857917929", size = 1634288, upload-time = "2025-11-04T12:42:21.174Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gunicorn"
|
||||||
|
version = "23.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "packaging" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031, upload-time = "2024-08-10T20:25:27.378Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h11"
|
name = "h11"
|
||||||
version = "0.16.0"
|
version = "0.16.0"
|
||||||
@@ -345,6 +438,69 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/d1/5d/c059c180c84f7962db0aeae7c3b9303ed1d73d76f2bfbc32bc231c8be314/macholib-1.16.3-py2.py3-none-any.whl", hash = "sha256:0e315d7583d38b8c77e815b1ecbdbf504a8258d8b3e17b61165c6feb60d18f2c", size = 38094, upload-time = "2023-09-25T09:10:14.188Z" },
|
{ url = "https://files.pythonhosted.org/packages/d1/5d/c059c180c84f7962db0aeae7c3b9303ed1d73d76f2bfbc32bc231c8be314/macholib-1.16.3-py2.py3-none-any.whl", hash = "sha256:0e315d7583d38b8c77e815b1ecbdbf504a8258d8b3e17b61165c6feb60d18f2c", size = 38094, upload-time = "2023-09-25T09:10:14.188Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mako"
|
||||||
|
version = "1.3.10"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "markupsafe" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "markdown-it-py"
|
||||||
|
version = "4.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "mdurl" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "markupsafe"
|
||||||
|
version = "3.0.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mdurl"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "multidict"
|
name = "multidict"
|
||||||
version = "6.6.4"
|
version = "6.6.4"
|
||||||
@@ -408,6 +564,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/55/26/d0ad8b448476d0a1e8d3ea5622dc77b916db84c6aa3cb1e1c0965af948fc/pefile-2023.2.7-py3-none-any.whl", hash = "sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6", size = 71791, upload-time = "2023-02-07T12:28:36.678Z" },
|
{ url = "https://files.pythonhosted.org/packages/55/26/d0ad8b448476d0a1e8d3ea5622dc77b916db84c6aa3cb1e1c0965af948fc/pefile-2023.2.7-py3-none-any.whl", hash = "sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6", size = 71791, upload-time = "2023-02-07T12:28:36.678Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "platformdirs"
|
||||||
|
version = "4.5.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pluggy"
|
name = "pluggy"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
@@ -458,6 +623,17 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663, upload-time = "2025-06-09T22:56:04.484Z" },
|
{ url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663, upload-time = "2025-06-09T22:56:04.484Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "psutil"
|
||||||
|
version = "7.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b3/31/4723d756b59344b643542936e37a31d1d3204bcdc42a7daa8ee9eb06fb50/psutil-7.1.0.tar.gz", hash = "sha256:655708b3c069387c8b77b072fc429a57d0e214221d01c0a772df7dfedcb3bcd2", size = 497660, upload-time = "2025-09-17T20:14:52.902Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/79/87/157c8e7959ec39ced1b11cc93c730c4fb7f9d408569a6c59dbd92ceb35db/psutil-7.1.0-cp37-abi3-win32.whl", hash = "sha256:09ad740870c8d219ed8daae0ad3b726d3bf9a028a198e7f3080f6a1888b99bca", size = 244812, upload-time = "2025-09-17T20:15:07.462Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/e9/b44c4f697276a7a95b8e94d0e320a7bf7f3318521b23de69035540b39838/psutil-7.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:57f5e987c36d3146c0dd2528cd42151cf96cd359b9d67cfff836995cc5df9a3d", size = 247965, upload-time = "2025-09-17T20:15:09.673Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/26/65/1070a6e3c036f39142c2820c4b52e9243246fcfc3f96239ac84472ba361e/psutil-7.1.0-cp37-abi3-win_arm64.whl", hash = "sha256:6937cb68133e7c97b6cc9649a570c9a18ba0efebed46d8c5dae4c07fa1b67a07", size = 244971, upload-time = "2025-09-17T20:15:12.262Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic"
|
name = "pydantic"
|
||||||
version = "2.11.7"
|
version = "2.11.7"
|
||||||
@@ -576,6 +752,40 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" },
|
{ url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-engineio"
|
||||||
|
version = "4.12.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "simple-websocket" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c9/d8/63e5535ab21dc4998ba1cfe13690ccf122883a38f025dca24d6e56c05eba/python_engineio-4.12.3.tar.gz", hash = "sha256:35633e55ec30915e7fc8f7e34ca8d73ee0c080cec8a8cd04faf2d7396f0a7a7a", size = 91910, upload-time = "2025-09-28T06:31:36.765Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d8/f0/c5aa0a69fd9326f013110653543f36ece4913c17921f3e1dbd78e1b423ee/python_engineio-4.12.3-py3-none-any.whl", hash = "sha256:7c099abb2a27ea7ab429c04da86ab2d82698cdd6c52406cb73766fe454feb7e1", size = 59637, upload-time = "2025-09-28T06:31:35.354Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-multipart"
|
||||||
|
version = "0.0.20"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-socketio"
|
||||||
|
version = "5.14.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "bidict" },
|
||||||
|
{ name = "python-engineio" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/05/c2/a9ae3d0eb4488748a2d9c15defddb7277a852234e29e50c73136834dff1b/python_socketio-5.14.1.tar.gz", hash = "sha256:bf49657073b90ee09e4cbd6651044b46bb526694276621e807a1b8fcc0c1b25b", size = 123068, upload-time = "2025-10-02T18:44:54.467Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/5e/302c3499a134a52b68e4e6fb345cea52ab1c41460949bcdb09f8bd0e3594/python_socketio-5.14.1-py3-none-any.whl", hash = "sha256:3419f5917f0e3942317836a77146cb4caa23ad804c8fd1a7e3f44a6657a8406e", size = 78496, upload-time = "2025-10-02T18:44:52.649Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pywin32-ctypes"
|
name = "pywin32-ctypes"
|
||||||
version = "0.2.3"
|
version = "0.2.3"
|
||||||
@@ -585,6 +795,59 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" },
|
{ url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redis"
|
||||||
|
version = "6.4.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/0d/d6/e8b92798a5bd67d659d51a18170e91c16ac3b59738d91894651ee255ed49/redis-6.4.0.tar.gz", hash = "sha256:b01bc7282b8444e28ec36b261df5375183bb47a07eb9c603f284e89cbc5ef010", size = 4647399, upload-time = "2025-08-07T08:10:11.441Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/02/89e2ed7e85db6c93dfa9e8f691c5087df4e3551ab39081a4d7c6d1f90e05/redis-6.4.0-py3-none-any.whl", hash = "sha256:f0544fa9604264e9464cdf4814e7d4830f74b165d52f2a330a760a88dd248b7f", size = 279847, upload-time = "2025-08-07T08:10:09.84Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "reflex"
|
||||||
|
version = "0.8.14.post1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "alembic" },
|
||||||
|
{ name = "click" },
|
||||||
|
{ name = "granian", extra = ["reload"] },
|
||||||
|
{ name = "httpx" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "platformdirs" },
|
||||||
|
{ name = "psutil", marker = "sys_platform == 'win32'" },
|
||||||
|
{ name = "pydantic" },
|
||||||
|
{ name = "python-multipart" },
|
||||||
|
{ name = "python-socketio" },
|
||||||
|
{ name = "redis" },
|
||||||
|
{ name = "reflex-hosting-cli" },
|
||||||
|
{ name = "rich" },
|
||||||
|
{ name = "sqlmodel" },
|
||||||
|
{ name = "starlette" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
{ name = "wrapt" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/78/fe/b08a7cd740474ef63386c4d69c36b30e0eb4644f97cfcd179c996581f1e8/reflex-0.8.14.post1.tar.gz", hash = "sha256:d78dac6c7176477b68dcb84720efe7972a4b3f52f4ab0d3a95cac3cab02ed4ac", size = 604830, upload-time = "2025-10-06T21:30:35.535Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1d/b3/f8e2a07c94396285cff5e585bce3a3071fe4e9fb112ad8b521b47677cec2/reflex-0.8.14.post1-py3-none-any.whl", hash = "sha256:3bd986cd9e8e6481d14276e95f82125acd5ca3a7492dcaf57c317211b20354b6", size = 894684, upload-time = "2025-10-06T21:30:33.189Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "reflex-hosting-cli"
|
||||||
|
version = "0.1.56"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "click" },
|
||||||
|
{ name = "httpx" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "platformdirs" },
|
||||||
|
{ name = "rich" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/98/51/029207a63593f25cddb9cfe5c59147cf1e5de9a967dfa87cd8b6296e7c41/reflex_hosting_cli-0.1.56.tar.gz", hash = "sha256:b29bb0a2bee0ee59ac4f1c7beecc727e914c3c0ab73fc4a536a5b33c237f40b7", size = 35889, upload-time = "2025-09-25T03:28:29.977Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c6/10/ab023eabe375eed3a0e0ac3350f007465e10a59ca6d70dbde455abcb404d/reflex_hosting_cli-0.1.56-py3-none-any.whl", hash = "sha256:9c6e5f0a26ab61fa20a89a4ea053d36d94e3134507fc1fca428500a0738139b2", size = 45420, upload-time = "2025-09-25T03:28:28.685Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "requests"
|
name = "requests"
|
||||||
version = "2.32.5"
|
version = "2.32.5"
|
||||||
@@ -600,6 +863,19 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
|
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rich"
|
||||||
|
version = "14.2.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "markdown-it-py" },
|
||||||
|
{ name = "pygments" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "setuptools"
|
name = "setuptools"
|
||||||
version = "80.9.0"
|
version = "80.9.0"
|
||||||
@@ -609,6 +885,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" },
|
{ url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simple-websocket"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "wsproto" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b0/d4/bfa032f961103eba93de583b161f0e6a5b63cebb8f2c7d0c6e6efe1e3d2e/simple_websocket-1.1.0.tar.gz", hash = "sha256:7939234e7aa067c534abdab3a9ed933ec9ce4691b0713c78acb195560aa52ae4", size = 17300, upload-time = "2024-10-10T22:39:31.412Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl", hash = "sha256:4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c", size = 13842, upload-time = "2024-10-10T22:39:29.645Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sniffio"
|
name = "sniffio"
|
||||||
version = "1.3.1"
|
version = "1.3.1"
|
||||||
@@ -627,6 +915,40 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679, upload-time = "2025-08-27T15:39:50.179Z" },
|
{ url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679, upload-time = "2025-08-27T15:39:50.179Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sqlalchemy"
|
||||||
|
version = "2.0.44"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f0/f2/840d7b9496825333f532d2e3976b8eadbf52034178aac53630d09fe6e1ef/sqlalchemy-2.0.44.tar.gz", hash = "sha256:0ae7454e1ab1d780aee69fd2aae7d6b8670a581d8847f2d1e0f7ddfbf47e5a22", size = 9819830, upload-time = "2025-10-10T14:39:12.935Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/45/d3/c67077a2249fdb455246e6853166360054c331db4613cda3e31ab1cadbef/sqlalchemy-2.0.44-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ff486e183d151e51b1d694c7aa1695747599bb00b9f5f604092b54b74c64a8e1", size = 2135479, upload-time = "2025-10-10T16:03:37.671Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/91/eabd0688330d6fd114f5f12c4f89b0d02929f525e6bf7ff80aa17ca802af/sqlalchemy-2.0.44-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b1af8392eb27b372ddb783b317dea0f650241cea5bd29199b22235299ca2e45", size = 2123212, upload-time = "2025-10-10T16:03:41.755Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b0/bb/43e246cfe0e81c018076a16036d9b548c4cc649de241fa27d8d9ca6f85ab/sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b61188657e3a2b9ac4e8f04d6cf8e51046e28175f79464c67f2fd35bceb0976", size = 3255353, upload-time = "2025-10-10T15:35:31.221Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b9/96/c6105ed9a880abe346b64d3b6ddef269ddfcab04f7f3d90a0bf3c5a88e82/sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b87e7b91a5d5973dda5f00cd61ef72ad75a1db73a386b62877d4875a8840959c", size = 3260222, upload-time = "2025-10-10T15:43:50.124Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/16/1857e35a47155b5ad927272fee81ae49d398959cb749edca6eaa399b582f/sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:15f3326f7f0b2bfe406ee562e17f43f36e16167af99c4c0df61db668de20002d", size = 3189614, upload-time = "2025-10-10T15:35:32.578Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/ee/4afb39a8ee4fc786e2d716c20ab87b5b1fb33d4ac4129a1aaa574ae8a585/sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e77faf6ff919aa8cd63f1c4e561cac1d9a454a191bb864d5dd5e545935e5a40", size = 3226248, upload-time = "2025-10-10T15:43:51.862Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/32/d5/0e66097fc64fa266f29a7963296b40a80d6a997b7ac13806183700676f86/sqlalchemy-2.0.44-cp313-cp313-win32.whl", hash = "sha256:ee51625c2d51f8baadf2829fae817ad0b66b140573939dd69284d2ba3553ae73", size = 2101275, upload-time = "2025-10-10T15:03:26.096Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/03/51/665617fe4f8c6450f42a6d8d69243f9420f5677395572c2fe9d21b493b7b/sqlalchemy-2.0.44-cp313-cp313-win_amd64.whl", hash = "sha256:c1c80faaee1a6c3428cecf40d16a2365bcf56c424c92c2b6f0f9ad204b899e9e", size = 2127901, upload-time = "2025-10-10T15:03:27.548Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/5e/6a29fa884d9fb7ddadf6b69490a9d45fded3b38541713010dad16b77d015/sqlalchemy-2.0.44-py3-none-any.whl", hash = "sha256:19de7ca1246fbef9f9d1bff8f1ab25641569df226364a0e40457dc5457c54b05", size = 1928718, upload-time = "2025-10-10T15:29:45.32Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sqlmodel"
|
||||||
|
version = "0.0.27"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pydantic" },
|
||||||
|
{ name = "sqlalchemy" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/90/5a/693d90866233e837d182da76082a6d4c2303f54d3aaaa5c78e1238c5d863/sqlmodel-0.0.27.tar.gz", hash = "sha256:ad1227f2014a03905aef32e21428640848ac09ff793047744a73dfdd077ff620", size = 118053, upload-time = "2025-10-08T16:39:11.938Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8c/92/c35e036151fe53822893979f8a13e6f235ae8191f4164a79ae60a95d66aa/sqlmodel-0.0.27-py3-none-any.whl", hash = "sha256:667fe10aa8ff5438134668228dc7d7a08306f4c5c4c7e6ad3ad68defa0e7aa49", size = 29131, upload-time = "2025-10-08T16:39:10.917Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "squareup"
|
name = "squareup"
|
||||||
version = "43.0.2.20250820"
|
version = "43.0.2.20250820"
|
||||||
@@ -642,6 +964,57 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/aa/3e/162f70cdaa9f68864f77db4c29aaa70754800181b06b629dc66461c7135e/squareup-43.0.2.20250820-py3-none-any.whl", hash = "sha256:26de4e23c9bb735d4c5bb89a72ff1bd6678b03b39d30e01acd6047d5db9cfc9f", size = 2205057, upload-time = "2025-08-20T15:13:33.199Z" },
|
{ url = "https://files.pythonhosted.org/packages/aa/3e/162f70cdaa9f68864f77db4c29aaa70754800181b06b629dc66461c7135e/squareup-43.0.2.20250820-py3-none-any.whl", hash = "sha256:26de4e23c9bb735d4c5bb89a72ff1bd6678b03b39d30e01acd6047d5db9cfc9f", size = 2205057, upload-time = "2025-08-20T15:13:33.199Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "starlette"
|
||||||
|
version = "0.48.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "anyio" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a7/a5/d6f429d43394057b67a6b5bbe6eae2f77a6bf7459d961fdb224bf206eee6/starlette-0.48.0.tar.gz", hash = "sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46", size = 2652949, upload-time = "2025-09-13T08:41:05.699Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/72/2db2f49247d0a18b4f1bb9a5a39a0162869acf235f3a96418363947b3d46/starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659", size = 73736, upload-time = "2025-09-13T08:41:03.869Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "studpider"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { virtual = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "aiohttp" },
|
||||||
|
{ name = "aiosqlite" },
|
||||||
|
{ name = "apscheduler" },
|
||||||
|
{ name = "asyncio-throttle" },
|
||||||
|
{ name = "bs4" },
|
||||||
|
{ name = "dotenv" },
|
||||||
|
{ name = "fastapi" },
|
||||||
|
{ name = "gunicorn" },
|
||||||
|
{ name = "pyinstaller" },
|
||||||
|
{ name = "pytest" },
|
||||||
|
{ name = "reflex" },
|
||||||
|
{ name = "requests" },
|
||||||
|
{ name = "squareup" },
|
||||||
|
{ name = "uvicorn" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "aiohttp", specifier = ">=3.12.13,<4.0.0" },
|
||||||
|
{ name = "aiosqlite", specifier = ">=0.21.0,<0.22.0" },
|
||||||
|
{ name = "apscheduler", specifier = ">=3.11.0,<4.0.0" },
|
||||||
|
{ name = "asyncio-throttle", specifier = ">=1.0.2,<2.0.0" },
|
||||||
|
{ name = "bs4", specifier = ">=0.0.2" },
|
||||||
|
{ name = "dotenv", specifier = ">=0.9.9,<0.10.0" },
|
||||||
|
{ name = "fastapi", specifier = ">=0.128.0" },
|
||||||
|
{ name = "gunicorn", specifier = ">=23.0.0" },
|
||||||
|
{ name = "pyinstaller", specifier = ">=6.14.1,<7.0.0" },
|
||||||
|
{ name = "pytest", specifier = ">=8.4.2,<9.0.0" },
|
||||||
|
{ name = "reflex", specifier = ">=0.8.14.post1" },
|
||||||
|
{ name = "requests", specifier = ">=2.32.4,<3.0.0" },
|
||||||
|
{ name = "squareup", specifier = ">=43.0.0.20250618,<44.0.0.0" },
|
||||||
|
{ name = "uvicorn", specifier = ">=0.40.0" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "4.15.0"
|
version = "4.15.0"
|
||||||
@@ -693,6 +1066,84 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
|
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uvicorn"
|
||||||
|
version = "0.40.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "click" },
|
||||||
|
{ name = "h11" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "watchfiles"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "anyio" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wrapt"
|
||||||
|
version = "1.17.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wsproto"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "h11" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c9/4a/44d3c295350d776427904d73c189e10aeae66d7f555bb2feee16d1e4ba5a/wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065", size = 53425, upload-time = "2022-08-23T19:58:21.447Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/58/e860788190eba3bcce367f74d29c4675466ce8dddfba85f7827588416f01/wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736", size = 24226, upload-time = "2022-08-23T19:58:19.96Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yarl"
|
name = "yarl"
|
||||||
version = "1.20.1"
|
version = "1.20.1"
|
||||||
|
|||||||
Reference in new issue
Block a user