42 Commits
Author SHA1 Message Date
C 10537855f6 add initial web hook handling 2025-10-26 15:33:02 -07:00
C 3a5f477914 ignore things i forgot 2025-10-26 15:32:43 -07:00
C de9852e374 add arguments for host and port!!! yay!! 2025-10-23 14:42:41 -07:00
C 5440da0bdf BIG rename 2025-10-16 17:03:34 -07:00
C 40d9999a80 wtf is going on with these imports man 2025-10-14 15:53:08 -07:00
C 5887771a38 rename sql -> sqlite 2025-10-14 15:52:59 -07:00
C 55e04f0a7c all the reflex web junk lole 2025-10-14 15:52:42 -07:00
C 1df6c42b41 add reflex for web stuff! 2025-10-14 15:52:22 -07:00
C 1ba6f903d9 global throttle things 2025-10-12 11:52:04 -07:00
C 8faf56f9ad time stuff 2025-10-12 11:51:49 -07:00
ph0sgene d1059905c6 Update README.md 2025-10-11 21:43:11 +00:00
ph0sgene 6e488ca352 Update README.md 2025-10-11 21:42:35 +00:00
ph0sgene dd356b0d13 Update README.md 2025-10-11 21:42:02 +00:00
ph0sgene 1a77623e4d Update README.md 2025-10-11 21:40:10 +00:00
C 8d7564185f fancy new readme 2025-10-11 14:26:30 -07:00
C e1da46d61b spooder 2025-10-11 14:17:51 -07:00
C 86d6e5a61d mk test in test because test 2025-10-11 13:57:02 -07:00
C 5d580d68f6 modulize it >:( stupid wsl 2025-10-11 13:56:53 -07:00
C 2308d183e9 misc fixes 2025-10-11 13:56:34 -07:00
C 67891e5222 fix up db fetch and insert 2025-10-11 13:56:13 -07:00
C 04cfd42b4b remove previous stuff from asquare and put it in glue 2025-10-11 13:56:01 -07:00
C d612003f32 make middleware module (glue) 2025-10-11 13:55:39 -07:00
C 8ded841d44 global throttler implementation 2025-10-11 13:55:15 -07:00
C 76fa9a8db1 ignore venv 2025-10-11 13:54:10 -07:00
C 005bf87df5 add initial review testing for sql 2025-10-11 11:39:29 -07:00
C aa8883c822 rate limit 2025-10-11 11:39:12 -07:00
C 53994f99dc move throttler example. we dont need it in src 2025-10-11 11:38:54 -07:00
C 0f8b9b444b this is the schedule manager not the db 2025-10-11 11:33:14 -07:00
C 90ca36f426 big sexy sqlite db 2025-10-11 11:33:00 -07:00
C 5691cebc11 add rate limit handling to synch version :( 2025-10-10 11:05:39 -07:00
C f1c9655748 bs4 for testing 2025-10-10 11:05:21 -07:00
C 4fe20615d3 i guess this should be versioned 2025-10-10 11:05:14 -07:00
C d32c17a5da add stuff todo for fixing database 2025-10-09 17:15:12 -07:00
C 729746b0bd uv refresh 2025-10-09 17:14:59 -07:00
C 152bfb316b add verbosity to startup 2025-10-09 17:14:53 -07:00
C 369ccdbfc0 testing subclassed asquare 2025-10-09 17:14:37 -07:00
C a419dd498f handle no numbers in set name 2025-10-09 17:13:56 -07:00
C b4b0ccdac6 added rotating logs! epicsauce 2025-10-09 13:33:59 -07:00
C 32664c3a10 added optional logging to brickset 2025-10-09 13:33:51 -07:00
C 41ce94c85c Auto stash before rebase of "uv" 2025-10-09 12:55:38 -07:00
ph0sgene 397eddf299 rm 314t 2025-10-09 12:54:32 -07:00
C 8affb90308 refactor: Migrate build system to Hatch and integrate UV 2025-10-09 12:49:37 -07:00
18 changed files with 35 additions and 2111 deletions

No files matched your search

-83
View File
@@ -1,83 +0,0 @@
# 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
-5
View File
@@ -35,9 +35,4 @@ assets/external/
__pycache__/ __pycache__/
win/ win/
.github/ .github/
bin/
activate/ activate/
build/
.pem
.p7b
nginx/
-136
View File
@@ -1,136 +0,0 @@
# 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
-166
View File
@@ -1,166 +0,0 @@
# 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
-356
View File
@@ -1,356 +0,0 @@
# 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!
-191
View File
@@ -1,191 +0,0 @@
# 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
```
-3
View File
@@ -18,9 +18,6 @@ dependencies = [
"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", "reflex>=0.8.14.post1",
"fastapi>=0.128.0",
"uvicorn>=0.40.0",
"gunicorn>=23.0.0",
] ]
[build-system] [build-system]
-9
View File
@@ -6,15 +6,6 @@ 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")
-110
View File
@@ -1,110 +0,0 @@
"""
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)
-166
View File
@@ -1,166 +0,0 @@
"""
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!")
File renamed without changes.
-76
View File
@@ -1,76 +0,0 @@
"""
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)
+35 -242
View File
@@ -1,7 +1,6 @@
import argparse 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
@@ -15,6 +14,9 @@ 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...")
os.makedirs("./logs/", exist_ok=True)
import banner import banner
import get_sq_env import get_sq_env
from time import time from time import time
@@ -22,17 +24,12 @@ from time import time
time = int(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
@@ -87,153 +84,44 @@ 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: async def hook_handler(request: web.Request) -> web.Response:
"""Handle incoming JSON webhook POSTs on /hook endpoint""" """Handle incoming JSON webhook POSTs on /hook endpoint"""
try: try:
current_time = int(asyncio.get_event_loop().time())
# Verify request method # Verify request method
if request.method != "POST": if request.method != "POST":
logger.error( logger.error(
f"{current_time}\tInvalid request method to /hook from %s", f"{time}\tInvalid request method to /hook from %s", request.remote
request.remote,
) )
return web.Response(status=405, text="Method not allowed. Use POST.") return web.Response(status=405, text="Method not allowed. Use POST.")
# Read raw body for signature verification # Get JSON data from request
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: try:
data = json.loads(body_str) data = await request.json()
except json.JSONDecodeError as e: except json.JSONDecodeError:
print(f"[@DEBUG] Invalid JSON to /hook from {request.remote}: {str(e)}") logger.error("Invalid JSON to /hook from %s", request.remote)
logger.error(f"Invalid JSON to /hook from {request.remote}: {str(e)}")
return web.Response(status=400, text="Invalid JSON in request body") return web.Response(status=400, text="Invalid JSON in request body")
# Check for idempotency (event_id) # Log the received webhook
event_id = data.get("event_id") print(f"[@DEBUG] Webhook received at {time} from {request.remote}")
if event_id: logger.info(
if event_id in processed_event_ids: f"{time}\tWebhook received from {request.remote}: {json.dumps(data)}"
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 # Process the webhook data here
processed_event_ids.add(event_id) # TODO: Add your webhook processing logic
# 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 = { response_data = {
"status": "success", "status": "success",
"message": "Webhook received", "message": "Webhook received",
"event_id": event_id_val, "timestamp": time,
"data_received": data,
} }
# 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) 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: except Exception as e:
error_msg = f"Webhook handler error: {str(e)}" logger.error(f"Webhook handler error: {str(e)}")
print(f"[@DEBUG] {error_msg}") print(f"Webhook handler error: {str(e)}")
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") return web.Response(status=500, text="Internal server error")
@@ -280,7 +168,7 @@ async def request_handler(request: web.Request) -> web.Response:
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}")
@@ -300,119 +188,32 @@ app = web.Application()
app.router.add_post("/api", request_handler) app.router.add_post("/api", request_handler)
app.router.add_post("/hook", hook_handler) app.router.add_post("/hook", hook_handler)
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(description="studspider")
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( parser.add_argument(
"--host", "--host",
type=str, type=str,
default="localhost", default="localhost",
help="IP address to listen on (default: localhost, use 0.0.0.0 for all interfaces)", help="IP address to listen on. default: localhost",
) )
parser.add_argument( parser.add_argument(
"--port", type=int, default=7432, help="Port for main API endpoint (default: 7432)" "--port", type=int, default=7432, help="Port to listen on. default: 7432"
) )
parser.add_argument( parser.add_argument(
"--hook-port", "--hook-port",
type=int, type=int,
default=None, default=8080,
help="Separate port for webhook endpoint (default: None - uses same port as --port)", help="Optional separate port for webhook endpoint. default: 8080",
)
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() args = parser.parse_args()
if __name__ == "__main__": if __name__ == "__main__":
print("Creating log directory if it doesn't exist...")
os.makedirs("./logs/", exist_ok=True)
print(banner.generate("studspider v0.1")) print(banner.generate("studspider v0.1"))
print("Built for PLAY-WELL TEKNOLOGIES by contact@studspider.sh\n\n") print("Built for PLAY-WELL TEKNOLOGIES by contact@traps.moe\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 hook-port is specified, run a separate server for webhooks
if args.hook_port: if args.hook_port:
protocol_main = "https" if (ssl_context and not args.hook_ssl_only) else "http" print(f"Starting main server on {args.host}:{args.port}")
protocol_hook = "https" if ssl_context else "http" print(f"Starting webhook server on {args.host}:{args.hook_port}")
# Create separate app for webhook # Create separate app for webhook
hook_app = web.Application() hook_app = web.Application()
@@ -422,31 +223,23 @@ if __name__ == "__main__":
async def run_servers(): async def run_servers():
runner1 = web.AppRunner(app) runner1 = web.AppRunner(app)
await runner1.setup() await runner1.setup()
# Main server uses SSL only if not hook-ssl-only mode site1 = web.TCPSite(runner1, args.host, args.port)
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) runner2 = web.AppRunner(hook_app)
await runner2.setup() await runner2.setup()
# Webhook server always uses SSL if certificates provided site2 = web.TCPSite(runner2, args.host, args.hook_port)
site2 = web.TCPSite(
runner2, args.host, args.hook_port, ssl_context=ssl_context
)
await site1.start() await site1.start()
await site2.start() await site2.start()
print(f"Starting main server on {args.host}:{args.port} ({protocol_main})") print(f"Main server running on http://{args.host}:{args.port}")
print( print(f"Webhook server running on http://{args.host}:{args.hook_port}/hook")
f"Starting webhook server on {args.host}:{args.hook_port} ({protocol_hook})"
)
# Keep running # Keep running
await asyncio.Event().wait() await asyncio.Event().wait()
asyncio.run(run_servers()) asyncio.run(run_servers())
else: else:
protocol = "https" if ssl_context else "http" print(f"Server running on {args.host}:{args.port}")
print(f"Server running on {args.host}:{args.port} ({protocol})") print(f"Webhook endpoint: http://{args.host}:{args.port}/hook")
print(f"Webhook endpoint: {protocol}://{args.host}:{args.port}/hook") web.run_app(app, host=args.host, port=args.port)
web.run_app(app, host=args.host, port=args.port, ssl_context=ssl_context)
-102
View File
@@ -1,102 +0,0 @@
"""
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))
-80
View File
@@ -1,80 +0,0 @@
"""
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")
-278
View File
@@ -1,278 +0,0 @@
"""
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")
-51
View File
@@ -1,51 +0,0 @@
"""
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)
Generated
-57
View File
@@ -92,15 +92,6 @@ 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"
@@ -248,21 +239,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b2/b7/545d2c10c1fc15e48653c91efde329a790f2eecfbbf2bd16003b5db2bab0/dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9", size = 1892, upload-time = "2025-02-19T22:15:01.647Z" }, { url = "https://files.pythonhosted.org/packages/b2/b7/545d2c10c1fc15e48653c91efde329a790f2eecfbbf2bd16003b5db2bab0/dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9", size = 1892, upload-time = "2025-02-19T22:15:01.647Z" },
] ]
[[package]]
name = "fastapi"
version = "0.128.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-doc" },
{ name = "pydantic" },
{ name = "starlette" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/52/08/8c8508db6c7b9aae8f7175046af41baad690771c9bcde676419965e338c7/fastapi-0.128.0.tar.gz", hash = "sha256:1cc179e1cef10a6be60ffe429f79b829dce99d8de32d7acb7e6c8dfdf7f2645a", size = 365682, upload-time = "2025-12-27T15:21:13.714Z" }
wheels = [
{ 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" },
]
[[package]] [[package]]
name = "frozenlist" name = "frozenlist"
version = "1.7.0" version = "1.7.0"
@@ -354,23 +330,9 @@ wheels = [
{ 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/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/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/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" }, { 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"
@@ -987,14 +949,11 @@ dependencies = [
{ name = "asyncio-throttle" }, { name = "asyncio-throttle" },
{ name = "bs4" }, { name = "bs4" },
{ name = "dotenv" }, { name = "dotenv" },
{ name = "fastapi" },
{ name = "gunicorn" },
{ name = "pyinstaller" }, { name = "pyinstaller" },
{ name = "pytest" }, { name = "pytest" },
{ name = "reflex" }, { name = "reflex" },
{ name = "requests" }, { name = "requests" },
{ name = "squareup" }, { name = "squareup" },
{ name = "uvicorn" },
] ]
[package.metadata] [package.metadata]
@@ -1005,14 +964,11 @@ requires-dist = [
{ name = "asyncio-throttle", specifier = ">=1.0.2,<2.0.0" }, { name = "asyncio-throttle", specifier = ">=1.0.2,<2.0.0" },
{ name = "bs4", specifier = ">=0.0.2" }, { name = "bs4", specifier = ">=0.0.2" },
{ name = "dotenv", specifier = ">=0.9.9,<0.10.0" }, { 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 = "pyinstaller", specifier = ">=6.14.1,<7.0.0" },
{ name = "pytest", specifier = ">=8.4.2,<9.0.0" }, { name = "pytest", specifier = ">=8.4.2,<9.0.0" },
{ name = "reflex", specifier = ">=0.8.14.post1" }, { name = "reflex", specifier = ">=0.8.14.post1" },
{ name = "requests", specifier = ">=2.32.4,<3.0.0" }, { name = "requests", specifier = ">=2.32.4,<3.0.0" },
{ name = "squareup", specifier = ">=43.0.0.20250618,<44.0.0.0" }, { name = "squareup", specifier = ">=43.0.0.20250618,<44.0.0.0" },
{ name = "uvicorn", specifier = ">=0.40.0" },
] ]
[[package]] [[package]]
@@ -1066,19 +1022,6 @@ 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]] [[package]]
name = "watchfiles" name = "watchfiles"
version = "1.1.1" version = "1.1.1"