kk
Browse files- APSCHEDULER_SETUP.md +165 -0
- GEMINI.md +5 -3
- MIGRATION_TO_APSCHEDULER.md +84 -0
- backend/api/schedules.py +24 -23
- backend/app.py +8 -3
- backend/celery_app.py +0 -36
- backend/celery_beat_config.py +0 -24
- backend/celery_config.py +0 -75
- backend/celery_tasks/__init__.py +0 -6
- backend/celery_tasks/content_tasks.py +0 -193
- backend/celery_tasks/schedule_loader.py +0 -209
- backend/celery_tasks/scheduler.py +0 -105
- backend/requirements.txt +0 -4
- backend/scheduler/__init__.py +1 -0
- backend/scheduler/apscheduler_service.py +345 -0
- backend/scheduler/task_scheduler.py +0 -270
- backend/scheduler/task_scheduler.py.bak +0 -252
- backend/start_celery.bat +0 -44
- backend/start_celery.py +0 -129
- backend/start_celery.sh +0 -58
- start_app.py +4 -80
APSCHEDULER_SETUP.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# APScheduler Scheduling Setup Guide
|
| 2 |
+
|
| 3 |
+
This guide explains how to set up and use the APScheduler scheduling system with your Lin application.
|
| 4 |
+
|
| 5 |
+
## Overview
|
| 6 |
+
|
| 7 |
+
The updated `start_app.py` now automatically starts the Flask application with APScheduler integrated. This ensures that your scheduled tasks will execute properly without requiring external dependencies like Redis.
|
| 8 |
+
|
| 9 |
+
## Prerequisites
|
| 10 |
+
|
| 11 |
+
### 1. Python Dependencies
|
| 12 |
+
Install the required packages:
|
| 13 |
+
```bash
|
| 14 |
+
pip install -r backend/requirements.txt
|
| 15 |
+
```
|
| 16 |
+
|
| 17 |
+
## Starting the Application
|
| 18 |
+
|
| 19 |
+
### Using start_app.py (Recommended)
|
| 20 |
+
```bash
|
| 21 |
+
python start_app.py
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
This will:
|
| 25 |
+
1. Start the Flask application with APScheduler integrated
|
| 26 |
+
2. APScheduler will automatically load schedules from the database every 5 minutes
|
| 27 |
+
3. Schedules will be executed according to their defined times
|
| 28 |
+
|
| 29 |
+
## Configuration
|
| 30 |
+
|
| 31 |
+
### Environment Variables
|
| 32 |
+
Make sure these are set in your `.env` file:
|
| 33 |
+
|
| 34 |
+
```env
|
| 35 |
+
# Supabase configuration
|
| 36 |
+
SUPABASE_URL="your_supabase_url"
|
| 37 |
+
SUPABASE_KEY="your_supabase_key"
|
| 38 |
+
|
| 39 |
+
# Scheduler configuration
|
| 40 |
+
SCHEDULER_ENABLED=True
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
### APScheduler Configuration
|
| 44 |
+
The scheduler configuration is in `backend/scheduler/apscheduler_service.py`:
|
| 45 |
+
|
| 46 |
+
```python
|
| 47 |
+
# APScheduler will run every 5 minutes as a backup
|
| 48 |
+
scheduler.add_job(
|
| 49 |
+
func=self.load_schedules,
|
| 50 |
+
trigger=CronTrigger(minute='*/5'), # Every 5 minutes
|
| 51 |
+
id='load_schedules',
|
| 52 |
+
name='Load schedules from database',
|
| 53 |
+
replace_existing=True
|
| 54 |
+
)
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
## How Scheduling Works
|
| 58 |
+
|
| 59 |
+
### 1. Schedule Loading
|
| 60 |
+
- **Immediate Updates**: When you create or delete a schedule via the API, APScheduler is updated immediately
|
| 61 |
+
- **Periodic Updates**: APScheduler also runs every 5 minutes as a backup
|
| 62 |
+
- Fetches schedules from Supabase database
|
| 63 |
+
- Creates individual periodic tasks for each schedule
|
| 64 |
+
|
| 65 |
+
### 2. Task Execution
|
| 66 |
+
- **Content Generation**: Runs 5 minutes before scheduled time
|
| 67 |
+
- **Post Publishing**: Runs at the scheduled time
|
| 68 |
+
|
| 69 |
+
### 3. Database Integration
|
| 70 |
+
- Uses Supabase for schedule storage
|
| 71 |
+
- Automatically creates tasks based on schedule data
|
| 72 |
+
- Handles social network authentication
|
| 73 |
+
|
| 74 |
+
## Monitoring and Debugging
|
| 75 |
+
|
| 76 |
+
### Checking Scheduler Status
|
| 77 |
+
The scheduler runs in the same process as the Flask application, so you can check the console output for logs.
|
| 78 |
+
|
| 79 |
+
### Viewing Logs
|
| 80 |
+
- **Flask Application**: Check console output
|
| 81 |
+
- **Scheduler**: Look for scheduler process logs in the same console
|
| 82 |
+
|
| 83 |
+
### Common Issues
|
| 84 |
+
|
| 85 |
+
**1. Tasks Not Executing**
|
| 86 |
+
- Check if the Flask application is running
|
| 87 |
+
- Verify schedule data in the database
|
| 88 |
+
- Check the console logs for any errors
|
| 89 |
+
|
| 90 |
+
**2. Schedule Not Loading**
|
| 91 |
+
- Check Supabase database connection
|
| 92 |
+
- Verify schedule data in database
|
| 93 |
+
- Check task registration in APScheduler
|
| 94 |
+
|
| 95 |
+
## Testing the Scheduling System
|
| 96 |
+
|
| 97 |
+
### Manual Testing
|
| 98 |
+
```python
|
| 99 |
+
# Test schedule loading
|
| 100 |
+
from backend.scheduler.apscheduler_service import APSchedulerService
|
| 101 |
+
scheduler = APSchedulerService()
|
| 102 |
+
result = scheduler.load_schedules()
|
| 103 |
+
print(result)
|
| 104 |
+
```
|
| 105 |
+
|
| 106 |
+
### API Testing (Recommended)
|
| 107 |
+
1. **Create a schedule via the API**:
|
| 108 |
+
```bash
|
| 109 |
+
curl -X POST http://localhost:5000/api/schedules/ \
|
| 110 |
+
-H "Content-Type: application/json" \
|
| 111 |
+
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
| 112 |
+
-d '{
|
| 113 |
+
"social_network": "1",
|
| 114 |
+
"schedule_time": "09:00",
|
| 115 |
+
"days": ["Monday", "Wednesday", "Friday"]
|
| 116 |
+
}'
|
| 117 |
+
```
|
| 118 |
+
|
| 119 |
+
2. **Check the response**: You should see a message indicating the scheduler was updated immediately
|
| 120 |
+
|
| 121 |
+
3. **Verify in Logs**: Check if the individual tasks were created in the console logs
|
| 122 |
+
|
| 123 |
+
### Database Testing
|
| 124 |
+
1. Add a schedule directly in the Supabase database
|
| 125 |
+
2. Wait 5 minutes for the loader task to run (or trigger via API)
|
| 126 |
+
3. Check if individual tasks were created
|
| 127 |
+
4. Verify task execution times
|
| 128 |
+
|
| 129 |
+
## Production Deployment
|
| 130 |
+
|
| 131 |
+
### Using Docker (Recommended for Hugging Face Spaces)
|
| 132 |
+
```bash
|
| 133 |
+
# Build the Docker image
|
| 134 |
+
docker build -t lin-app .
|
| 135 |
+
|
| 136 |
+
# Run the container
|
| 137 |
+
docker run -p 7860:7860 lin-app
|
| 138 |
+
|
| 139 |
+
# For Hugging Face Spaces deployment:
|
| 140 |
+
# 1. Update your Dockerfile (already done above)
|
| 141 |
+
# 2. Push to Hugging Face Spaces
|
| 142 |
+
# 3. The container will automatically start your app with APScheduler
|
| 143 |
+
```
|
| 144 |
+
|
| 145 |
+
### Using Docker Compose (Not Required)
|
| 146 |
+
Since APScheduler doesn't require external dependencies like Redis, you don't need Docker Compose for the scheduler.
|
| 147 |
+
|
| 148 |
+
## Troubleshooting Checklist
|
| 149 |
+
|
| 150 |
+
1. ✅ All Python dependencies are installed
|
| 151 |
+
2. ✅ Environment variables are set correctly
|
| 152 |
+
3. ✅ Supabase database connection works
|
| 153 |
+
4. ✅ Schedule data exists in database
|
| 154 |
+
5. ✅ Flask application is running
|
| 155 |
+
6. ✅ Scheduler is properly initialized
|
| 156 |
+
|
| 157 |
+
## Support
|
| 158 |
+
|
| 159 |
+
If you encounter issues:
|
| 160 |
+
1. Check this guide first
|
| 161 |
+
2. Review the logs for error messages
|
| 162 |
+
3. Verify all prerequisites are met
|
| 163 |
+
4. Test components individually
|
| 164 |
+
|
| 165 |
+
For additional help, refer to the APScheduler documentation at: https://apscheduler.readthedocs.io/
|
GEMINI.md
CHANGED
|
@@ -96,6 +96,7 @@ pip install -r requirements.txt
|
|
| 96 |
- Uses Supabase for authentication and database
|
| 97 |
- Implements JWT for token-based authentication
|
| 98 |
- Uses SQLAlchemy for database operations
|
|
|
|
| 99 |
- Follows REST API design principles
|
| 100 |
|
| 101 |
### UI Components
|
|
@@ -111,8 +112,9 @@ The application features several key UI components:
|
|
| 111 |
1. **Authentication**: Login and registration functionality with JWT tokens
|
| 112 |
2. **LinkedIn Integration**: OAuth integration for connecting LinkedIn accounts
|
| 113 |
3. **Content Management**: Create, edit, and schedule posts
|
| 114 |
-
4. **
|
| 115 |
-
5. **
|
|
|
|
| 116 |
|
| 117 |
## Environment Setup
|
| 118 |
|
|
@@ -151,7 +153,7 @@ cp .env.example .env
|
|
| 151 |
- `JWT_SECRET_KEY` - Secret key for JWT token generation
|
| 152 |
- `SECRET_KEY` - Flask secret key
|
| 153 |
- `DEBUG` - Debug mode (True/False)
|
| 154 |
-
- `SCHEDULER_ENABLED` - Enable/disable
|
| 155 |
- `PORT` - Port to run the application on (default: 5000)
|
| 156 |
|
| 157 |
## Development URLs
|
|
|
|
| 96 |
- Uses Supabase for authentication and database
|
| 97 |
- Implements JWT for token-based authentication
|
| 98 |
- Uses SQLAlchemy for database operations
|
| 99 |
+
- Uses APScheduler for task scheduling
|
| 100 |
- Follows REST API design principles
|
| 101 |
|
| 102 |
### UI Components
|
|
|
|
| 112 |
1. **Authentication**: Login and registration functionality with JWT tokens
|
| 113 |
2. **LinkedIn Integration**: OAuth integration for connecting LinkedIn accounts
|
| 114 |
3. **Content Management**: Create, edit, and schedule posts
|
| 115 |
+
4. **Automated Scheduling**: Uses APScheduler for reliable task scheduling
|
| 116 |
+
5. **Analytics**: Dashboard with overview and analytics
|
| 117 |
+
6. **Responsive UI**: Mobile-friendly design with optimized touch interactions
|
| 118 |
|
| 119 |
## Environment Setup
|
| 120 |
|
|
|
|
| 153 |
- `JWT_SECRET_KEY` - Secret key for JWT token generation
|
| 154 |
- `SECRET_KEY` - Flask secret key
|
| 155 |
- `DEBUG` - Debug mode (True/False)
|
| 156 |
+
- `SCHEDULER_ENABLED` - Enable/disable APScheduler (True/False)
|
| 157 |
- `PORT` - Port to run the application on (default: 5000)
|
| 158 |
|
| 159 |
## Development URLs
|
MIGRATION_TO_APSCHEDULER.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Migrating from Celery to APScheduler
|
| 2 |
+
|
| 3 |
+
This guide explains how to migrate from Celery to APScheduler in the Lin application.
|
| 4 |
+
|
| 5 |
+
## Why Migrate to APScheduler?
|
| 6 |
+
|
| 7 |
+
1. **Simplified Architecture**: APScheduler runs within the Flask application process, eliminating the need for separate worker processes
|
| 8 |
+
2. **No External Dependencies**: Unlike Celery which requires Redis or RabbitMQ, APScheduler uses in-memory storage by default
|
| 9 |
+
3. **Easier Deployment**: Simplifies deployment since there are fewer components to manage
|
| 10 |
+
4. **Reduced Resource Usage**: Uses less memory and CPU compared to running separate Celery processes
|
| 11 |
+
|
| 12 |
+
## Changes Made
|
| 13 |
+
|
| 14 |
+
### 1. Removed Dependencies
|
| 15 |
+
- Removed `celery` and `redis` from `requirements.txt`
|
| 16 |
+
- Kept `apscheduler` as the scheduling library
|
| 17 |
+
|
| 18 |
+
### 2. New Scheduler Service
|
| 19 |
+
- Created `backend/scheduler/apscheduler_service.py` to handle all scheduling tasks
|
| 20 |
+
- Implemented content generation and post publishing as APScheduler jobs
|
| 21 |
+
|
| 22 |
+
### 3. Updated Flask Application
|
| 23 |
+
- Modified `backend/app.py` to initialize APScheduler instead of Celery
|
| 24 |
+
- Added scheduler initialization when `SCHEDULER_ENABLED` is True
|
| 25 |
+
|
| 26 |
+
### 4. Updated API Endpoints
|
| 27 |
+
- Modified `backend/api/schedules.py` to trigger APScheduler updates instead of Celery tasks
|
| 28 |
+
- Removed all references to Celery task IDs in responses
|
| 29 |
+
|
| 30 |
+
### 5. Simplified Startup Script
|
| 31 |
+
- Updated `start_app.py` to remove Celery component initialization
|
| 32 |
+
- The application now starts with just Flask and APScheduler
|
| 33 |
+
|
| 34 |
+
### 6. Removed Files
|
| 35 |
+
- Removed `backend/celery_app.py`
|
| 36 |
+
- Removed `backend/celery_config.py`
|
| 37 |
+
- Removed `backend/celery_tasks/` directory
|
| 38 |
+
- Removed `backend/start_celery.py`
|
| 39 |
+
|
| 40 |
+
## Migration Steps
|
| 41 |
+
|
| 42 |
+
### 1. Update Dependencies
|
| 43 |
+
```bash
|
| 44 |
+
# Update requirements
|
| 45 |
+
pip install -r backend/requirements.txt
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
### 2. Update Environment Variables
|
| 49 |
+
No changes needed for basic setup. The scheduler is enabled by default.
|
| 50 |
+
|
| 51 |
+
### 3. Remove Old Components
|
| 52 |
+
The old Celery components have been removed from the codebase.
|
| 53 |
+
|
| 54 |
+
### 4. Verify Functionality
|
| 55 |
+
1. Start the application:
|
| 56 |
+
```bash
|
| 57 |
+
python start_app.py
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
2. Create a test schedule via the API
|
| 61 |
+
|
| 62 |
+
3. Check the console logs to verify that schedules are being loaded and tasks are being created
|
| 63 |
+
|
| 64 |
+
## Benefits of the Migration
|
| 65 |
+
|
| 66 |
+
1. **Simpler Setup**: No need to install and configure Redis
|
| 67 |
+
2. **Easier Debugging**: All logs are in one place
|
| 68 |
+
3. **Reduced Complexity**: Fewer moving parts to manage
|
| 69 |
+
4. **Better Resource Usage**: Lower memory and CPU footprint
|
| 70 |
+
5. **Simplified Deployment**: Single process deployment
|
| 71 |
+
|
| 72 |
+
## Potential Considerations
|
| 73 |
+
|
| 74 |
+
1. **Scalability**: For high-volume applications, Celery with multiple workers might be more appropriate
|
| 75 |
+
2. **Persistence**: APScheduler uses in-memory storage by default, which means schedules are lost on application restart (this is mitigated by reloading from the database every 5 minutes)
|
| 76 |
+
3. **Task Isolation**: All tasks run in the same process, so a long-running task could block others
|
| 77 |
+
|
| 78 |
+
## Support
|
| 79 |
+
|
| 80 |
+
If you encounter issues during migration:
|
| 81 |
+
1. Check the application logs for error messages
|
| 82 |
+
2. Verify that all dependencies are correctly installed
|
| 83 |
+
3. Ensure the Supabase connection is working
|
| 84 |
+
4. Test creating and deleting schedules via the API
|
backend/api/schedules.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
| 1 |
from flask import Blueprint, request, jsonify, current_app
|
| 2 |
from flask_jwt_extended import jwt_required, get_jwt_identity
|
| 3 |
from backend.services.schedule_service import ScheduleService
|
| 4 |
-
from backend.celery_tasks.schedule_loader import load_schedules_task
|
| 5 |
|
| 6 |
schedules_bp = Blueprint('schedules', __name__)
|
| 7 |
|
|
@@ -132,19 +131,20 @@ def create_schedule():
|
|
| 132 |
result = schedule_service.create_schedule(user_id, social_network, schedule_time, days)
|
| 133 |
|
| 134 |
if result['success']:
|
| 135 |
-
# Trigger immediate
|
| 136 |
try:
|
| 137 |
-
print("[INFO] Triggering immediate
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
|
|
|
| 145 |
except Exception as e:
|
| 146 |
-
print(f"[WARNING] Failed to trigger immediate
|
| 147 |
-
# Don't fail the schedule creation if
|
| 148 |
result['message'] += ' (Note: Scheduler update will occur in 5 minutes)'
|
| 149 |
|
| 150 |
# Add CORS headers to success response
|
|
@@ -225,19 +225,20 @@ def delete_schedule(schedule_id):
|
|
| 225 |
result = schedule_service.delete_schedule(schedule_id)
|
| 226 |
|
| 227 |
if result['success']:
|
| 228 |
-
# Trigger immediate
|
| 229 |
try:
|
| 230 |
-
print("[INFO] Triggering immediate
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
|
|
|
| 238 |
except Exception as e:
|
| 239 |
-
print(f"[WARNING] Failed to trigger immediate
|
| 240 |
-
# Don't fail the schedule deletion if
|
| 241 |
result['message'] += ' (Note: Scheduler update will occur in 5 minutes)'
|
| 242 |
|
| 243 |
# Add CORS headers to success response
|
|
|
|
| 1 |
from flask import Blueprint, request, jsonify, current_app
|
| 2 |
from flask_jwt_extended import jwt_required, get_jwt_identity
|
| 3 |
from backend.services.schedule_service import ScheduleService
|
|
|
|
| 4 |
|
| 5 |
schedules_bp = Blueprint('schedules', __name__)
|
| 6 |
|
|
|
|
| 131 |
result = schedule_service.create_schedule(user_id, social_network, schedule_time, days)
|
| 132 |
|
| 133 |
if result['success']:
|
| 134 |
+
# Trigger immediate APScheduler update
|
| 135 |
try:
|
| 136 |
+
print("[INFO] Triggering immediate APScheduler update...")
|
| 137 |
+
if hasattr(current_app, 'scheduler'):
|
| 138 |
+
scheduler_updated = current_app.scheduler.trigger_immediate_update()
|
| 139 |
+
if scheduler_updated:
|
| 140 |
+
result['message'] += ' (Scheduler updated immediately)'
|
| 141 |
+
else:
|
| 142 |
+
result['message'] += ' (Note: Scheduler update will occur in 5 minutes)'
|
| 143 |
+
else:
|
| 144 |
+
result['message'] += ' (Note: Scheduler update will occur in 5 minutes)'
|
| 145 |
except Exception as e:
|
| 146 |
+
print(f"[WARNING] Failed to trigger immediate scheduler update: {str(e)}")
|
| 147 |
+
# Don't fail the schedule creation if scheduler update fails
|
| 148 |
result['message'] += ' (Note: Scheduler update will occur in 5 minutes)'
|
| 149 |
|
| 150 |
# Add CORS headers to success response
|
|
|
|
| 225 |
result = schedule_service.delete_schedule(schedule_id)
|
| 226 |
|
| 227 |
if result['success']:
|
| 228 |
+
# Trigger immediate APScheduler update
|
| 229 |
try:
|
| 230 |
+
print("[INFO] Triggering immediate APScheduler update after deletion...")
|
| 231 |
+
if hasattr(current_app, 'scheduler'):
|
| 232 |
+
scheduler_updated = current_app.scheduler.trigger_immediate_update()
|
| 233 |
+
if scheduler_updated:
|
| 234 |
+
result['message'] += ' (Scheduler updated immediately)'
|
| 235 |
+
else:
|
| 236 |
+
result['message'] += ' (Note: Scheduler update will occur in 5 minutes)'
|
| 237 |
+
else:
|
| 238 |
+
result['message'] += ' (Note: Scheduler update will occur in 5 minutes)'
|
| 239 |
except Exception as e:
|
| 240 |
+
print(f"[WARNING] Failed to trigger immediate scheduler update: {str(e)}")
|
| 241 |
+
# Don't fail the schedule deletion if scheduler update fails
|
| 242 |
result['message'] += ' (Note: Scheduler update will occur in 5 minutes)'
|
| 243 |
|
| 244 |
# Add CORS headers to success response
|
backend/app.py
CHANGED
|
@@ -13,8 +13,8 @@ from backend.config import Config
|
|
| 13 |
from backend.utils.database import init_supabase
|
| 14 |
from backend.utils.cookies import setup_secure_cookies, configure_jwt_with_cookies
|
| 15 |
|
| 16 |
-
#
|
| 17 |
-
from backend.
|
| 18 |
|
| 19 |
def setup_unicode_environment():
|
| 20 |
"""Setup Unicode environment for proper character handling."""
|
|
@@ -115,9 +115,14 @@ def create_app():
|
|
| 115 |
app.job_store = {}
|
| 116 |
|
| 117 |
# Initialize a ThreadPoolExecutor for running background tasks
|
| 118 |
-
# In production, you'd use a proper task
|
| 119 |
app.executor = ThreadPoolExecutor(max_workers=4)
|
| 120 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
# Register blueprints
|
| 122 |
from backend.api.auth import auth_bp
|
| 123 |
from backend.api.sources import sources_bp
|
|
|
|
| 13 |
from backend.utils.database import init_supabase
|
| 14 |
from backend.utils.cookies import setup_secure_cookies, configure_jwt_with_cookies
|
| 15 |
|
| 16 |
+
# APScheduler imports
|
| 17 |
+
from backend.scheduler.apscheduler_service import APSchedulerService
|
| 18 |
|
| 19 |
def setup_unicode_environment():
|
| 20 |
"""Setup Unicode environment for proper character handling."""
|
|
|
|
| 115 |
app.job_store = {}
|
| 116 |
|
| 117 |
# Initialize a ThreadPoolExecutor for running background tasks
|
| 118 |
+
# In production, you'd use a proper task scheduler like APScheduler
|
| 119 |
app.executor = ThreadPoolExecutor(max_workers=4)
|
| 120 |
|
| 121 |
+
# Initialize APScheduler
|
| 122 |
+
if app.config.get('SCHEDULER_ENABLED', True):
|
| 123 |
+
scheduler = APSchedulerService(app)
|
| 124 |
+
app.scheduler = scheduler
|
| 125 |
+
|
| 126 |
# Register blueprints
|
| 127 |
from backend.api.auth import auth_bp
|
| 128 |
from backend.api.sources import sources_bp
|
backend/celery_app.py
DELETED
|
@@ -1,36 +0,0 @@
|
|
| 1 |
-
import os
|
| 2 |
-
from celery import Celery
|
| 3 |
-
# Use relative import for the Config class to work with Hugging Face Spaces
|
| 4 |
-
from backend.config import Config
|
| 5 |
-
|
| 6 |
-
def make_celery(app_name=__name__):
|
| 7 |
-
"""Create and configure the Celery application."""
|
| 8 |
-
# Create Celery instance
|
| 9 |
-
celery = Celery(app_name)
|
| 10 |
-
|
| 11 |
-
# Configure Celery with broker and result backend from environment variables
|
| 12 |
-
celery.conf.broker_url = os.environ.get('CELERY_BROKER_URL', 'redis://localhost:6379/0')
|
| 13 |
-
celery.conf.result_backend = os.environ.get('CELERY_RESULT_BACKEND', 'redis://localhost:6379/0')
|
| 14 |
-
|
| 15 |
-
# Additional Celery configuration
|
| 16 |
-
celery.conf.update(
|
| 17 |
-
task_serializer='json',
|
| 18 |
-
accept_content=['json'],
|
| 19 |
-
result_serializer='json',
|
| 20 |
-
timezone='UTC',
|
| 21 |
-
enable_utc=True,
|
| 22 |
-
task_routes={
|
| 23 |
-
'celery_tasks.content_tasks.generate_content': {'queue': 'content'},
|
| 24 |
-
'celery_tasks.publish_tasks.publish_post': {'queue': 'publish'},
|
| 25 |
-
},
|
| 26 |
-
worker_prefetch_multiplier=1,
|
| 27 |
-
task_acks_late=True,
|
| 28 |
-
)
|
| 29 |
-
|
| 30 |
-
return celery
|
| 31 |
-
|
| 32 |
-
# Create the Celery instance
|
| 33 |
-
celery = make_celery()
|
| 34 |
-
|
| 35 |
-
if __name__ == '__main__':
|
| 36 |
-
celery.start()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/celery_beat_config.py
DELETED
|
@@ -1,24 +0,0 @@
|
|
| 1 |
-
from celery import Celery
|
| 2 |
-
from celery.schedules import crontab
|
| 3 |
-
import os
|
| 4 |
-
|
| 5 |
-
# Import the task function
|
| 6 |
-
from backend.celery_tasks.schedule_loader import load_schedules_task
|
| 7 |
-
|
| 8 |
-
# Create Celery instance for Beat scheduler
|
| 9 |
-
celery_beat = Celery('lin_scheduler')
|
| 10 |
-
|
| 11 |
-
# Configure Celery Beat
|
| 12 |
-
celery_beat.conf.broker_url = os.environ.get('CELERY_BROKER_URL', 'redis://localhost:6379/0')
|
| 13 |
-
celery_beat.conf.result_backend = os.environ.get('CELERY_RESULT_BACKEND', 'redis://localhost:6379/0')
|
| 14 |
-
|
| 15 |
-
# Configure schedules
|
| 16 |
-
celery_beat.conf.beat_schedule = {
|
| 17 |
-
# This task will run every 5 minutes to load schedules from the database
|
| 18 |
-
'load-schedules': {
|
| 19 |
-
'task': 'backend.celery_tasks.schedule_loader.load_schedules_task',
|
| 20 |
-
'schedule': crontab(minute='*/5'),
|
| 21 |
-
},
|
| 22 |
-
}
|
| 23 |
-
|
| 24 |
-
celery_beat.conf.timezone = 'UTC'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/celery_config.py
DELETED
|
@@ -1,75 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Unified Celery configuration for the Lin application.
|
| 3 |
-
This centralizes all Celery configuration to avoid conflicts.
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
import os
|
| 7 |
-
from celery import Celery
|
| 8 |
-
from celery.schedules import crontab
|
| 9 |
-
from backend.config import Config
|
| 10 |
-
|
| 11 |
-
# Create Celery instance
|
| 12 |
-
celery_app = Celery('lin_app')
|
| 13 |
-
|
| 14 |
-
# Configure Celery with broker and result backend
|
| 15 |
-
celery_app.conf.broker_url = os.environ.get('CELERY_BROKER_URL', 'redis://localhost:6379/0')
|
| 16 |
-
celery_app.conf.result_backend = os.environ.get('CELERY_RESULT_BACKEND', 'redis://localhost:6379/0')
|
| 17 |
-
|
| 18 |
-
# Additional Celery configuration
|
| 19 |
-
celery_app.conf.update(
|
| 20 |
-
# Task serialization
|
| 21 |
-
task_serializer='json',
|
| 22 |
-
accept_content=['json'],
|
| 23 |
-
result_serializer='json',
|
| 24 |
-
timezone='UTC',
|
| 25 |
-
enable_utc=True,
|
| 26 |
-
|
| 27 |
-
# Task routing
|
| 28 |
-
task_routes={
|
| 29 |
-
'backend.celery_tasks.content_tasks.generate_content_task': {'queue': 'content'},
|
| 30 |
-
'backend.celery_tasks.content_tasks.publish_post_task': {'queue': 'publish'},
|
| 31 |
-
'backend.celery_tasks.schedule_loader.load_schedules_task': {'queue': 'scheduler'},
|
| 32 |
-
},
|
| 33 |
-
|
| 34 |
-
# Worker configuration
|
| 35 |
-
worker_prefetch_multiplier=1,
|
| 36 |
-
task_acks_late=True,
|
| 37 |
-
worker_max_tasks_per_child=100,
|
| 38 |
-
|
| 39 |
-
# Beat schedule configuration (scheduler itself will be default)
|
| 40 |
-
# beat_scheduler is not set, so it defaults to 'celery.beat:PersistentScheduler'
|
| 41 |
-
beat_schedule={
|
| 42 |
-
# This task will run every 5 minutes to load schedules from the database
|
| 43 |
-
'load-schedules': {
|
| 44 |
-
'task': 'backend.celery_tasks.schedule_loader.load_schedules_task',
|
| 45 |
-
'schedule': crontab(minute='*/5'),
|
| 46 |
-
},
|
| 47 |
-
},
|
| 48 |
-
|
| 49 |
-
# Task result expiration
|
| 50 |
-
result_expires=3600, # 1 hour
|
| 51 |
-
|
| 52 |
-
# Task time limits
|
| 53 |
-
task_soft_time_limit=300, # 5 minutes
|
| 54 |
-
task_time_limit=600, # 10 minutes
|
| 55 |
-
|
| 56 |
-
# Rate limiting
|
| 57 |
-
task_annotations=(
|
| 58 |
-
('backend.celery_tasks.content_tasks.generate_content_task', {'rate_limit': '10/h'}),
|
| 59 |
-
('backend.celery_tasks.content_tasks.publish_post_task', {'rate_limit': '30/h'}),
|
| 60 |
-
),
|
| 61 |
-
|
| 62 |
-
# Error handling
|
| 63 |
-
task_reject_on_worker_lost=True,
|
| 64 |
-
worker_disable_rate_limits=False,
|
| 65 |
-
|
| 66 |
-
# Security
|
| 67 |
-
result_backend_transport_options={'visibility': 'hidden'},
|
| 68 |
-
broker_connection_max_retries=3,
|
| 69 |
-
broker_connection_retry_delay=5,
|
| 70 |
-
)
|
| 71 |
-
|
| 72 |
-
# Import tasks to ensure they're registered
|
| 73 |
-
from backend import celery_tasks
|
| 74 |
-
|
| 75 |
-
__all__ = ['celery_app']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/celery_tasks/__init__.py
DELETED
|
@@ -1,6 +0,0 @@
|
|
| 1 |
-
# Initialize the celery_tasks package
|
| 2 |
-
from . import content_tasks
|
| 3 |
-
from . import schedule_loader
|
| 4 |
-
|
| 5 |
-
# Import all tasks to ensure they are registered with Celery
|
| 6 |
-
__all__ = ['content_tasks', 'schedule_loader']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/celery_tasks/content_tasks.py
DELETED
|
@@ -1,193 +0,0 @@
|
|
| 1 |
-
from celery import current_task
|
| 2 |
-
from backend.services.content_service import ContentService
|
| 3 |
-
from backend.services.linkedin_service import LinkedInService
|
| 4 |
-
from backend.utils.database import init_supabase
|
| 5 |
-
from backend.celery_config import celery_app
|
| 6 |
-
|
| 7 |
-
# Configure logging
|
| 8 |
-
import logging
|
| 9 |
-
logger = logging.getLogger(__name__)
|
| 10 |
-
|
| 11 |
-
@celery_app.task(bind=True)
|
| 12 |
-
def generate_content_task(self, user_id: str, schedule_id: str, supabase_client_config: dict):
|
| 13 |
-
"""
|
| 14 |
-
Celery task to generate content for a scheduled post.
|
| 15 |
-
|
| 16 |
-
Args:
|
| 17 |
-
user_id (str): User ID
|
| 18 |
-
schedule_id (str): Schedule ID
|
| 19 |
-
supabase_client_config (dict): Supabase client configuration
|
| 20 |
-
|
| 21 |
-
Returns:
|
| 22 |
-
dict: Result of content generation
|
| 23 |
-
"""
|
| 24 |
-
try:
|
| 25 |
-
print(f"[CONTENT TASK] Starting content generation for schedule {schedule_id}")
|
| 26 |
-
logger.info(f"Starting content generation for schedule {schedule_id}")
|
| 27 |
-
|
| 28 |
-
# Update task state
|
| 29 |
-
self.update_state(state='PROGRESS', meta={'status': 'Generating content...'})
|
| 30 |
-
|
| 31 |
-
# Initialize content service
|
| 32 |
-
content_service = ContentService()
|
| 33 |
-
|
| 34 |
-
# Generate content using content service
|
| 35 |
-
generated_content = content_service.generate_post_content(user_id)
|
| 36 |
-
|
| 37 |
-
# Initialize Supabase client from config
|
| 38 |
-
from backend.utils.database import init_supabase
|
| 39 |
-
supabase_client = init_supabase(
|
| 40 |
-
supabase_client_config['SUPABASE_URL'],
|
| 41 |
-
supabase_client_config['SUPABASE_KEY']
|
| 42 |
-
)
|
| 43 |
-
|
| 44 |
-
# Store generated content in database
|
| 45 |
-
# We need to get the social account ID from the schedule
|
| 46 |
-
schedule_response = (
|
| 47 |
-
supabase_client
|
| 48 |
-
.table("Scheduling")
|
| 49 |
-
.select("id_social")
|
| 50 |
-
.eq("id", schedule_id)
|
| 51 |
-
.execute()
|
| 52 |
-
)
|
| 53 |
-
|
| 54 |
-
if not schedule_response.data:
|
| 55 |
-
raise Exception(f"Schedule {schedule_id} not found")
|
| 56 |
-
|
| 57 |
-
social_account_id = schedule_response.data[0]['id_social']
|
| 58 |
-
|
| 59 |
-
# Store the generated content
|
| 60 |
-
response = (
|
| 61 |
-
supabase_client
|
| 62 |
-
.table("Post_content")
|
| 63 |
-
.insert({
|
| 64 |
-
"social_account_id": social_account_id,
|
| 65 |
-
"Text_content": generated_content,
|
| 66 |
-
"is_published": False,
|
| 67 |
-
"sched": schedule_id
|
| 68 |
-
})
|
| 69 |
-
.execute()
|
| 70 |
-
)
|
| 71 |
-
|
| 72 |
-
if response.data:
|
| 73 |
-
logger.info(f"Content generated and stored for schedule {schedule_id}")
|
| 74 |
-
return {
|
| 75 |
-
'status': 'success',
|
| 76 |
-
'message': f'Content generated for schedule {schedule_id}',
|
| 77 |
-
'post_id': response.data[0]['id']
|
| 78 |
-
}
|
| 79 |
-
else:
|
| 80 |
-
logger.error(f"Failed to store generated content for schedule {schedule_id}")
|
| 81 |
-
return {
|
| 82 |
-
'status': 'failure',
|
| 83 |
-
'message': f'Failed to store generated content for schedule {schedule_id}'
|
| 84 |
-
}
|
| 85 |
-
|
| 86 |
-
except Exception as e:
|
| 87 |
-
logger.error(f"Error in content generation task for schedule {schedule_id}: {str(e)}")
|
| 88 |
-
return {
|
| 89 |
-
'status': 'failure',
|
| 90 |
-
'message': f'Error in content generation: {str(e)}'
|
| 91 |
-
}
|
| 92 |
-
|
| 93 |
-
@celery_app.task(bind=True)
|
| 94 |
-
def publish_post_task(self, schedule_id: str, supabase_client_config: dict):
|
| 95 |
-
"""
|
| 96 |
-
Celery task to publish a scheduled post.
|
| 97 |
-
|
| 98 |
-
Args:
|
| 99 |
-
schedule_id (str): Schedule ID
|
| 100 |
-
supabase_client_config (dict): Supabase client configuration
|
| 101 |
-
|
| 102 |
-
Returns:
|
| 103 |
-
dict: Result of post publishing
|
| 104 |
-
"""
|
| 105 |
-
try:
|
| 106 |
-
print(f"[PUBLISH TASK] Starting post publishing for schedule {schedule_id}")
|
| 107 |
-
logger.info(f"Starting post publishing for schedule {schedule_id}")
|
| 108 |
-
|
| 109 |
-
# Update task state
|
| 110 |
-
self.update_state(state='PROGRESS', meta={'status': 'Publishing post...'})
|
| 111 |
-
|
| 112 |
-
# Initialize Supabase client from config
|
| 113 |
-
from backend.utils.database import init_supabase
|
| 114 |
-
supabase_client = init_supabase(
|
| 115 |
-
supabase_client_config['SUPABASE_URL'],
|
| 116 |
-
supabase_client_config['SUPABASE_KEY']
|
| 117 |
-
)
|
| 118 |
-
|
| 119 |
-
# Fetch the post to publish
|
| 120 |
-
response = (
|
| 121 |
-
supabase_client
|
| 122 |
-
.table("Post_content")
|
| 123 |
-
.select("*")
|
| 124 |
-
.eq("sched", schedule_id)
|
| 125 |
-
.eq("is_published", False)
|
| 126 |
-
.order("created_at", desc=True)
|
| 127 |
-
.limit(1)
|
| 128 |
-
.execute()
|
| 129 |
-
)
|
| 130 |
-
|
| 131 |
-
if not response.data:
|
| 132 |
-
logger.info(f"No unpublished posts found for schedule {schedule_id}")
|
| 133 |
-
return {
|
| 134 |
-
'status': 'info',
|
| 135 |
-
'message': f'No unpublished posts found for schedule {schedule_id}'
|
| 136 |
-
}
|
| 137 |
-
|
| 138 |
-
post = response.data[0]
|
| 139 |
-
post_id = post.get('id')
|
| 140 |
-
text_content = post.get('Text_content')
|
| 141 |
-
image_url = post.get('image_content_url')
|
| 142 |
-
|
| 143 |
-
# Get social network credentials
|
| 144 |
-
schedule_response = (
|
| 145 |
-
supabase_client
|
| 146 |
-
.table("Scheduling")
|
| 147 |
-
.select("Social_network(token, sub)")
|
| 148 |
-
.eq("id", schedule_id)
|
| 149 |
-
.execute()
|
| 150 |
-
)
|
| 151 |
-
|
| 152 |
-
if not schedule_response.data:
|
| 153 |
-
raise Exception(f"Schedule {schedule_id} not found")
|
| 154 |
-
|
| 155 |
-
social_network = schedule_response.data[0].get('Social_network', {})
|
| 156 |
-
access_token = social_network.get('token')
|
| 157 |
-
user_sub = social_network.get('sub')
|
| 158 |
-
|
| 159 |
-
if not access_token or not user_sub:
|
| 160 |
-
logger.error(f"Missing social network credentials for schedule {schedule_id}")
|
| 161 |
-
return {
|
| 162 |
-
'status': 'failure',
|
| 163 |
-
'message': f'Missing social network credentials for schedule {schedule_id}'
|
| 164 |
-
}
|
| 165 |
-
|
| 166 |
-
# Publish to LinkedIn
|
| 167 |
-
linkedin_service = LinkedInService()
|
| 168 |
-
publish_response = linkedin_service.publish_post(
|
| 169 |
-
access_token, user_sub, text_content, image_url
|
| 170 |
-
)
|
| 171 |
-
|
| 172 |
-
# Update post status in database
|
| 173 |
-
update_response = (
|
| 174 |
-
supabase_client
|
| 175 |
-
.table("Post_content")
|
| 176 |
-
.update({"is_published": True})
|
| 177 |
-
.eq("id", post_id)
|
| 178 |
-
.execute()
|
| 179 |
-
)
|
| 180 |
-
|
| 181 |
-
logger.info(f"Post published successfully for schedule {schedule_id}")
|
| 182 |
-
return {
|
| 183 |
-
'status': 'success',
|
| 184 |
-
'message': f'Post published successfully for schedule {schedule_id}',
|
| 185 |
-
'linkedin_response': publish_response
|
| 186 |
-
}
|
| 187 |
-
|
| 188 |
-
except Exception as e:
|
| 189 |
-
logger.error(f"Error in publishing task for schedule {schedule_id}: {str(e)}")
|
| 190 |
-
return {
|
| 191 |
-
'status': 'failure',
|
| 192 |
-
'message': f'Error in publishing post: {str(e)}'
|
| 193 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/celery_tasks/schedule_loader.py
DELETED
|
@@ -1,209 +0,0 @@
|
|
| 1 |
-
from celery import current_app
|
| 2 |
-
from celery.schedules import crontab
|
| 3 |
-
from datetime import datetime
|
| 4 |
-
import logging
|
| 5 |
-
from backend.utils.database import init_supabase
|
| 6 |
-
from backend.config import Config
|
| 7 |
-
from backend.celery_tasks.scheduler import schedule_content_generation, schedule_post_publishing
|
| 8 |
-
from backend.celery_config import celery_app
|
| 9 |
-
|
| 10 |
-
# Configure logging
|
| 11 |
-
logger = logging.getLogger(__name__)
|
| 12 |
-
|
| 13 |
-
def get_supabase_config():
|
| 14 |
-
"""Get Supabase configuration from environment."""
|
| 15 |
-
return {
|
| 16 |
-
'SUPABASE_URL': Config.SUPABASE_URL,
|
| 17 |
-
'SUPABASE_KEY': Config.SUPABASE_KEY
|
| 18 |
-
}
|
| 19 |
-
|
| 20 |
-
def parse_schedule_time(schedule_time):
|
| 21 |
-
"""
|
| 22 |
-
Parse schedule time string into crontab format.
|
| 23 |
-
|
| 24 |
-
Args:
|
| 25 |
-
schedule_time (str): Schedule time in format "Day HH:MM"
|
| 26 |
-
|
| 27 |
-
Returns:
|
| 28 |
-
dict: Crontab parameters
|
| 29 |
-
"""
|
| 30 |
-
try:
|
| 31 |
-
print(f"[CELERY BEAT] Parsing schedule time: {schedule_time}")
|
| 32 |
-
day_name, time_str = schedule_time.split()
|
| 33 |
-
hour, minute = map(int, time_str.split(':'))
|
| 34 |
-
|
| 35 |
-
# Map day names to crontab format
|
| 36 |
-
day_map = {
|
| 37 |
-
'Monday': 1,
|
| 38 |
-
'Tuesday': 2,
|
| 39 |
-
'Wednesday': 3,
|
| 40 |
-
'Thursday': 4,
|
| 41 |
-
'Friday': 5,
|
| 42 |
-
'Saturday': 6,
|
| 43 |
-
'Sunday': 0
|
| 44 |
-
}
|
| 45 |
-
|
| 46 |
-
day_of_week = day_map.get(day_name, '*')
|
| 47 |
-
result = {
|
| 48 |
-
'minute': minute,
|
| 49 |
-
'hour': hour,
|
| 50 |
-
'day_of_week': day_of_week
|
| 51 |
-
}
|
| 52 |
-
print(f"[CELERY BEAT] Parsed schedule time result: {result}")
|
| 53 |
-
return result
|
| 54 |
-
except Exception as e:
|
| 55 |
-
logger.error(f"Error parsing schedule time {schedule_time}: {str(e)}")
|
| 56 |
-
# Default to every minute for error cases
|
| 57 |
-
return {
|
| 58 |
-
'minute': '*',
|
| 59 |
-
'hour': '*',
|
| 60 |
-
'day_of_week': '*'
|
| 61 |
-
}
|
| 62 |
-
|
| 63 |
-
@celery_app.task(bind=True)
|
| 64 |
-
def load_schedules_task(self):
|
| 65 |
-
"""
|
| 66 |
-
Celery task to load schedules from the database and create periodic tasks.
|
| 67 |
-
This task runs every 5 minutes to check for new or updated schedules.
|
| 68 |
-
"""
|
| 69 |
-
try:
|
| 70 |
-
print(f"[CELERY BEAT] Loading schedules from database at {datetime.now()}...")
|
| 71 |
-
logger.info("Loading schedules from database...")
|
| 72 |
-
|
| 73 |
-
# Get Supabase configuration
|
| 74 |
-
supabase_config = get_supabase_config()
|
| 75 |
-
print(f"[CELERY BEAT] Supabase config: URL={supabase_config['SUPABASE_URL'][:50]}...")
|
| 76 |
-
|
| 77 |
-
# Initialize Supabase client
|
| 78 |
-
supabase_client = init_supabase(
|
| 79 |
-
supabase_config['SUPABASE_URL'],
|
| 80 |
-
supabase_config['SUPABASE_KEY']
|
| 81 |
-
)
|
| 82 |
-
|
| 83 |
-
# Fetch all schedules from Supabase
|
| 84 |
-
print("[CELERY BEAT] Executing database query...")
|
| 85 |
-
response = (
|
| 86 |
-
supabase_client
|
| 87 |
-
.table("Scheduling")
|
| 88 |
-
.select("*, Social_network(id_utilisateur, token, sub)")
|
| 89 |
-
.execute()
|
| 90 |
-
)
|
| 91 |
-
|
| 92 |
-
print(f"[CELERY BEAT] Database query response: {type(response)}")
|
| 93 |
-
print(f"[CELERY BEAT] Response data: {response.data if response.data else 'None'}")
|
| 94 |
-
|
| 95 |
-
schedules = response.data if response.data else []
|
| 96 |
-
print(f"[CELERY BEAT] Found {len(schedules)} schedules in database")
|
| 97 |
-
logger.info(f"Found {len(schedules)} schedules")
|
| 98 |
-
|
| 99 |
-
# Log details of each schedule for debugging
|
| 100 |
-
for i, schedule in enumerate(schedules):
|
| 101 |
-
print(f"[CELERY BEAT] Schedule {i}: {schedule}")
|
| 102 |
-
schedule_id = schedule.get('id')
|
| 103 |
-
schedule_time = schedule.get('schedule_time')
|
| 104 |
-
adjusted_time = schedule.get('adjusted_time')
|
| 105 |
-
social_network = schedule.get('Social_network', {})
|
| 106 |
-
print(f"[CELERY BEAT] Schedule {schedule_id} - schedule_time: {schedule_time}, adjusted_time: {adjusted_time}, social_network: {social_network}")
|
| 107 |
-
|
| 108 |
-
# Get current beat schedule
|
| 109 |
-
current_schedule = celery_app.conf.beat_schedule
|
| 110 |
-
|
| 111 |
-
# Remove existing scheduled jobs (except the loader job)
|
| 112 |
-
# In a production environment, you might want to be more selective about this
|
| 113 |
-
loader_job = current_schedule.get('load-schedules', {})
|
| 114 |
-
new_schedule = {'load-schedules': loader_job}
|
| 115 |
-
|
| 116 |
-
# Create jobs for each schedule
|
| 117 |
-
for schedule in schedules:
|
| 118 |
-
try:
|
| 119 |
-
schedule_id = schedule.get('id')
|
| 120 |
-
schedule_time = schedule.get('schedule_time')
|
| 121 |
-
adjusted_time = schedule.get('adjusted_time')
|
| 122 |
-
|
| 123 |
-
print(f"[CELERY BEAT] Processing schedule {schedule_id}: schedule_time={schedule_time}, adjusted_time={adjusted_time}")
|
| 124 |
-
|
| 125 |
-
if not schedule_time or not adjusted_time:
|
| 126 |
-
logger.warning(f"Invalid schedule format for schedule {schedule_id}")
|
| 127 |
-
print(f"[CELERY BEAT] WARNING: Invalid schedule format for schedule {schedule_id}")
|
| 128 |
-
continue
|
| 129 |
-
|
| 130 |
-
# Parse schedule times
|
| 131 |
-
content_gen_time = parse_schedule_time(adjusted_time)
|
| 132 |
-
publish_time = parse_schedule_time(schedule_time)
|
| 133 |
-
|
| 134 |
-
print(f"[CELERY BEAT] Parsed times - Content gen: {content_gen_time}, Publish: {publish_time}")
|
| 135 |
-
|
| 136 |
-
# Create content generation job (5 minutes before publishing)
|
| 137 |
-
gen_job_id = f"gen_{schedule_id}"
|
| 138 |
-
task_schedule = crontab(
|
| 139 |
-
minute=content_gen_time['minute'],
|
| 140 |
-
hour=content_gen_time['hour'],
|
| 141 |
-
day_of_week=content_gen_time['day_of_week']
|
| 142 |
-
)
|
| 143 |
-
print(f"[CELERY BEAT] Creating content task - ID: {gen_job_id}")
|
| 144 |
-
print(f"[CELERY BEAT] Content task schedule: minute={content_gen_time['minute']}, hour={content_gen_time['hour']}, day_of_week={content_gen_time['day_of_week']}")
|
| 145 |
-
args = (
|
| 146 |
-
schedule.get('Social_network', {}).get('id_utilisateur'),
|
| 147 |
-
schedule_id,
|
| 148 |
-
supabase_config
|
| 149 |
-
)
|
| 150 |
-
print(f"[CELERY BEAT] Content task args: {args}")
|
| 151 |
-
new_schedule[gen_job_id] = {
|
| 152 |
-
'task': 'backend.celery_tasks.content_tasks.generate_content_task',
|
| 153 |
-
'schedule': task_schedule,
|
| 154 |
-
'args': (
|
| 155 |
-
schedule.get('Social_network', {}).get('id_utilisateur'),
|
| 156 |
-
schedule_id,
|
| 157 |
-
supabase_config
|
| 158 |
-
)
|
| 159 |
-
}
|
| 160 |
-
logger.info(f"Created content generation job: {gen_job_id}")
|
| 161 |
-
print(f"[CELERY BEAT] Created content generation job: {gen_job_id}")
|
| 162 |
-
|
| 163 |
-
# Create publishing job
|
| 164 |
-
pub_job_id = f"pub_{schedule_id}"
|
| 165 |
-
task_schedule = crontab(
|
| 166 |
-
minute=publish_time['minute'],
|
| 167 |
-
hour=publish_time['hour'],
|
| 168 |
-
day_of_week=publish_time['day_of_week']
|
| 169 |
-
)
|
| 170 |
-
print(f"[CELERY BEAT] Creating publish task - ID: {pub_job_id}")
|
| 171 |
-
print(f"[CELERY BEAT] Publish task schedule: minute={publish_time['minute']}, hour={publish_time['hour']}, day_of_week={publish_time['day_of_week']}")
|
| 172 |
-
args = (
|
| 173 |
-
schedule_id,
|
| 174 |
-
supabase_config
|
| 175 |
-
)
|
| 176 |
-
print(f"[CELERY BEAT] Publish task args: {args}")
|
| 177 |
-
new_schedule[pub_job_id] = {
|
| 178 |
-
'task': 'backend.celery_tasks.content_tasks.publish_post_task',
|
| 179 |
-
'schedule': task_schedule,
|
| 180 |
-
'args': (
|
| 181 |
-
schedule_id,
|
| 182 |
-
supabase_config
|
| 183 |
-
)
|
| 184 |
-
}
|
| 185 |
-
logger.info(f"Created publishing job: {pub_job_id}")
|
| 186 |
-
print(f"[CELERY BEAT] Created publishing job: {pub_job_id}")
|
| 187 |
-
|
| 188 |
-
except Exception as e:
|
| 189 |
-
logger.error(f"Error creating jobs for schedule {schedule.get('id')}: {str(e)}")
|
| 190 |
-
|
| 191 |
-
# Update the beat schedule
|
| 192 |
-
print(f"[CELERY BEAT] Current schedule keys before update: {list(current_app.conf.beat_schedule.keys())}")
|
| 193 |
-
print(f"[CELERY BEAT] New schedule keys: {list(new_schedule.keys())}")
|
| 194 |
-
current_app.conf.beat_schedule = new_schedule
|
| 195 |
-
print(f"[CELERY BEAT] Successfully updated Celery Beat schedule with {len(new_schedule)} jobs")
|
| 196 |
-
logger.info("Updated Celery Beat schedule")
|
| 197 |
-
|
| 198 |
-
return {
|
| 199 |
-
'status': 'success',
|
| 200 |
-
'message': f'Loaded {len(schedules)} schedules',
|
| 201 |
-
'schedules_count': len(schedules)
|
| 202 |
-
}
|
| 203 |
-
|
| 204 |
-
except Exception as e:
|
| 205 |
-
logger.error(f"Error loading schedules: {str(e)}")
|
| 206 |
-
return {
|
| 207 |
-
'status': 'error',
|
| 208 |
-
'message': f'Error loading schedules: {str(e)}'
|
| 209 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/celery_tasks/scheduler.py
DELETED
|
@@ -1,105 +0,0 @@
|
|
| 1 |
-
from datetime import datetime, timedelta
|
| 2 |
-
from celery import chain
|
| 3 |
-
import logging
|
| 4 |
-
from backend.celery_config import celery_app
|
| 5 |
-
from backend.celery_tasks.content_tasks import generate_content_task, publish_post_task
|
| 6 |
-
|
| 7 |
-
# Configure logging
|
| 8 |
-
logging.basicConfig(level=logging.INFO)
|
| 9 |
-
logger = logging.getLogger(__name__)
|
| 10 |
-
|
| 11 |
-
def init_celery_scheduler(supabase_client):
|
| 12 |
-
"""
|
| 13 |
-
Initialize the Celery-based task scheduler.
|
| 14 |
-
|
| 15 |
-
Args:
|
| 16 |
-
supabase_client: Supabase client instance
|
| 17 |
-
"""
|
| 18 |
-
logger.info("Initializing Celery scheduler")
|
| 19 |
-
# In a Celery-based approach, we don't need to initialize a scheduler here
|
| 20 |
-
# Tasks will be scheduled through Celery Beat or called directly
|
| 21 |
-
|
| 22 |
-
def schedule_content_generation(schedule: dict, supabase_client_config: dict):
|
| 23 |
-
"""
|
| 24 |
-
Schedule content generation task using Celery.
|
| 25 |
-
|
| 26 |
-
Args:
|
| 27 |
-
schedule (dict): Schedule data
|
| 28 |
-
supabase_client_config (dict): Supabase client configuration
|
| 29 |
-
|
| 30 |
-
Returns:
|
| 31 |
-
dict: Celery task result
|
| 32 |
-
"""
|
| 33 |
-
schedule_id = schedule.get('id')
|
| 34 |
-
user_id = schedule.get('Social_network', {}).get('id_utilisateur')
|
| 35 |
-
|
| 36 |
-
if not user_id:
|
| 37 |
-
logger.warning(f"No user ID found for schedule {schedule_id}")
|
| 38 |
-
return None
|
| 39 |
-
|
| 40 |
-
logger.info(f"Scheduling content generation for schedule {schedule_id}")
|
| 41 |
-
|
| 42 |
-
# Schedule the content generation task
|
| 43 |
-
task = generate_content_task.delay(user_id, schedule_id, supabase_client_config)
|
| 44 |
-
return {
|
| 45 |
-
'task_id': task.id,
|
| 46 |
-
'status': 'scheduled',
|
| 47 |
-
'message': f'Content generation scheduled for schedule {schedule_id}'
|
| 48 |
-
}
|
| 49 |
-
|
| 50 |
-
def schedule_post_publishing(schedule: dict, supabase_client_config: dict):
|
| 51 |
-
"""
|
| 52 |
-
Schedule post publishing task using Celery.
|
| 53 |
-
|
| 54 |
-
Args:
|
| 55 |
-
schedule (dict): Schedule data
|
| 56 |
-
supabase_client_config (dict): Supabase client configuration
|
| 57 |
-
|
| 58 |
-
Returns:
|
| 59 |
-
dict: Celery task result
|
| 60 |
-
"""
|
| 61 |
-
schedule_id = schedule.get('id')
|
| 62 |
-
logger.info(f"Scheduling post publishing for schedule {schedule_id}")
|
| 63 |
-
|
| 64 |
-
# Schedule the post publishing task
|
| 65 |
-
task = publish_post_task.delay(schedule_id, supabase_client_config)
|
| 66 |
-
return {
|
| 67 |
-
'task_id': task.id,
|
| 68 |
-
'status': 'scheduled',
|
| 69 |
-
'message': f'Post publishing scheduled for schedule {schedule_id}'
|
| 70 |
-
}
|
| 71 |
-
|
| 72 |
-
def schedule_content_and_publish(schedule: dict, supabase_client_config: dict):
|
| 73 |
-
"""
|
| 74 |
-
Schedule both content generation and post publishing as a chain.
|
| 75 |
-
|
| 76 |
-
Args:
|
| 77 |
-
schedule (dict): Schedule data
|
| 78 |
-
supabase_client_config (dict): Supabase client configuration
|
| 79 |
-
|
| 80 |
-
Returns:
|
| 81 |
-
dict: Celery task result
|
| 82 |
-
"""
|
| 83 |
-
schedule_id = schedule.get('id')
|
| 84 |
-
user_id = schedule.get('Social_network', {}).get('id_utilisateur')
|
| 85 |
-
|
| 86 |
-
if not user_id:
|
| 87 |
-
logger.warning(f"No user ID found for schedule {schedule_id}")
|
| 88 |
-
return None
|
| 89 |
-
|
| 90 |
-
logger.info(f"Scheduling content generation and publishing chain for schedule {schedule_id}")
|
| 91 |
-
|
| 92 |
-
# Create a chain of tasks: generate content first, then publish
|
| 93 |
-
task_chain = chain(
|
| 94 |
-
generate_content_task.s(user_id, schedule_id, supabase_client_config),
|
| 95 |
-
publish_post_task.s(supabase_client_config)
|
| 96 |
-
)
|
| 97 |
-
|
| 98 |
-
# Apply the chain asynchronously
|
| 99 |
-
result = task_chain.apply_async()
|
| 100 |
-
|
| 101 |
-
return {
|
| 102 |
-
'task_id': result.id,
|
| 103 |
-
'status': 'scheduled',
|
| 104 |
-
'message': f'Content generation and publishing chain scheduled for schedule {schedule_id}'
|
| 105 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/requirements.txt
CHANGED
|
@@ -27,10 +27,6 @@ supabase>=2.16.0
|
|
| 27 |
# Security
|
| 28 |
bcrypt>=4.3.0
|
| 29 |
|
| 30 |
-
# Task queue
|
| 31 |
-
celery>=5.5.3
|
| 32 |
-
redis>=6.4.0
|
| 33 |
-
|
| 34 |
# Testing
|
| 35 |
pytest>=8.4.1
|
| 36 |
pytest-cov>=6.2.1
|
|
|
|
| 27 |
# Security
|
| 28 |
bcrypt>=4.3.0
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
# Testing
|
| 31 |
pytest>=8.4.1
|
| 32 |
pytest-cov>=6.2.1
|
backend/scheduler/__init__.py
CHANGED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Scheduler package for the Lin application."""
|
backend/scheduler/apscheduler_service.py
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""APS Scheduler service for the Lin application."""
|
| 2 |
+
|
| 3 |
+
import logging
|
| 4 |
+
from datetime import datetime, timedelta
|
| 5 |
+
from apscheduler.schedulers.background import BackgroundScheduler
|
| 6 |
+
from apscheduler.triggers.cron import CronTrigger
|
| 7 |
+
from apscheduler.jobstores.memory import MemoryJobStore
|
| 8 |
+
from apscheduler.executors.pool import ThreadPoolExecutor
|
| 9 |
+
from backend.services.content_service import ContentService
|
| 10 |
+
from backend.services.linkedin_service import LinkedInService
|
| 11 |
+
from backend.utils.database import init_supabase
|
| 12 |
+
from backend.config import Config
|
| 13 |
+
|
| 14 |
+
# Configure logging
|
| 15 |
+
logger = logging.getLogger(__name__)
|
| 16 |
+
|
| 17 |
+
class APSchedulerService:
|
| 18 |
+
"""Service for managing APScheduler tasks."""
|
| 19 |
+
|
| 20 |
+
def __init__(self, app=None):
|
| 21 |
+
self.app = app
|
| 22 |
+
self.scheduler = None
|
| 23 |
+
self.supabase_client = None
|
| 24 |
+
|
| 25 |
+
# Initialize scheduler if app is provided
|
| 26 |
+
if app is not None:
|
| 27 |
+
self.init_app(app)
|
| 28 |
+
|
| 29 |
+
def init_app(self, app):
|
| 30 |
+
"""Initialize the scheduler with the Flask app."""
|
| 31 |
+
self.app = app
|
| 32 |
+
|
| 33 |
+
# Initialize Supabase client
|
| 34 |
+
self.supabase_client = init_supabase(
|
| 35 |
+
app.config['SUPABASE_URL'],
|
| 36 |
+
app.config['SUPABASE_KEY']
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
# Configure job stores and executors
|
| 40 |
+
jobstores = {
|
| 41 |
+
'default': MemoryJobStore()
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
executors = {
|
| 45 |
+
'default': ThreadPoolExecutor(20),
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
job_defaults = {
|
| 49 |
+
'coalesce': False,
|
| 50 |
+
'max_instances': 3
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
# Create scheduler
|
| 54 |
+
self.scheduler = BackgroundScheduler(
|
| 55 |
+
jobstores=jobstores,
|
| 56 |
+
executors=executors,
|
| 57 |
+
job_defaults=job_defaults,
|
| 58 |
+
timezone='UTC'
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
# Add the scheduler to the app
|
| 62 |
+
app.scheduler = self
|
| 63 |
+
|
| 64 |
+
# Start the scheduler
|
| 65 |
+
self.scheduler.start()
|
| 66 |
+
|
| 67 |
+
# Add the periodic job to load schedules from database
|
| 68 |
+
self.scheduler.add_job(
|
| 69 |
+
func=self.load_schedules,
|
| 70 |
+
trigger=CronTrigger(minute='*/5'), # Every 5 minutes
|
| 71 |
+
id='load_schedules',
|
| 72 |
+
name='Load schedules from database',
|
| 73 |
+
replace_existing=True
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
logger.info("APS Scheduler initialized and started")
|
| 77 |
+
|
| 78 |
+
def load_schedules(self):
|
| 79 |
+
"""Load schedules from the database and create jobs."""
|
| 80 |
+
try:
|
| 81 |
+
logger.info("Loading schedules from database...")
|
| 82 |
+
|
| 83 |
+
if not self.supabase_client:
|
| 84 |
+
logger.error("Supabase client not initialized")
|
| 85 |
+
return
|
| 86 |
+
|
| 87 |
+
# Fetch all schedules from Supabase
|
| 88 |
+
response = (
|
| 89 |
+
self.supabase_client
|
| 90 |
+
.table("Scheduling")
|
| 91 |
+
.select("*, Social_network(id_utilisateur, token, sub)")
|
| 92 |
+
.execute()
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
schedules = response.data if response.data else []
|
| 96 |
+
logger.info(f"Found {len(schedules)} schedules in database")
|
| 97 |
+
|
| 98 |
+
# Remove existing scheduled jobs (except the loader job)
|
| 99 |
+
jobs_to_remove = []
|
| 100 |
+
for job in self.scheduler.get_jobs():
|
| 101 |
+
if job.id != 'load_schedules':
|
| 102 |
+
jobs_to_remove.append(job.id)
|
| 103 |
+
|
| 104 |
+
for job_id in jobs_to_remove:
|
| 105 |
+
try:
|
| 106 |
+
self.scheduler.remove_job(job_id)
|
| 107 |
+
logger.info(f"Removed job: {job_id}")
|
| 108 |
+
except Exception as e:
|
| 109 |
+
logger.warning(f"Failed to remove job {job_id}: {str(e)}")
|
| 110 |
+
|
| 111 |
+
# Create jobs for each schedule
|
| 112 |
+
for schedule in schedules:
|
| 113 |
+
try:
|
| 114 |
+
schedule_id = schedule.get('id')
|
| 115 |
+
schedule_time = schedule.get('schedule_time')
|
| 116 |
+
adjusted_time = schedule.get('adjusted_time')
|
| 117 |
+
|
| 118 |
+
if not schedule_time or not adjusted_time:
|
| 119 |
+
logger.warning(f"Invalid schedule format for schedule {schedule_id}")
|
| 120 |
+
continue
|
| 121 |
+
|
| 122 |
+
# Parse schedule times
|
| 123 |
+
content_gen_cron = self._parse_schedule_time(adjusted_time)
|
| 124 |
+
publish_cron = self._parse_schedule_time(schedule_time)
|
| 125 |
+
|
| 126 |
+
# Create content generation job (5 minutes before publishing)
|
| 127 |
+
gen_job_id = f"gen_{schedule_id}"
|
| 128 |
+
self.scheduler.add_job(
|
| 129 |
+
func=self.generate_content_task,
|
| 130 |
+
trigger=CronTrigger(
|
| 131 |
+
minute=content_gen_cron['minute'],
|
| 132 |
+
hour=content_gen_cron['hour'],
|
| 133 |
+
day_of_week=content_gen_cron['day_of_week']
|
| 134 |
+
),
|
| 135 |
+
id=gen_job_id,
|
| 136 |
+
name=f"Content generation for schedule {schedule_id}",
|
| 137 |
+
args=[schedule.get('Social_network', {}).get('id_utilisateur'), schedule_id],
|
| 138 |
+
replace_existing=True
|
| 139 |
+
)
|
| 140 |
+
logger.info(f"Created content generation job: {gen_job_id}")
|
| 141 |
+
|
| 142 |
+
# Create publishing job
|
| 143 |
+
pub_job_id = f"pub_{schedule_id}"
|
| 144 |
+
self.scheduler.add_job(
|
| 145 |
+
func=self.publish_post_task,
|
| 146 |
+
trigger=CronTrigger(
|
| 147 |
+
minute=publish_cron['minute'],
|
| 148 |
+
hour=publish_cron['hour'],
|
| 149 |
+
day_of_week=publish_cron['day_of_week']
|
| 150 |
+
),
|
| 151 |
+
id=pub_job_id,
|
| 152 |
+
name=f"Post publishing for schedule {schedule_id}",
|
| 153 |
+
args=[schedule_id],
|
| 154 |
+
replace_existing=True
|
| 155 |
+
)
|
| 156 |
+
logger.info(f"Created publishing job: {pub_job_id}")
|
| 157 |
+
|
| 158 |
+
except Exception as e:
|
| 159 |
+
logger.error(f"Error creating jobs for schedule {schedule.get('id')}: {str(e)}")
|
| 160 |
+
|
| 161 |
+
logger.info("Updated APScheduler schedule")
|
| 162 |
+
|
| 163 |
+
except Exception as e:
|
| 164 |
+
logger.error(f"Error loading schedules: {str(e)}")
|
| 165 |
+
|
| 166 |
+
def _parse_schedule_time(self, schedule_time):
|
| 167 |
+
"""
|
| 168 |
+
Parse schedule time string into cron format.
|
| 169 |
+
|
| 170 |
+
Args:
|
| 171 |
+
schedule_time (str): Schedule time in format "Day HH:MM"
|
| 172 |
+
|
| 173 |
+
Returns:
|
| 174 |
+
dict: Cron parameters
|
| 175 |
+
"""
|
| 176 |
+
try:
|
| 177 |
+
day_name, time_str = schedule_time.split()
|
| 178 |
+
hour, minute = map(int, time_str.split(':'))
|
| 179 |
+
|
| 180 |
+
# Map day names to cron format
|
| 181 |
+
day_map = {
|
| 182 |
+
'Monday': 0,
|
| 183 |
+
'Tuesday': 1,
|
| 184 |
+
'Wednesday': 2,
|
| 185 |
+
'Thursday': 3,
|
| 186 |
+
'Friday': 4,
|
| 187 |
+
'Saturday': 5,
|
| 188 |
+
'Sunday': 6
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
day_of_week = day_map.get(day_name, '*')
|
| 192 |
+
return {
|
| 193 |
+
'minute': minute,
|
| 194 |
+
'hour': hour,
|
| 195 |
+
'day_of_week': day_of_week
|
| 196 |
+
}
|
| 197 |
+
except Exception as e:
|
| 198 |
+
logger.error(f"Error parsing schedule time {schedule_time}: {str(e)}")
|
| 199 |
+
# Default to every minute for error cases
|
| 200 |
+
return {
|
| 201 |
+
'minute': '*',
|
| 202 |
+
'hour': '*',
|
| 203 |
+
'day_of_week': '*'
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
def generate_content_task(self, user_id: str, schedule_id: str):
|
| 207 |
+
"""
|
| 208 |
+
APScheduler task to generate content for a scheduled post.
|
| 209 |
+
|
| 210 |
+
Args:
|
| 211 |
+
user_id (str): User ID
|
| 212 |
+
schedule_id (str): Schedule ID
|
| 213 |
+
"""
|
| 214 |
+
try:
|
| 215 |
+
logger.info(f"Starting content generation for schedule {schedule_id}")
|
| 216 |
+
|
| 217 |
+
# Initialize content service
|
| 218 |
+
content_service = ContentService()
|
| 219 |
+
|
| 220 |
+
# Generate content using content service
|
| 221 |
+
generated_content = content_service.generate_post_content(user_id)
|
| 222 |
+
|
| 223 |
+
# Store generated content in database
|
| 224 |
+
# We need to get the social account ID from the schedule
|
| 225 |
+
schedule_response = (
|
| 226 |
+
self.supabase_client
|
| 227 |
+
.table("Scheduling")
|
| 228 |
+
.select("id_social")
|
| 229 |
+
.eq("id", schedule_id)
|
| 230 |
+
.execute()
|
| 231 |
+
)
|
| 232 |
+
|
| 233 |
+
if not schedule_response.data:
|
| 234 |
+
raise Exception(f"Schedule {schedule_id} not found")
|
| 235 |
+
|
| 236 |
+
social_account_id = schedule_response.data[0]['id_social']
|
| 237 |
+
|
| 238 |
+
# Store the generated content
|
| 239 |
+
response = (
|
| 240 |
+
self.supabase_client
|
| 241 |
+
.table("Post_content")
|
| 242 |
+
.insert({
|
| 243 |
+
"social_account_id": social_account_id,
|
| 244 |
+
"Text_content": generated_content,
|
| 245 |
+
"is_published": False,
|
| 246 |
+
"sched": schedule_id
|
| 247 |
+
})
|
| 248 |
+
.execute()
|
| 249 |
+
)
|
| 250 |
+
|
| 251 |
+
if response.data:
|
| 252 |
+
logger.info(f"Content generated and stored for schedule {schedule_id}")
|
| 253 |
+
else:
|
| 254 |
+
logger.error(f"Failed to store generated content for schedule {schedule_id}")
|
| 255 |
+
|
| 256 |
+
except Exception as e:
|
| 257 |
+
logger.error(f"Error in content generation task for schedule {schedule_id}: {str(e)}")
|
| 258 |
+
|
| 259 |
+
def publish_post_task(self, schedule_id: str):
|
| 260 |
+
"""
|
| 261 |
+
APScheduler task to publish a scheduled post.
|
| 262 |
+
|
| 263 |
+
Args:
|
| 264 |
+
schedule_id (str): Schedule ID
|
| 265 |
+
"""
|
| 266 |
+
try:
|
| 267 |
+
logger.info(f"Starting post publishing for schedule {schedule_id}")
|
| 268 |
+
|
| 269 |
+
# Fetch the post to publish
|
| 270 |
+
response = (
|
| 271 |
+
self.supabase_client
|
| 272 |
+
.table("Post_content")
|
| 273 |
+
.select("*")
|
| 274 |
+
.eq("sched", schedule_id)
|
| 275 |
+
.eq("is_published", False)
|
| 276 |
+
.order("created_at", desc=True)
|
| 277 |
+
.limit(1)
|
| 278 |
+
.execute()
|
| 279 |
+
)
|
| 280 |
+
|
| 281 |
+
if not response.data:
|
| 282 |
+
logger.info(f"No unpublished posts found for schedule {schedule_id}")
|
| 283 |
+
return
|
| 284 |
+
|
| 285 |
+
post = response.data[0]
|
| 286 |
+
post_id = post.get('id')
|
| 287 |
+
text_content = post.get('Text_content')
|
| 288 |
+
image_url = post.get('image_content_url')
|
| 289 |
+
|
| 290 |
+
# Get social network credentials
|
| 291 |
+
schedule_response = (
|
| 292 |
+
self.supabase_client
|
| 293 |
+
.table("Scheduling")
|
| 294 |
+
.select("Social_network(token, sub)")
|
| 295 |
+
.eq("id", schedule_id)
|
| 296 |
+
.execute()
|
| 297 |
+
)
|
| 298 |
+
|
| 299 |
+
if not schedule_response.data:
|
| 300 |
+
raise Exception(f"Schedule {schedule_id} not found")
|
| 301 |
+
|
| 302 |
+
social_network = schedule_response.data[0].get('Social_network', {})
|
| 303 |
+
access_token = social_network.get('token')
|
| 304 |
+
user_sub = social_network.get('sub')
|
| 305 |
+
|
| 306 |
+
if not access_token or not user_sub:
|
| 307 |
+
logger.error(f"Missing social network credentials for schedule {schedule_id}")
|
| 308 |
+
return
|
| 309 |
+
|
| 310 |
+
# Publish to LinkedIn
|
| 311 |
+
linkedin_service = LinkedInService()
|
| 312 |
+
publish_response = linkedin_service.publish_post(
|
| 313 |
+
access_token, user_sub, text_content, image_url
|
| 314 |
+
)
|
| 315 |
+
|
| 316 |
+
# Update post status in database
|
| 317 |
+
update_response = (
|
| 318 |
+
self.supabase_client
|
| 319 |
+
.table("Post_content")
|
| 320 |
+
.update({"is_published": True})
|
| 321 |
+
.eq("id", post_id)
|
| 322 |
+
.execute()
|
| 323 |
+
)
|
| 324 |
+
|
| 325 |
+
logger.info(f"Post published successfully for schedule {schedule_id}")
|
| 326 |
+
|
| 327 |
+
except Exception as e:
|
| 328 |
+
logger.error(f"Error in publishing task for schedule {schedule_id}: {str(e)}")
|
| 329 |
+
|
| 330 |
+
def trigger_immediate_update(self):
|
| 331 |
+
"""Trigger immediate schedule update."""
|
| 332 |
+
try:
|
| 333 |
+
logger.info("Triggering immediate schedule update...")
|
| 334 |
+
self.load_schedules()
|
| 335 |
+
logger.info("Immediate schedule update completed")
|
| 336 |
+
return True
|
| 337 |
+
except Exception as e:
|
| 338 |
+
logger.error(f"Error triggering immediate schedule update: {str(e)}")
|
| 339 |
+
return False
|
| 340 |
+
|
| 341 |
+
def shutdown(self):
|
| 342 |
+
"""Shutdown the scheduler."""
|
| 343 |
+
if self.scheduler:
|
| 344 |
+
self.scheduler.shutdown()
|
| 345 |
+
logger.info("APS Scheduler shutdown")
|
backend/scheduler/task_scheduler.py
DELETED
|
@@ -1,270 +0,0 @@
|
|
| 1 |
-
import logging
|
| 2 |
-
from datetime import datetime, timedelta
|
| 3 |
-
from celery import current_app
|
| 4 |
-
from celery.schedules import crontab
|
| 5 |
-
from backend.services.content_service import ContentService
|
| 6 |
-
from backend.services.linkedin_service import LinkedInService
|
| 7 |
-
# Use relative import for the Config class to work with Hugging Face Spaces
|
| 8 |
-
from backend.config import Config
|
| 9 |
-
|
| 10 |
-
# Configure logging
|
| 11 |
-
logging.basicConfig(level=logging.INFO)
|
| 12 |
-
logger = logging.getLogger(__name__)
|
| 13 |
-
|
| 14 |
-
def get_supabase_config():
|
| 15 |
-
"""Get Supabase configuration from environment."""
|
| 16 |
-
return {
|
| 17 |
-
'SUPABASE_URL': Config.SUPABASE_URL,
|
| 18 |
-
'SUPABASE_KEY': Config.SUPABASE_KEY
|
| 19 |
-
}
|
| 20 |
-
|
| 21 |
-
def init_scheduler(supabase_client):
|
| 22 |
-
"""
|
| 23 |
-
Initialize the Celery-based task scheduler.
|
| 24 |
-
|
| 25 |
-
Args:
|
| 26 |
-
supabase_client: Supabase client instance
|
| 27 |
-
"""
|
| 28 |
-
logger.info("Initializing Celery scheduler")
|
| 29 |
-
# In a Celery-based approach, we don't need to initialize a scheduler here
|
| 30 |
-
# Tasks will be scheduled through Celery Beat or called directly
|
| 31 |
-
|
| 32 |
-
def parse_schedule_time(schedule_time):
|
| 33 |
-
"""
|
| 34 |
-
Parse schedule time string into crontab format.
|
| 35 |
-
|
| 36 |
-
Args:
|
| 37 |
-
schedule_time (str): Schedule time in format "Day HH:MM"
|
| 38 |
-
|
| 39 |
-
Returns:
|
| 40 |
-
dict: Crontab parameters
|
| 41 |
-
"""
|
| 42 |
-
try:
|
| 43 |
-
day_name, time_str = schedule_time.split()
|
| 44 |
-
hour, minute = map(int, time_str.split(':'))
|
| 45 |
-
|
| 46 |
-
# Map day names to crontab format
|
| 47 |
-
day_map = {
|
| 48 |
-
'Monday': 1,
|
| 49 |
-
'Tuesday': 2,
|
| 50 |
-
'Wednesday': 3,
|
| 51 |
-
'Thursday': 4,
|
| 52 |
-
'Friday': 5,
|
| 53 |
-
'Saturday': 6,
|
| 54 |
-
'Sunday': 0
|
| 55 |
-
}
|
| 56 |
-
|
| 57 |
-
day_of_week = day_map.get(day_name, '*')
|
| 58 |
-
|
| 59 |
-
return {
|
| 60 |
-
'minute': minute,
|
| 61 |
-
'hour': hour,
|
| 62 |
-
'day_of_week': day_of_week
|
| 63 |
-
}
|
| 64 |
-
except Exception as e:
|
| 65 |
-
logger.error(f"Error parsing schedule time {schedule_time}: {str(e)}")
|
| 66 |
-
# Default to every minute for error cases
|
| 67 |
-
return {
|
| 68 |
-
'minute': '*',
|
| 69 |
-
'hour': '*',
|
| 70 |
-
'day_of_week': '*'
|
| 71 |
-
}
|
| 72 |
-
|
| 73 |
-
def load_schedules(supabase_client):
|
| 74 |
-
"""
|
| 75 |
-
Load schedules from the database and create periodic tasks.
|
| 76 |
-
This function is called by the Celery Beat scheduler.
|
| 77 |
-
|
| 78 |
-
Args:
|
| 79 |
-
supabase_client: Supabase client instance
|
| 80 |
-
"""
|
| 81 |
-
try:
|
| 82 |
-
logger.info("Loading schedules from database...")
|
| 83 |
-
|
| 84 |
-
# Get Supabase configuration
|
| 85 |
-
supabase_config = get_supabase_config()
|
| 86 |
-
|
| 87 |
-
# Fetch all schedules from Supabase
|
| 88 |
-
response = (
|
| 89 |
-
supabase_client
|
| 90 |
-
.table("Scheduling")
|
| 91 |
-
.select("*, Social_network(id_utilisateur, token, sub)")
|
| 92 |
-
.execute()
|
| 93 |
-
)
|
| 94 |
-
|
| 95 |
-
schedules = response.data if response.data else []
|
| 96 |
-
logger.info(f"Found {len(schedules)} schedules")
|
| 97 |
-
|
| 98 |
-
# Get current beat schedule
|
| 99 |
-
current_schedule = current_app.conf.beat_schedule
|
| 100 |
-
|
| 101 |
-
# Remove existing scheduled jobs (except the loader job)
|
| 102 |
-
# In a production environment, you might want to be more selective about this
|
| 103 |
-
loader_job = current_schedule.get('load-schedules', {})
|
| 104 |
-
new_schedule = {'load-schedules': loader_job}
|
| 105 |
-
|
| 106 |
-
# Create jobs for each schedule
|
| 107 |
-
for schedule in schedules:
|
| 108 |
-
try:
|
| 109 |
-
schedule_id = schedule.get('id')
|
| 110 |
-
schedule_time = schedule.get('schedule_time')
|
| 111 |
-
adjusted_time = schedule.get('adjusted_time')
|
| 112 |
-
|
| 113 |
-
if not schedule_time or not adjusted_time:
|
| 114 |
-
logger.warning(f"Invalid schedule format for schedule {schedule_id}")
|
| 115 |
-
continue
|
| 116 |
-
|
| 117 |
-
# Parse schedule times
|
| 118 |
-
content_gen_time = parse_schedule_time(adjusted_time)
|
| 119 |
-
publish_time = parse_schedule_time(schedule_time)
|
| 120 |
-
|
| 121 |
-
# Create content generation job (5 minutes before publishing)
|
| 122 |
-
gen_job_id = f"gen_{schedule_id}"
|
| 123 |
-
new_schedule[gen_job_id] = {
|
| 124 |
-
'task': 'celery_tasks.content_tasks.generate_content_task',
|
| 125 |
-
'schedule': crontab(
|
| 126 |
-
minute=content_gen_time['minute'],
|
| 127 |
-
hour=content_gen_time['hour'],
|
| 128 |
-
day_of_week=content_gen_time['day_of_week']
|
| 129 |
-
),
|
| 130 |
-
'args': (
|
| 131 |
-
schedule.get('Social_network', {}).get('id_utilisateur'),
|
| 132 |
-
schedule_id,
|
| 133 |
-
supabase_config
|
| 134 |
-
)
|
| 135 |
-
}
|
| 136 |
-
logger.info(f"Created content generation job: {gen_job_id}")
|
| 137 |
-
|
| 138 |
-
# Create publishing job
|
| 139 |
-
pub_job_id = f"pub_{schedule_id}"
|
| 140 |
-
new_schedule[pub_job_id] = {
|
| 141 |
-
'task': 'celery_tasks.content_tasks.publish_post_task',
|
| 142 |
-
'schedule': crontab(
|
| 143 |
-
minute=publish_time['minute'],
|
| 144 |
-
hour=publish_time['hour'],
|
| 145 |
-
day_of_week=publish_time['day_of_week']
|
| 146 |
-
),
|
| 147 |
-
'args': (
|
| 148 |
-
schedule_id,
|
| 149 |
-
supabase_config
|
| 150 |
-
)
|
| 151 |
-
}
|
| 152 |
-
logger.info(f"Created publishing job: {pub_job_id}")
|
| 153 |
-
|
| 154 |
-
except Exception as e:
|
| 155 |
-
logger.error(f"Error creating jobs for schedule {schedule.get('id')}: {str(e)}")
|
| 156 |
-
|
| 157 |
-
# Update the beat schedule
|
| 158 |
-
current_app.conf.beat_schedule = new_schedule
|
| 159 |
-
logger.info("Updated Celery Beat schedule")
|
| 160 |
-
|
| 161 |
-
except Exception as e:
|
| 162 |
-
logger.error(f"Error loading schedules: {str(e)}")
|
| 163 |
-
|
| 164 |
-
def generate_content_job(schedule: dict, supabase_client):
|
| 165 |
-
"""
|
| 166 |
-
Job to generate content for a scheduled post.
|
| 167 |
-
This function is kept for backward compatibility but should be replaced with Celery tasks.
|
| 168 |
-
|
| 169 |
-
Args:
|
| 170 |
-
schedule (dict): Schedule data
|
| 171 |
-
supabase_client: Supabase client instance
|
| 172 |
-
"""
|
| 173 |
-
try:
|
| 174 |
-
schedule_id = schedule.get('id')
|
| 175 |
-
user_id = schedule.get('Social_network', {}).get('id_utilisateur')
|
| 176 |
-
|
| 177 |
-
if not user_id:
|
| 178 |
-
logger.warning(f"No user ID found for schedule {schedule_id}")
|
| 179 |
-
return
|
| 180 |
-
|
| 181 |
-
logger.info(f"Generating content for schedule {schedule_id}")
|
| 182 |
-
|
| 183 |
-
# Generate content using content service
|
| 184 |
-
content_service = ContentService()
|
| 185 |
-
generated_content = content_service.generate_post_content(user_id)
|
| 186 |
-
|
| 187 |
-
# Store generated content in database
|
| 188 |
-
social_account_id = schedule.get('id_social')
|
| 189 |
-
|
| 190 |
-
response = (
|
| 191 |
-
supabase_client
|
| 192 |
-
.table("Post_content")
|
| 193 |
-
.insert({
|
| 194 |
-
"social_account_id": social_account_id,
|
| 195 |
-
"Text_content": generated_content,
|
| 196 |
-
"is_published": False,
|
| 197 |
-
"sched": schedule_id
|
| 198 |
-
})
|
| 199 |
-
.execute()
|
| 200 |
-
)
|
| 201 |
-
|
| 202 |
-
if response.data:
|
| 203 |
-
logger.info(f"Content generated and stored for schedule {schedule_id}")
|
| 204 |
-
else:
|
| 205 |
-
logger.error(f"Failed to store generated content for schedule {schedule_id}")
|
| 206 |
-
|
| 207 |
-
except Exception as e:
|
| 208 |
-
logger.error(f"Error in content generation job for schedule {schedule.get('id')}: {str(e)}")
|
| 209 |
-
|
| 210 |
-
def publish_post_job(schedule: dict, supabase_client):
|
| 211 |
-
"""
|
| 212 |
-
Job to publish a scheduled post.
|
| 213 |
-
This function is kept for backward compatibility but should be replaced with Celery tasks.
|
| 214 |
-
|
| 215 |
-
Args:
|
| 216 |
-
schedule (dict): Schedule data
|
| 217 |
-
supabase_client: Supabase client instance
|
| 218 |
-
"""
|
| 219 |
-
try:
|
| 220 |
-
schedule_id = schedule.get('id')
|
| 221 |
-
logger.info(f"Publishing post for schedule {schedule_id}")
|
| 222 |
-
|
| 223 |
-
# Fetch the post to publish
|
| 224 |
-
response = (
|
| 225 |
-
supabase_client
|
| 226 |
-
.table("Post_content")
|
| 227 |
-
.select("*")
|
| 228 |
-
.eq("sched", schedule_id)
|
| 229 |
-
.eq("is_published", False)
|
| 230 |
-
.order("created_at", desc=True)
|
| 231 |
-
.limit(1)
|
| 232 |
-
.execute()
|
| 233 |
-
)
|
| 234 |
-
|
| 235 |
-
if not response.data:
|
| 236 |
-
logger.info(f"No unpublished posts found for schedule {schedule_id}")
|
| 237 |
-
return
|
| 238 |
-
|
| 239 |
-
post = response.data[0]
|
| 240 |
-
post_id = post.get('id')
|
| 241 |
-
text_content = post.get('Text_content')
|
| 242 |
-
image_url = post.get('image_content_url')
|
| 243 |
-
|
| 244 |
-
# Get social network credentials
|
| 245 |
-
access_token = schedule.get('Social_network', {}).get('token')
|
| 246 |
-
user_sub = schedule.get('Social_network', {}).get('sub')
|
| 247 |
-
|
| 248 |
-
if not access_token or not user_sub:
|
| 249 |
-
logger.error(f"Missing social network credentials for schedule {schedule_id}")
|
| 250 |
-
return
|
| 251 |
-
|
| 252 |
-
# Publish to LinkedIn
|
| 253 |
-
linkedin_service = LinkedInService()
|
| 254 |
-
publish_response = linkedin_service.publish_post(
|
| 255 |
-
access_token, user_sub, text_content, image_url
|
| 256 |
-
)
|
| 257 |
-
|
| 258 |
-
# Update post status in database
|
| 259 |
-
update_response = (
|
| 260 |
-
supabase_client
|
| 261 |
-
.table("Post_content")
|
| 262 |
-
.update({"is_published": True})
|
| 263 |
-
.eq("id", post_id)
|
| 264 |
-
.execute()
|
| 265 |
-
)
|
| 266 |
-
|
| 267 |
-
logger.info(f"Post published successfully for schedule {schedule_id}")
|
| 268 |
-
|
| 269 |
-
except Exception as e:
|
| 270 |
-
logger.error(f"Error in publishing job for schedule {schedule.get('id')}: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/scheduler/task_scheduler.py.bak
DELETED
|
@@ -1,252 +0,0 @@
|
|
| 1 |
-
from apscheduler.schedulers.background import BackgroundScheduler
|
| 2 |
-
from apscheduler.triggers.cron import CronTrigger
|
| 3 |
-
from datetime import datetime, timedelta
|
| 4 |
-
import logging
|
| 5 |
-
from services.content_service import ContentService
|
| 6 |
-
from services.linkedin_service import LinkedInService
|
| 7 |
-
|
| 8 |
-
# Configure logging
|
| 9 |
-
logging.basicConfig(level=logging.INFO)
|
| 10 |
-
logger = logging.getLogger(__name__)
|
| 11 |
-
|
| 12 |
-
def init_scheduler(scheduler: BackgroundScheduler, supabase_client):
|
| 13 |
-
"""
|
| 14 |
-
Initialize the task scheduler with jobs.
|
| 15 |
-
|
| 16 |
-
Args:
|
| 17 |
-
scheduler (BackgroundScheduler): The scheduler instance
|
| 18 |
-
supabase_client: Supabase client instance
|
| 19 |
-
"""
|
| 20 |
-
# Add a job to load schedules from database
|
| 21 |
-
scheduler.add_job(
|
| 22 |
-
func=load_schedules,
|
| 23 |
-
trigger=CronTrigger(minute='*/5'), # Run every 5 minutes
|
| 24 |
-
id='load_schedules',
|
| 25 |
-
name='Load schedules from database',
|
| 26 |
-
args=[scheduler, supabase_client]
|
| 27 |
-
)
|
| 28 |
-
|
| 29 |
-
# Load initial schedules
|
| 30 |
-
load_schedules(scheduler, supabase_client)
|
| 31 |
-
|
| 32 |
-
def load_schedules(scheduler: BackgroundScheduler, supabase_client):
|
| 33 |
-
"""
|
| 34 |
-
Load schedules from the database and create jobs.
|
| 35 |
-
|
| 36 |
-
Args:
|
| 37 |
-
scheduler (BackgroundScheduler): The scheduler instance
|
| 38 |
-
supabase_client: Supabase client instance
|
| 39 |
-
"""
|
| 40 |
-
try:
|
| 41 |
-
logger.info("Loading schedules from database...")
|
| 42 |
-
|
| 43 |
-
# Fetch all schedules from Supabase
|
| 44 |
-
response = (
|
| 45 |
-
supabase_client
|
| 46 |
-
.table("Scheduling")
|
| 47 |
-
.select("*, Social_network(id_utilisateur, token, sub)")
|
| 48 |
-
.execute()
|
| 49 |
-
)
|
| 50 |
-
|
| 51 |
-
schedules = response.data if response.data else []
|
| 52 |
-
logger.info(f"Found {len(schedules)} schedules")
|
| 53 |
-
|
| 54 |
-
# Remove existing scheduled jobs (except the loader job)
|
| 55 |
-
job_ids = [job.id for job in scheduler.get_jobs() if job.id != 'load_schedules']
|
| 56 |
-
for job_id in job_ids:
|
| 57 |
-
scheduler.remove_job(job_id)
|
| 58 |
-
logger.info(f"Removed job: {job_id}")
|
| 59 |
-
|
| 60 |
-
# Create jobs for each schedule
|
| 61 |
-
for schedule in schedules:
|
| 62 |
-
try:
|
| 63 |
-
create_scheduling_jobs(scheduler, schedule, supabase_client)
|
| 64 |
-
except Exception as e:
|
| 65 |
-
logger.error(f"Error creating jobs for schedule {schedule.get('id')}: {str(e)}")
|
| 66 |
-
|
| 67 |
-
except Exception as e:
|
| 68 |
-
logger.error(f"Error loading schedules: {str(e)}")
|
| 69 |
-
|
| 70 |
-
def create_scheduling_jobs(scheduler: BackgroundScheduler, schedule: dict, supabase_client):
|
| 71 |
-
"""
|
| 72 |
-
Create jobs for a specific schedule.
|
| 73 |
-
|
| 74 |
-
Args:
|
| 75 |
-
scheduler (BackgroundScheduler): The scheduler instance
|
| 76 |
-
schedule (dict): Schedule data
|
| 77 |
-
supabase_client: Supabase client instance
|
| 78 |
-
"""
|
| 79 |
-
schedule_id = schedule.get('id')
|
| 80 |
-
schedule_time = schedule.get('schedule_time')
|
| 81 |
-
adjusted_time = schedule.get('adjusted_time')
|
| 82 |
-
|
| 83 |
-
if not schedule_time or not adjusted_time:
|
| 84 |
-
logger.warning(f"Invalid schedule format for schedule {schedule_id}")
|
| 85 |
-
return
|
| 86 |
-
|
| 87 |
-
# Parse schedule times
|
| 88 |
-
try:
|
| 89 |
-
# Parse main schedule time (publishing)
|
| 90 |
-
day_name, time_str = schedule_time.split()
|
| 91 |
-
hour, minute = map(int, time_str.split(':'))
|
| 92 |
-
|
| 93 |
-
# Parse adjusted time (content generation)
|
| 94 |
-
adj_day_name, adj_time_str = adjusted_time.split()
|
| 95 |
-
adj_hour, adj_minute = map(int, adj_time_str.split(':'))
|
| 96 |
-
|
| 97 |
-
# Map day names to cron format
|
| 98 |
-
day_map = {
|
| 99 |
-
'Monday': 'mon',
|
| 100 |
-
'Tuesday': 'tue',
|
| 101 |
-
'Wednesday': 'wed',
|
| 102 |
-
'Thursday': 'thu',
|
| 103 |
-
'Friday': 'fri',
|
| 104 |
-
'Saturday': 'sat',
|
| 105 |
-
'Sunday': 'sun'
|
| 106 |
-
}
|
| 107 |
-
|
| 108 |
-
if day_name not in day_map or adj_day_name not in day_map:
|
| 109 |
-
logger.warning(f"Invalid day name in schedule {schedule_id}")
|
| 110 |
-
return
|
| 111 |
-
|
| 112 |
-
day_cron = day_map[day_name]
|
| 113 |
-
adj_day_cron = day_map[adj_day_name]
|
| 114 |
-
|
| 115 |
-
# Create content generation job (5 minutes before publishing)
|
| 116 |
-
gen_job_id = f"gen_{schedule_id}"
|
| 117 |
-
scheduler.add_job(
|
| 118 |
-
func=generate_content_job,
|
| 119 |
-
trigger=CronTrigger(
|
| 120 |
-
day_of_week=adj_day_cron,
|
| 121 |
-
hour=adj_hour,
|
| 122 |
-
minute=adj_minute
|
| 123 |
-
),
|
| 124 |
-
id=gen_job_id,
|
| 125 |
-
name=f"Generate content for schedule {schedule_id}",
|
| 126 |
-
args=[schedule, supabase_client]
|
| 127 |
-
)
|
| 128 |
-
logger.info(f"Created content generation job: {gen_job_id}")
|
| 129 |
-
|
| 130 |
-
# Create publishing job
|
| 131 |
-
pub_job_id = f"pub_{schedule_id}"
|
| 132 |
-
scheduler.add_job(
|
| 133 |
-
func=publish_post_job,
|
| 134 |
-
trigger=CronTrigger(
|
| 135 |
-
day_of_week=day_cron,
|
| 136 |
-
hour=hour,
|
| 137 |
-
minute=minute
|
| 138 |
-
),
|
| 139 |
-
id=pub_job_id,
|
| 140 |
-
name=f"Publish post for schedule {schedule_id}",
|
| 141 |
-
args=[schedule, supabase_client]
|
| 142 |
-
)
|
| 143 |
-
logger.info(f"Created publishing job: {pub_job_id}")
|
| 144 |
-
|
| 145 |
-
except Exception as e:
|
| 146 |
-
logger.error(f"Error creating jobs for schedule {schedule_id}: {str(e)}")
|
| 147 |
-
|
| 148 |
-
def generate_content_job(schedule: dict, supabase_client):
|
| 149 |
-
"""
|
| 150 |
-
Job to generate content for a scheduled post.
|
| 151 |
-
|
| 152 |
-
Args:
|
| 153 |
-
schedule (dict): Schedule data
|
| 154 |
-
supabase_client: Supabase client instance
|
| 155 |
-
"""
|
| 156 |
-
try:
|
| 157 |
-
schedule_id = schedule.get('id')
|
| 158 |
-
user_id = schedule.get('Social_network', {}).get('id_utilisateur')
|
| 159 |
-
|
| 160 |
-
if not user_id:
|
| 161 |
-
logger.warning(f"No user ID found for schedule {schedule_id}")
|
| 162 |
-
return
|
| 163 |
-
|
| 164 |
-
logger.info(f"Generating content for schedule {schedule_id}")
|
| 165 |
-
|
| 166 |
-
# Generate content using content service
|
| 167 |
-
content_service = ContentService()
|
| 168 |
-
generated_content = content_service.generate_post_content(user_id)
|
| 169 |
-
|
| 170 |
-
# Store generated content in database
|
| 171 |
-
social_account_id = schedule.get('id_social')
|
| 172 |
-
|
| 173 |
-
response = (
|
| 174 |
-
supabase_client
|
| 175 |
-
.table("Post_content")
|
| 176 |
-
.insert({
|
| 177 |
-
"social_account_id": social_account_id,
|
| 178 |
-
"Text_content": generated_content,
|
| 179 |
-
"is_published": False,
|
| 180 |
-
"sched": schedule_id
|
| 181 |
-
})
|
| 182 |
-
.execute()
|
| 183 |
-
)
|
| 184 |
-
|
| 185 |
-
if response.data:
|
| 186 |
-
logger.info(f"Content generated and stored for schedule {schedule_id}")
|
| 187 |
-
else:
|
| 188 |
-
logger.error(f"Failed to store generated content for schedule {schedule_id}")
|
| 189 |
-
|
| 190 |
-
except Exception as e:
|
| 191 |
-
logger.error(f"Error in content generation job for schedule {schedule.get('id')}: {str(e)}")
|
| 192 |
-
|
| 193 |
-
def publish_post_job(schedule: dict, supabase_client):
|
| 194 |
-
"""
|
| 195 |
-
Job to publish a scheduled post.
|
| 196 |
-
|
| 197 |
-
Args:
|
| 198 |
-
schedule (dict): Schedule data
|
| 199 |
-
supabase_client: Supabase client instance
|
| 200 |
-
"""
|
| 201 |
-
try:
|
| 202 |
-
schedule_id = schedule.get('id')
|
| 203 |
-
logger.info(f"Publishing post for schedule {schedule_id}")
|
| 204 |
-
|
| 205 |
-
# Fetch the post to publish
|
| 206 |
-
response = (
|
| 207 |
-
supabase_client
|
| 208 |
-
.table("Post_content")
|
| 209 |
-
.select("*")
|
| 210 |
-
.eq("sched", schedule_id)
|
| 211 |
-
.eq("is_published", False)
|
| 212 |
-
.order("created_at", desc=True)
|
| 213 |
-
.limit(1)
|
| 214 |
-
.execute()
|
| 215 |
-
)
|
| 216 |
-
|
| 217 |
-
if not response.data:
|
| 218 |
-
logger.info(f"No unpublished posts found for schedule {schedule_id}")
|
| 219 |
-
return
|
| 220 |
-
|
| 221 |
-
post = response.data[0]
|
| 222 |
-
post_id = post.get('id')
|
| 223 |
-
text_content = post.get('Text_content')
|
| 224 |
-
image_url = post.get('image_content_url')
|
| 225 |
-
|
| 226 |
-
# Get social network credentials
|
| 227 |
-
access_token = schedule.get('Social_network', {}).get('token')
|
| 228 |
-
user_sub = schedule.get('Social_network', {}).get('sub')
|
| 229 |
-
|
| 230 |
-
if not access_token or not user_sub:
|
| 231 |
-
logger.error(f"Missing social network credentials for schedule {schedule_id}")
|
| 232 |
-
return
|
| 233 |
-
|
| 234 |
-
# Publish to LinkedIn
|
| 235 |
-
linkedin_service = LinkedInService()
|
| 236 |
-
publish_response = linkedin_service.publish_post(
|
| 237 |
-
access_token, user_sub, text_content, image_url
|
| 238 |
-
)
|
| 239 |
-
|
| 240 |
-
# Update post status in database
|
| 241 |
-
update_response = (
|
| 242 |
-
supabase_client
|
| 243 |
-
.table("Post_content")
|
| 244 |
-
.update({"is_published": True})
|
| 245 |
-
.eq("id", post_id)
|
| 246 |
-
.execute()
|
| 247 |
-
)
|
| 248 |
-
|
| 249 |
-
logger.info(f"Post published successfully for schedule {schedule_id}")
|
| 250 |
-
|
| 251 |
-
except Exception as e:
|
| 252 |
-
logger.error(f"Error in publishing job for schedule {schedule.get('id')}: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/start_celery.bat
DELETED
|
@@ -1,44 +0,0 @@
|
|
| 1 |
-
@echo off
|
| 2 |
-
REM Script to start Celery components on Windows
|
| 3 |
-
|
| 4 |
-
REM Check if we're in the right directory
|
| 5 |
-
if not exist "app.py" (
|
| 6 |
-
echo Please run this script from the backend directory
|
| 7 |
-
pause
|
| 8 |
-
exit /b 1
|
| 9 |
-
)
|
| 10 |
-
|
| 11 |
-
REM Function to start Celery worker
|
| 12 |
-
:start_worker
|
| 13 |
-
echo Starting Celery worker...
|
| 14 |
-
start "Celery Worker" cmd /k "python start_celery.py worker"
|
| 15 |
-
echo Celery worker started
|
| 16 |
-
goto :eof
|
| 17 |
-
|
| 18 |
-
REM Function to start Celery Beat scheduler
|
| 19 |
-
:start_beat
|
| 20 |
-
echo Starting Celery Beat scheduler...
|
| 21 |
-
start "Celery Beat" cmd /k "python start_celery.py beat"
|
| 22 |
-
echo Celery Beat scheduler started
|
| 23 |
-
goto :eof
|
| 24 |
-
|
| 25 |
-
REM Main script logic
|
| 26 |
-
if "%1"=="worker" (
|
| 27 |
-
call :start_worker
|
| 28 |
-
) else if "%1"=="beat" (
|
| 29 |
-
call :start_beat
|
| 30 |
-
) else if "%1"=="all" (
|
| 31 |
-
call :start_worker
|
| 32 |
-
call :start_beat
|
| 33 |
-
) else if "%1"=="check" (
|
| 34 |
-
echo Checking system requirements...
|
| 35 |
-
python start_celery.py check
|
| 36 |
-
) else (
|
| 37 |
-
echo Usage: %0 {worker^|beat^|all^|check}
|
| 38 |
-
echo worker - Start Celery worker
|
| 39 |
-
echo beat - Start Celery Beat scheduler
|
| 40 |
-
echo all - Start both worker and scheduler
|
| 41 |
-
echo check - Check system requirements
|
| 42 |
-
pause
|
| 43 |
-
exit /b 1
|
| 44 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/start_celery.py
DELETED
|
@@ -1,129 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
Script to start Celery components for the Lin application.
|
| 4 |
-
This script provides a unified way to start Celery worker and beat scheduler.
|
| 5 |
-
"""
|
| 6 |
-
|
| 7 |
-
import os
|
| 8 |
-
import sys
|
| 9 |
-
import subprocess
|
| 10 |
-
import platform
|
| 11 |
-
from pathlib import Path
|
| 12 |
-
|
| 13 |
-
# Add the backend directory to Python path
|
| 14 |
-
backend_dir = Path(__file__).parent
|
| 15 |
-
sys.path.insert(0, str(backend_dir))
|
| 16 |
-
|
| 17 |
-
def check_redis():
|
| 18 |
-
"""Check if Redis is running."""
|
| 19 |
-
try:
|
| 20 |
-
import redis
|
| 21 |
-
client = redis.Redis(host='localhost', port=6379, db=0)
|
| 22 |
-
client.ping()
|
| 23 |
-
print("✓ Redis connection successful")
|
| 24 |
-
return True
|
| 25 |
-
except Exception as e:
|
| 26 |
-
print(f"✗ Redis connection failed: {e}")
|
| 27 |
-
print("Please start Redis server first:")
|
| 28 |
-
print(" Windows: redis-server")
|
| 29 |
-
print(" Linux/Mac: sudo systemctl start redis")
|
| 30 |
-
return False
|
| 31 |
-
|
| 32 |
-
def start_worker():
|
| 33 |
-
"""Start Celery worker."""
|
| 34 |
-
print("Starting Celery worker...")
|
| 35 |
-
# Add project root to PYTHONPATH
|
| 36 |
-
project_root = backend_dir.parent.resolve()
|
| 37 |
-
env = os.environ.copy()
|
| 38 |
-
current_pythonpath = env.get('PYTHONPATH', '')
|
| 39 |
-
env['PYTHONPATH'] = str(project_root) + os.pathsep + current_pythonpath if current_pythonpath else str(project_root)
|
| 40 |
-
|
| 41 |
-
cmd = [
|
| 42 |
-
sys.executable, "-m", "celery",
|
| 43 |
-
"-A", "celery_config:celery_app",
|
| 44 |
-
"worker",
|
| 45 |
-
"--loglevel=debug",
|
| 46 |
-
"--pool=solo",
|
| 47 |
-
"--max-tasks-per-child=100",
|
| 48 |
-
"--events" # Enable task events for monitoring
|
| 49 |
-
]
|
| 50 |
-
|
| 51 |
-
if platform.system() == "Windows":
|
| 52 |
-
subprocess.Popen(cmd, cwd=backend_dir, env=env)
|
| 53 |
-
else:
|
| 54 |
-
subprocess.Popen(cmd, cwd=backend_dir, env=env)
|
| 55 |
-
|
| 56 |
-
print("Celery worker started")
|
| 57 |
-
|
| 58 |
-
def start_beat():
|
| 59 |
-
"""Start Celery Beat scheduler."""
|
| 60 |
-
print("Starting Celery Beat scheduler...")
|
| 61 |
-
# Add project root to PYTHONPATH
|
| 62 |
-
project_root = backend_dir.parent.resolve()
|
| 63 |
-
env = os.environ.copy()
|
| 64 |
-
current_pythonpath = env.get('PYTHONPATH', '')
|
| 65 |
-
env['PYTHONPATH'] = str(project_root) + os.pathsep + current_pythonpath if current_pythonpath else str(project_root)
|
| 66 |
-
|
| 67 |
-
cmd = [
|
| 68 |
-
sys.executable, "-m", "celery",
|
| 69 |
-
"-A", "celery_config:celery_app",
|
| 70 |
-
"beat",
|
| 71 |
-
"--loglevel=debug",
|
| 72 |
-
"--schedule=/tmp/celerybeat-schedule" # Specify writable location for schedule file
|
| 73 |
-
]
|
| 74 |
-
|
| 75 |
-
if platform.system() == "Windows":
|
| 76 |
-
subprocess.Popen(cmd, cwd=backend_dir, env=env)
|
| 77 |
-
else:
|
| 78 |
-
subprocess.Popen(cmd, cwd=backend_dir, env=env)
|
| 79 |
-
|
| 80 |
-
print("Celery Beat scheduler started")
|
| 81 |
-
|
| 82 |
-
def start_all():
|
| 83 |
-
"""Start both worker and beat."""
|
| 84 |
-
if not check_redis():
|
| 85 |
-
return False
|
| 86 |
-
|
| 87 |
-
print("Starting all Celery components...")
|
| 88 |
-
start_worker()
|
| 89 |
-
start_beat()
|
| 90 |
-
print("All Celery components started")
|
| 91 |
-
return True
|
| 92 |
-
|
| 93 |
-
def main():
|
| 94 |
-
"""Main function."""
|
| 95 |
-
if len(sys.argv) < 2:
|
| 96 |
-
print("Usage: python start_celery.py <command>")
|
| 97 |
-
print("Commands:")
|
| 98 |
-
print(" worker - Start Celery worker only")
|
| 99 |
-
print(" beat - Start Celery Beat scheduler only")
|
| 100 |
-
print(" all - Start both worker and beat")
|
| 101 |
-
print(" check - Check system requirements")
|
| 102 |
-
sys.exit(1)
|
| 103 |
-
|
| 104 |
-
command = sys.argv[1].lower()
|
| 105 |
-
|
| 106 |
-
if command == "worker":
|
| 107 |
-
if not check_redis():
|
| 108 |
-
sys.exit(1)
|
| 109 |
-
start_worker()
|
| 110 |
-
elif command == "beat":
|
| 111 |
-
if not check_redis():
|
| 112 |
-
sys.exit(1)
|
| 113 |
-
start_beat()
|
| 114 |
-
elif command == "all":
|
| 115 |
-
if not start_all():
|
| 116 |
-
sys.exit(1)
|
| 117 |
-
elif command == "check":
|
| 118 |
-
print("Checking system requirements...")
|
| 119 |
-
if check_redis():
|
| 120 |
-
print("✓ All requirements met")
|
| 121 |
-
else:
|
| 122 |
-
print("✗ Some requirements not met")
|
| 123 |
-
sys.exit(1)
|
| 124 |
-
else:
|
| 125 |
-
print(f"Unknown command: {command}")
|
| 126 |
-
sys.exit(1)
|
| 127 |
-
|
| 128 |
-
if __name__ == "__main__":
|
| 129 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/start_celery.sh
DELETED
|
@@ -1,58 +0,0 @@
|
|
| 1 |
-
#!/bin/bash
|
| 2 |
-
# Script to start Celery components
|
| 3 |
-
|
| 4 |
-
# Check if we're in the right directory
|
| 5 |
-
if [ ! -f "app.py" ]; then
|
| 6 |
-
echo "Please run this script from the backend directory"
|
| 7 |
-
exit 1
|
| 8 |
-
fi
|
| 9 |
-
|
| 10 |
-
# Function to start Celery worker
|
| 11 |
-
start_worker() {
|
| 12 |
-
echo "Starting Celery worker..."
|
| 13 |
-
python start_celery.py worker &
|
| 14 |
-
echo "Celery worker started with PID $!"
|
| 15 |
-
}
|
| 16 |
-
|
| 17 |
-
# Function to start Celery Beat scheduler
|
| 18 |
-
start_beat() {
|
| 19 |
-
echo "Starting Celery Beat scheduler..."
|
| 20 |
-
python start_celery.py beat &
|
| 21 |
-
echo "Celery Beat scheduler started with PID $!"
|
| 22 |
-
}
|
| 23 |
-
|
| 24 |
-
# Function to start both worker and beat
|
| 25 |
-
start_all() {
|
| 26 |
-
start_worker
|
| 27 |
-
start_beat
|
| 28 |
-
}
|
| 29 |
-
|
| 30 |
-
# Function to check system requirements
|
| 31 |
-
check_requirements() {
|
| 32 |
-
echo "Checking system requirements..."
|
| 33 |
-
python start_celery.py check
|
| 34 |
-
}
|
| 35 |
-
|
| 36 |
-
# Main script logic
|
| 37 |
-
case "$1" in
|
| 38 |
-
worker)
|
| 39 |
-
start_worker
|
| 40 |
-
;;
|
| 41 |
-
beat)
|
| 42 |
-
start_beat
|
| 43 |
-
;;
|
| 44 |
-
all)
|
| 45 |
-
start_all
|
| 46 |
-
;;
|
| 47 |
-
check)
|
| 48 |
-
check_requirements
|
| 49 |
-
;;
|
| 50 |
-
*)
|
| 51 |
-
echo "Usage: $0 {worker|beat|all|check}"
|
| 52 |
-
echo " worker - Start Celery worker"
|
| 53 |
-
echo " beat - Start Celery Beat scheduler"
|
| 54 |
-
echo " all - Start both worker and scheduler"
|
| 55 |
-
echo " check - Check system requirements"
|
| 56 |
-
exit 1
|
| 57 |
-
;;
|
| 58 |
-
esac
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
start_app.py
CHANGED
|
@@ -1,82 +1,12 @@
|
|
| 1 |
#!/usr/bin/env python
|
| 2 |
"""
|
| 3 |
Entry point for the Lin application.
|
| 4 |
-
This script starts
|
| 5 |
"""
|
| 6 |
import os
|
| 7 |
import sys
|
| 8 |
-
import subprocess
|
| 9 |
-
import platform
|
| 10 |
-
import time
|
| 11 |
from pathlib import Path
|
| 12 |
|
| 13 |
-
def check_redis():
|
| 14 |
-
"""Check if Redis is running."""
|
| 15 |
-
try:
|
| 16 |
-
import redis
|
| 17 |
-
client = redis.Redis(host='localhost', port=6379, db=0)
|
| 18 |
-
client.ping()
|
| 19 |
-
print("✓ Redis connection successful")
|
| 20 |
-
return True
|
| 21 |
-
except Exception as e:
|
| 22 |
-
print(f"✗ Redis connection failed: {e}")
|
| 23 |
-
print("Please start Redis server first:")
|
| 24 |
-
print(" Windows: redis-server")
|
| 25 |
-
print(" Linux/Mac: sudo systemctl start redis")
|
| 26 |
-
return False
|
| 27 |
-
|
| 28 |
-
def start_celery_components():
|
| 29 |
-
"""Start Celery worker and beat scheduler in background processes."""
|
| 30 |
-
print("Starting Celery components...")
|
| 31 |
-
|
| 32 |
-
project_root = Path(__file__).parent.resolve() # Get the project root directory
|
| 33 |
-
backend_dir = project_root / "backend"
|
| 34 |
-
|
| 35 |
-
# Prepare the environment with updated PYTHONPATH
|
| 36 |
-
env = os.environ.copy()
|
| 37 |
-
# Prepend the project root to PYTHONPATH so 'backend' package can be found
|
| 38 |
-
current_pythonpath = env.get('PYTHONPATH', '')
|
| 39 |
-
env['PYTHONPATH'] = str(project_root) + os.pathsep + current_pythonpath if current_pythonpath else str(project_root)
|
| 40 |
-
|
| 41 |
-
# Start Celery worker
|
| 42 |
-
worker_cmd = [
|
| 43 |
-
sys.executable, "-m", "celery",
|
| 44 |
-
"-A", "celery_config:celery_app",
|
| 45 |
-
"worker",
|
| 46 |
-
"--loglevel=debug",
|
| 47 |
-
"--pool=solo",
|
| 48 |
-
"--max-tasks-per-child=100",
|
| 49 |
-
"--events", # Enable task events for monitoring
|
| 50 |
-
"-Q", "content,publish,scheduler" # Listen to all queues
|
| 51 |
-
]
|
| 52 |
-
|
| 53 |
-
# Start Celery beat
|
| 54 |
-
beat_cmd = [
|
| 55 |
-
sys.executable, "-m", "celery",
|
| 56 |
-
"-A", "celery_config:celery_app",
|
| 57 |
-
"beat",
|
| 58 |
-
"--loglevel=debug",
|
| 59 |
-
"--schedule=/tmp/celerybeat-schedule" # Specify writable location for schedule file
|
| 60 |
-
]
|
| 61 |
-
|
| 62 |
-
if platform.system() == "Windows":
|
| 63 |
-
# Windows: Use subprocess to start background processes with visible logs
|
| 64 |
-
subprocess.Popen(worker_cmd, cwd=backend_dir, env=env, # Pass the modified env
|
| 65 |
-
stdout=sys.stdout, stderr=sys.stderr,
|
| 66 |
-
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP)
|
| 67 |
-
subprocess.Popen(beat_cmd, cwd=backend_dir, env=env, # Pass the modified env
|
| 68 |
-
stdout=sys.stdout, stderr=sys.stderr,
|
| 69 |
-
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP)
|
| 70 |
-
else:
|
| 71 |
-
# Linux/Mac: Use subprocess with visible logs
|
| 72 |
-
subprocess.Popen(worker_cmd, cwd=backend_dir, env=env, # Pass the modified env
|
| 73 |
-
stdout=sys.stdout, stderr=sys.stderr)
|
| 74 |
-
subprocess.Popen(beat_cmd, cwd=backend_dir, env=env, # Pass the modified env
|
| 75 |
-
stdout=sys.stdout, stderr=sys.stderr)
|
| 76 |
-
|
| 77 |
-
print("Celery worker and beat scheduler started in background")
|
| 78 |
-
time.sleep(2) # Give Celery components time to start
|
| 79 |
-
|
| 80 |
if __name__ == "__main__":
|
| 81 |
# Set the port for Hugging Face Spaces
|
| 82 |
port = os.environ.get('PORT', '7860')
|
|
@@ -85,16 +15,7 @@ if __name__ == "__main__":
|
|
| 85 |
print(f"Starting Lin application on port {port}...")
|
| 86 |
print("=" * 60)
|
| 87 |
|
| 88 |
-
# Check if Redis is available
|
| 89 |
-
if not check_redis():
|
| 90 |
-
print("Warning: Redis not available. Celery may not function properly.")
|
| 91 |
-
print("Continuing with Flask app only...")
|
| 92 |
-
print("=" * 60)
|
| 93 |
-
|
| 94 |
try:
|
| 95 |
-
# Start Celery components first
|
| 96 |
-
start_celery_components()
|
| 97 |
-
|
| 98 |
# Import and run the backend Flask app directly
|
| 99 |
from backend.app import create_app
|
| 100 |
app = create_app()
|
|
@@ -115,6 +36,9 @@ if __name__ == "__main__":
|
|
| 115 |
|
| 116 |
except KeyboardInterrupt:
|
| 117 |
print("\nShutting down application...")
|
|
|
|
|
|
|
|
|
|
| 118 |
sys.exit(0)
|
| 119 |
except Exception as e:
|
| 120 |
print(f"Failed to start Lin application: {e}")
|
|
|
|
| 1 |
#!/usr/bin/env python
|
| 2 |
"""
|
| 3 |
Entry point for the Lin application.
|
| 4 |
+
This script starts the Flask application with APScheduler.
|
| 5 |
"""
|
| 6 |
import os
|
| 7 |
import sys
|
|
|
|
|
|
|
|
|
|
| 8 |
from pathlib import Path
|
| 9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
if __name__ == "__main__":
|
| 11 |
# Set the port for Hugging Face Spaces
|
| 12 |
port = os.environ.get('PORT', '7860')
|
|
|
|
| 15 |
print(f"Starting Lin application on port {port}...")
|
| 16 |
print("=" * 60)
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
try:
|
|
|
|
|
|
|
|
|
|
| 19 |
# Import and run the backend Flask app directly
|
| 20 |
from backend.app import create_app
|
| 21 |
app = create_app()
|
|
|
|
| 36 |
|
| 37 |
except KeyboardInterrupt:
|
| 38 |
print("\nShutting down application...")
|
| 39 |
+
# Shutdown scheduler if it exists
|
| 40 |
+
if hasattr(app, 'scheduler'):
|
| 41 |
+
app.scheduler.shutdown()
|
| 42 |
sys.exit(0)
|
| 43 |
except Exception as e:
|
| 44 |
print(f"Failed to start Lin application: {e}")
|