Saving local changes before rebase
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .env.example +6 -0
- .gitignore +3 -0
- Dockerfile +56 -0
- README.md +135 -8
- app.py +6 -0
- app/__init__.py +37 -0
- app/__pycache__/__init__.cpython-312.pyc +0 -0
- app/forms.py +15 -0
- app/models.py +22 -0
- app/routes/__init__.py +1 -0
- app/routes/api.py +61 -0
- app/routes/auth.py +35 -0
- app/routes/compliance.py +27 -0
- app/routes/dashboard.py +21 -0
- app/scripts/setup_yolo.py +57 -0
- app/services/ai_processor.py +66 -0
- app/services/blockchain.py +29 -0
- app/services/plugin_manager.py +20 -0
- app/services/predictive.py +16 -0
- app/services/scraper.py +46 -0
- app/static/css/styles.css +160 -0
- app/static/images/logo.png +0 -0
- app/static/js/chart.js +24 -0
- app/templates/base.html +25 -0
- app/templates/compliance_report.html +44 -0
- app/templates/dashboard.html +37 -0
- app/templates/login.html +15 -0
- app/tests/__init__.py +1 -0
- app/tests/test_models.py +26 -0
- app/tests/test_routes.py +33 -0
- app/tests/test_services.py +20 -0
- app/utils/__init__.py +1 -0
- app/utils/decorators.py +39 -0
- app/utils/helpers.py +24 -0
- app/utils/validators.py +33 -0
- celery_worker.py +4 -0
- check_dependencies.py +88 -0
- config.py +20 -0
- docker-compose.yml +97 -0
- gunicorn.conf.py +29 -0
- hf_env/Lib/site-packages/PyYAML-6.0.2.dist-info/INSTALLER +1 -0
- hf_env/Lib/site-packages/PyYAML-6.0.2.dist-info/LICENSE +20 -0
- hf_env/Lib/site-packages/PyYAML-6.0.2.dist-info/METADATA +46 -0
- hf_env/Lib/site-packages/PyYAML-6.0.2.dist-info/RECORD +43 -0
- hf_env/Lib/site-packages/PyYAML-6.0.2.dist-info/WHEEL +5 -0
- hf_env/Lib/site-packages/PyYAML-6.0.2.dist-info/top_level.txt +2 -0
- hf_env/Lib/site-packages/__pycache__/typing_extensions.cpython-312.pyc +0 -0
- hf_env/Lib/site-packages/_yaml/__init__.py +33 -0
- hf_env/Lib/site-packages/_yaml/__pycache__/__init__.cpython-312.pyc +0 -0
- hf_env/Lib/site-packages/certifi-2025.1.31.dist-info/INSTALLER +1 -0
.env.example
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FLASK_APP=app.py
|
| 2 |
+
FLASK_ENV=development
|
| 3 |
+
SECRET_KEY=your-secret-key-here
|
| 4 |
+
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
|
| 5 |
+
REDIS_URL=redis://localhost:6379/0
|
| 6 |
+
SELENIUM_HUB_URL=http://selenium-hub:4444/wd/hub
|
.gitignore
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# YOLO model files
|
| 2 |
+
app/models/*.weights
|
| 3 |
+
app/models/*.cfg
|
Dockerfile
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.9-slim
|
| 2 |
+
|
| 3 |
+
# Set environment variables
|
| 4 |
+
ENV FLASK_APP=app.py
|
| 5 |
+
ENV FLASK_ENV=production
|
| 6 |
+
ENV PATH="/home/appuser/.local/bin:$PATH"
|
| 7 |
+
ENV PYTHONUNBUFFERED=1
|
| 8 |
+
|
| 9 |
+
# Install system dependencies for OpenCV and other requirements
|
| 10 |
+
RUN apt-get update && apt-get install -y \
|
| 11 |
+
libgl1-mesa-glx \
|
| 12 |
+
libglib2.0-0 \
|
| 13 |
+
libsm6 \
|
| 14 |
+
libxext6 \
|
| 15 |
+
libxrender-dev \
|
| 16 |
+
tesseract-ocr \
|
| 17 |
+
wget \
|
| 18 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 19 |
+
|
| 20 |
+
# Create a non-root user
|
| 21 |
+
RUN useradd -m appuser && mkdir -p /app /app/instance /app/models && chown -R appuser:appuser /app
|
| 22 |
+
|
| 23 |
+
# Set the working directory
|
| 24 |
+
WORKDIR /app
|
| 25 |
+
|
| 26 |
+
# Copy requirements and install dependencies
|
| 27 |
+
COPY --chown=appuser:appuser requirements.txt .
|
| 28 |
+
|
| 29 |
+
# Install dependencies with proper error handling
|
| 30 |
+
RUN pip install --no-cache-dir "numpy<2.0.0" && \
|
| 31 |
+
pip install --no-cache-dir -r requirements.txt && \
|
| 32 |
+
# Verify critical packages are installed
|
| 33 |
+
python -c "import flask, flask_sqlalchemy, redis, celery; print('All critical dependencies installed successfully')"
|
| 34 |
+
|
| 35 |
+
# Download YOLO files
|
| 36 |
+
RUN wget https://github.com/AlexeyAB/darknet/releases/download/darknet_yolo_v3_optimal/yolov4.weights -O /app/models/yolov4.weights && \
|
| 37 |
+
wget https://raw.githubusercontent.com/AlexeyAB/darknet/master/cfg/yolov4.cfg -O /app/models/yolov4.cfg && \
|
| 38 |
+
chown appuser:appuser /app/models/yolov4.*
|
| 39 |
+
|
| 40 |
+
# Create a health check script
|
| 41 |
+
RUN echo '#!/bin/sh\npython -c "import flask, flask_sqlalchemy, redis, celery; print(\"Dependencies OK\")"' > /app/healthcheck.sh
|
| 42 |
+
|
| 43 |
+
# Copy the application code
|
| 44 |
+
COPY --chown=appuser:appuser . .
|
| 45 |
+
|
| 46 |
+
# Make scripts executable
|
| 47 |
+
RUN chmod +x /app/prestart.sh /app/healthcheck.sh
|
| 48 |
+
|
| 49 |
+
# Switch to non-root user
|
| 50 |
+
USER appuser
|
| 51 |
+
|
| 52 |
+
# Expose the port
|
| 53 |
+
EXPOSE 5000
|
| 54 |
+
|
| 55 |
+
# Run the prestart script and then start the application
|
| 56 |
+
CMD ["/bin/bash", "-c", "/app/prestart.sh && gunicorn --config=./gunicorn.conf.py --workers=2 app:create_app()"]
|
README.md
CHANGED
|
@@ -1,12 +1,139 @@
|
|
| 1 |
---
|
| 2 |
-
title: Fb
|
| 3 |
-
emoji: 📊
|
| 4 |
-
colorFrom: red
|
| 5 |
-
colorTo: yellow
|
| 6 |
-
sdk: docker
|
| 7 |
-
pinned: false
|
| 8 |
license: apache-2.0
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
| 1 |
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
license: apache-2.0
|
| 3 |
+
title: Facebook Ad Analytics
|
| 4 |
+
sdk: docker
|
| 5 |
+
emoji: 📊
|
| 6 |
+
colorFrom: green
|
| 7 |
+
colorTo: blue
|
| 8 |
+
pinned: true
|
| 9 |
+
short_description: facebook-ad-analytics
|
| 10 |
---
|
| 11 |
+
# Facebook Ad Analytics
|
| 12 |
+
|
| 13 |
+
A comprehensive tool for scraping, analyzing, and visualizing Facebook ads using AI and machine learning.
|
| 14 |
+
|
| 15 |
+
## Features
|
| 16 |
+
- **Ad Scraping**: Scrape Facebook ads using Selenium.
|
| 17 |
+
- **Sentiment Analysis**: Analyze ad sentiment using AI (Hugging Face Transformers).
|
| 18 |
+
- **Image Analysis**: Extract text from images (OCR) and detect objects using YOLOv4.
|
| 19 |
+
- **Compliance Reporting**: Generate compliance reports and anonymize ads.
|
| 20 |
+
- **Predictive Analytics**: Forecast ad performance using machine learning.
|
| 21 |
+
- **Google Ads Analysis**: Scrape and analyze Google Search and Display ads.
|
| 22 |
+
|
| 23 |
+
## Requirements
|
| 24 |
+
- Python 3.9+
|
| 25 |
+
- PostgreSQL (recommended for production)
|
| 26 |
+
- Redis (for Celery task queue)
|
| 27 |
+
- Tesseract OCR
|
| 28 |
+
|
| 29 |
+
## Setup
|
| 30 |
+
|
| 31 |
+
### Quick Setup
|
| 32 |
+
1. Clone the repository:
|
| 33 |
+
```bash
|
| 34 |
+
git clone https://github.com/yourusername/facebook-ad-analytics.git
|
| 35 |
+
cd facebook-ad-analytics
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
2. Run the setup script:
|
| 39 |
+
```bash
|
| 40 |
+
python setup.py
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
3. Update the `.env` file with your settings.
|
| 44 |
+
|
| 45 |
+
4. Run the application:
|
| 46 |
+
```bash
|
| 47 |
+
python manage.py run
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
### Manual Setup
|
| 51 |
+
1. Clone the repository:
|
| 52 |
+
```bash
|
| 53 |
+
git clone https://github.com/yourusername/facebook-ad-analytics.git
|
| 54 |
+
cd facebook-ad-analytics
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
2. Create a virtual environment and install dependencies:
|
| 58 |
+
```bash
|
| 59 |
+
python -m venv venv
|
| 60 |
+
source venv/bin/activate # On Windows: venv\Scripts\activate
|
| 61 |
+
pip install -r requirements.txt
|
| 62 |
+
```
|
| 63 |
+
|
| 64 |
+
3. Copy the example environment file and update it:
|
| 65 |
+
```bash
|
| 66 |
+
cp .env.example .env
|
| 67 |
+
# Edit .env with your settings
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
4. Initialize the database:
|
| 71 |
+
```bash
|
| 72 |
+
python manage.py init-db
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
5. Download YOLOv4 models:
|
| 76 |
+
```bash
|
| 77 |
+
mkdir -p app/models
|
| 78 |
+
wget https://github.com/AlexeyAB/darknet/releases/download/darknet_yolo_v3_optimal/yolov4.weights -O app/models/yolov4.weights
|
| 79 |
+
wget https://raw.githubusercontent.com/AlexeyAB/darknet/master/cfg/yolov4.cfg -O app/models/yolov4.cfg
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
6. Run the application:
|
| 83 |
+
```bash
|
| 84 |
+
python manage.py run
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
## Docker Deployment
|
| 88 |
+
For production deployment, use Docker Compose:
|
| 89 |
+
|
| 90 |
+
```bash
|
| 91 |
+
# Set environment variables in .env file first
|
| 92 |
+
docker-compose up -d
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
## Troubleshooting
|
| 96 |
+
|
| 97 |
+
### Missing Dependencies
|
| 98 |
+
If you encounter errors about missing dependencies, run the dependency checker:
|
| 99 |
+
|
| 100 |
+
```bash
|
| 101 |
+
python check_dependencies.py
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
This will identify any missing packages. You can install them with:
|
| 105 |
+
|
| 106 |
+
```bash
|
| 107 |
+
pip install -r requirements.txt
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
### Common Issues
|
| 111 |
+
|
| 112 |
+
#### ModuleNotFoundError: No module named 'ratelimit'
|
| 113 |
+
This error occurs when the ratelimit package is missing. Install it with:
|
| 114 |
+
|
| 115 |
+
```bash
|
| 116 |
+
pip install ratelimit
|
| 117 |
+
```
|
| 118 |
+
|
| 119 |
+
#### AI Model Warnings
|
| 120 |
+
Warnings about missing PyTorch, TensorFlow, or Flax are normal if you don't need the full AI capabilities. For full functionality, install:
|
| 121 |
+
|
| 122 |
+
```bash
|
| 123 |
+
pip install torch==2.0.1
|
| 124 |
+
```
|
| 125 |
+
|
| 126 |
+
## Testing
|
| 127 |
+
Run the test suite:
|
| 128 |
+
```bash
|
| 129 |
+
python manage.py test
|
| 130 |
+
```
|
| 131 |
+
|
| 132 |
+
## API Documentation
|
| 133 |
+
The API endpoints are available at `/api/v1/` and include:
|
| 134 |
+
- `/api/v1/ads` - List and create ads
|
| 135 |
+
- `/api/v1/ads/<id>` - Get, update, or delete an ad
|
| 136 |
+
- `/api/v1/analyze` - Analyze ad content
|
| 137 |
|
| 138 |
+
## License
|
| 139 |
+
This project is licensed under the Apache 2.0 License - see the LICENSE file for details.
|
app.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Flask
|
| 2 |
+
|
| 3 |
+
def create_app():
|
| 4 |
+
app = Flask(__name__)
|
| 5 |
+
app.config['INSTANCE_PATH'] = '/tmp/instance' # Ensure this path exists
|
| 6 |
+
return app
|
app/__init__.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Flask
|
| 2 |
+
from flask_sqlalchemy import SQLAlchemy
|
| 3 |
+
from flask_login import LoginManager
|
| 4 |
+
from celery import Celery
|
| 5 |
+
import redis
|
| 6 |
+
|
| 7 |
+
db = SQLAlchemy()
|
| 8 |
+
login = LoginManager()
|
| 9 |
+
celery = Celery(__name__)
|
| 10 |
+
cache = redis.Redis()
|
| 11 |
+
|
| 12 |
+
def create_app():
|
| 13 |
+
# Create the Flask app first
|
| 14 |
+
app = Flask(__name__)
|
| 15 |
+
|
| 16 |
+
# Load configuration
|
| 17 |
+
app.config.from_object('config.Config')
|
| 18 |
+
|
| 19 |
+
# Set the instance path after loading the config
|
| 20 |
+
app.instance_path = app.config['INSTANCE_PATH']
|
| 21 |
+
|
| 22 |
+
# Initialize extensions
|
| 23 |
+
db.init_app(app)
|
| 24 |
+
login.init_app(app)
|
| 25 |
+
celery.conf.update(app.config)
|
| 26 |
+
|
| 27 |
+
# Register Blueprints
|
| 28 |
+
from .routes.auth import auth_bp
|
| 29 |
+
from .routes.dashboard import dashboard_bp
|
| 30 |
+
from .routes.api import api_bp
|
| 31 |
+
from .routes.compliance import compliance_bp
|
| 32 |
+
app.register_blueprint(auth_bp)
|
| 33 |
+
app.register_blueprint(dashboard_bp)
|
| 34 |
+
app.register_blueprint(api_bp)
|
| 35 |
+
app.register_blueprint(compliance_bp)
|
| 36 |
+
|
| 37 |
+
return app
|
app/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (1.56 kB). View file
|
|
|
app/forms.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask_wtf import FlaskForm
|
| 2 |
+
from wtforms import StringField, PasswordField, SubmitField
|
| 3 |
+
from wtforms.validators import DataRequired, Email, EqualTo
|
| 4 |
+
|
| 5 |
+
class LoginForm(FlaskForm):
|
| 6 |
+
email = StringField('Email', validators=[DataRequired(), Email()])
|
| 7 |
+
password = PasswordField('Password', validators=[DataRequired()])
|
| 8 |
+
submit = SubmitField('Login')
|
| 9 |
+
|
| 10 |
+
class RegistrationForm(FlaskForm):
|
| 11 |
+
email = StringField('Email', validators=[DataRequired(), Email()])
|
| 12 |
+
password = PasswordField('Password', validators=[DataRequired()])
|
| 13 |
+
password2 = PasswordField(
|
| 14 |
+
'Repeat Password', validators=[DataRequired(), EqualTo('password')])
|
| 15 |
+
submit = SubmitField('Register')
|
app/models.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from . import db
|
| 2 |
+
from datetime import datetime
|
| 3 |
+
|
| 4 |
+
class User(db.Model):
|
| 5 |
+
id = db.Column(db.Integer, primary_key=True)
|
| 6 |
+
email = db.Column(db.String(120), unique=True, nullable=False)
|
| 7 |
+
password = db.Column(db.String(128), nullable=False)
|
| 8 |
+
role = db.Column(db.String(20), default="viewer")
|
| 9 |
+
|
| 10 |
+
class Ad(db.Model):
|
| 11 |
+
id = db.Column(db.UUID, primary_key=True)
|
| 12 |
+
content = db.Column(db.Text)
|
| 13 |
+
sentiment = db.Column(db.JSON)
|
| 14 |
+
ad_metadata = db.Column(db.JSON)
|
| 15 |
+
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
| 16 |
+
versions = db.relationship('AdVersion', backref='ad')
|
| 17 |
+
|
| 18 |
+
class AdVersion(db.Model):
|
| 19 |
+
id = db.Column(db.Integer, primary_key=True)
|
| 20 |
+
ad_id = db.Column(db.UUID, db.ForeignKey('ad.id'))
|
| 21 |
+
content = db.Column(db.Text)
|
| 22 |
+
timestamp = db.Column(db.DateTime, default=datetime.utcnow)
|
app/routes/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Empty file to make the directory a package
|
app/routes/api.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Blueprint, jsonify, request
|
| 2 |
+
from flask_login import login_required
|
| 3 |
+
from ..models import Ad
|
| 4 |
+
from ..services.ai_processor import AIPipeline
|
| 5 |
+
from ..utils.validators import validate_request_json
|
| 6 |
+
from ..utils.decorators import cache_response
|
| 7 |
+
import logging
|
| 8 |
+
|
| 9 |
+
logger = logging.getLogger(__name__)
|
| 10 |
+
|
| 11 |
+
# Try to import ratelimit, but provide a fallback if it's not available
|
| 12 |
+
try:
|
| 13 |
+
from ratelimit import limits, RateLimitException
|
| 14 |
+
RATELIMIT_AVAILABLE = True
|
| 15 |
+
logger.info("Rate limiting is enabled")
|
| 16 |
+
except ImportError:
|
| 17 |
+
logger.warning("ratelimit package not found. Rate limiting is disabled.")
|
| 18 |
+
RATELIMIT_AVAILABLE = False
|
| 19 |
+
|
| 20 |
+
# Define fallback decorator and exception
|
| 21 |
+
def limits(calls, period):
|
| 22 |
+
def decorator(f):
|
| 23 |
+
return f
|
| 24 |
+
return decorator
|
| 25 |
+
|
| 26 |
+
class RateLimitException(Exception):
|
| 27 |
+
pass
|
| 28 |
+
|
| 29 |
+
api_bp = Blueprint('api', __name__)
|
| 30 |
+
|
| 31 |
+
ONE_MINUTE = 60
|
| 32 |
+
MAX_REQUESTS_PER_MINUTE = 30
|
| 33 |
+
|
| 34 |
+
@api_bp.route('/ads', methods=['GET'])
|
| 35 |
+
@login_required
|
| 36 |
+
def get_ads():
|
| 37 |
+
ads = Ad.query.all()
|
| 38 |
+
return jsonify([{
|
| 39 |
+
'id': ad.id,
|
| 40 |
+
'content': ad.content,
|
| 41 |
+
'sentiment': ad.sentiment
|
| 42 |
+
} for ad in ads])
|
| 43 |
+
|
| 44 |
+
@api_bp.route('/analyze', methods=['POST'])
|
| 45 |
+
@login_required
|
| 46 |
+
@limits(calls=MAX_REQUESTS_PER_MINUTE, period=ONE_MINUTE)
|
| 47 |
+
def analyze_ad():
|
| 48 |
+
validation_error = validate_request_json(['text'])
|
| 49 |
+
if validation_error:
|
| 50 |
+
return validation_error
|
| 51 |
+
|
| 52 |
+
try:
|
| 53 |
+
data = request.json
|
| 54 |
+
ad_text = data['text']
|
| 55 |
+
ai = AIPipeline()
|
| 56 |
+
result = ai.process_ad(ad_text)
|
| 57 |
+
return jsonify(result)
|
| 58 |
+
except RateLimitException:
|
| 59 |
+
return jsonify({"error": "Rate limit exceeded"}), 429
|
| 60 |
+
except Exception as e:
|
| 61 |
+
return jsonify({"error": str(e)}), 500
|
app/routes/auth.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Blueprint, render_template, redirect, url_for, flash
|
| 2 |
+
from flask_login import login_user, logout_user, login_required
|
| 3 |
+
from ..models import User
|
| 4 |
+
from ..forms import LoginForm, RegistrationForm
|
| 5 |
+
|
| 6 |
+
auth_bp = Blueprint('auth', __name__)
|
| 7 |
+
|
| 8 |
+
@auth_bp.route('/login', methods=['GET', 'POST'])
|
| 9 |
+
def login():
|
| 10 |
+
form = LoginForm()
|
| 11 |
+
if form.validate_on_submit():
|
| 12 |
+
user = User.query.filter_by(email=form.email.data).first()
|
| 13 |
+
if user and user.check_password(form.password.data):
|
| 14 |
+
login_user(user)
|
| 15 |
+
return redirect(url_for('dashboard.index'))
|
| 16 |
+
flash('Invalid email or password')
|
| 17 |
+
return render_template('login.html', form=form)
|
| 18 |
+
|
| 19 |
+
@auth_bp.route('/logout')
|
| 20 |
+
@login_required
|
| 21 |
+
def logout():
|
| 22 |
+
logout_user()
|
| 23 |
+
return redirect(url_for('auth.login'))
|
| 24 |
+
|
| 25 |
+
@auth_bp.route('/register', methods=['GET', 'POST'])
|
| 26 |
+
def register():
|
| 27 |
+
form = RegistrationForm()
|
| 28 |
+
if form.validate_on_submit():
|
| 29 |
+
user = User(email=form.email.data)
|
| 30 |
+
user.set_password(form.password.data)
|
| 31 |
+
db.session.add(user)
|
| 32 |
+
db.session.commit()
|
| 33 |
+
flash('Registration successful!')
|
| 34 |
+
return redirect(url_for('auth.login'))
|
| 35 |
+
return render_template('register.html', form=form)
|
app/routes/compliance.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Blueprint, render_template, request, jsonify
|
| 2 |
+
from flask_login import login_required
|
| 3 |
+
from ..models import Ad
|
| 4 |
+
from ..utils.decorators import admin_required
|
| 5 |
+
from .. import db
|
| 6 |
+
|
| 7 |
+
compliance_bp = Blueprint('compliance', __name__)
|
| 8 |
+
|
| 9 |
+
@compliance_bp.route('/report')
|
| 10 |
+
@login_required
|
| 11 |
+
@admin_required
|
| 12 |
+
def compliance_report():
|
| 13 |
+
ads = Ad.query.all()
|
| 14 |
+
return render_template('compliance_report.html', ads=ads)
|
| 15 |
+
|
| 16 |
+
@compliance_bp.route('/anonymize/<ad_id>', methods=['POST'])
|
| 17 |
+
@login_required
|
| 18 |
+
@admin_required
|
| 19 |
+
def anonymize_ad(ad_id):
|
| 20 |
+
try:
|
| 21 |
+
ad = Ad.query.get_or_404(ad_id)
|
| 22 |
+
ad.content = "REDACTED"
|
| 23 |
+
db.session.commit()
|
| 24 |
+
return jsonify({'status': 'success'})
|
| 25 |
+
except Exception as e:
|
| 26 |
+
db.session.rollback()
|
| 27 |
+
return jsonify({'status': 'error', 'message': str(e)}), 500
|
app/routes/dashboard.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Blueprint, render_template, request
|
| 2 |
+
from flask_login import login_required
|
| 3 |
+
from ..models import Ad
|
| 4 |
+
from ..services.ai_processor import AIPipeline
|
| 5 |
+
|
| 6 |
+
dashboard_bp = Blueprint('dashboard', __name__)
|
| 7 |
+
|
| 8 |
+
@dashboard_bp.route('/')
|
| 9 |
+
@login_required
|
| 10 |
+
def index():
|
| 11 |
+
page = request.args.get('page', 1, type=int)
|
| 12 |
+
per_page = 10
|
| 13 |
+
query = request.args.get('query', '')
|
| 14 |
+
sentiment_filter = request.args.get('sentiment', '')
|
| 15 |
+
|
| 16 |
+
ads = Ad.query.filter(
|
| 17 |
+
Ad.content.contains(query),
|
| 18 |
+
Ad.sentiment.contains(sentiment_filter)
|
| 19 |
+
).paginate(page=page, per_page=per_page)
|
| 20 |
+
|
| 21 |
+
return render_template('dashboard.html', ads=ads, query=query, sentiment_filter=sentiment_filter)
|
app/scripts/setup_yolo.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import urllib.request
|
| 2 |
+
import os
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import logging
|
| 5 |
+
|
| 6 |
+
logging.basicConfig(level=logging.INFO)
|
| 7 |
+
logger = logging.getLogger(__name__)
|
| 8 |
+
|
| 9 |
+
YOLO_CONFIG = {
|
| 10 |
+
'weights': {
|
| 11 |
+
'url': 'https://github.com/AlexeyAB/darknet/releases/download/darknet_yolo_v3_optimal/yolov4.weights',
|
| 12 |
+
'filename': 'yolov4.weights'
|
| 13 |
+
},
|
| 14 |
+
'config': {
|
| 15 |
+
'url': 'https://raw.githubusercontent.com/AlexeyAB/darknet/master/cfg/yolov4.cfg',
|
| 16 |
+
'filename': 'yolov4.cfg'
|
| 17 |
+
}
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
def download_file(url: str, filename: str):
|
| 21 |
+
"""Download a file and show progress."""
|
| 22 |
+
try:
|
| 23 |
+
logger.info(f"Downloading {filename}...")
|
| 24 |
+
|
| 25 |
+
def report_progress(count, block_size, total_size):
|
| 26 |
+
percent = int(count * block_size * 100 / total_size)
|
| 27 |
+
print(f"\rProgress: {percent}%", end='')
|
| 28 |
+
|
| 29 |
+
filepath = Path('app/models') / filename
|
| 30 |
+
filepath.parent.mkdir(parents=True, exist_ok=True)
|
| 31 |
+
|
| 32 |
+
urllib.request.urlretrieve(
|
| 33 |
+
url,
|
| 34 |
+
filepath,
|
| 35 |
+
reporthook=report_progress
|
| 36 |
+
)
|
| 37 |
+
print() # New line after progress
|
| 38 |
+
logger.info(f"Successfully downloaded {filename}")
|
| 39 |
+
return True
|
| 40 |
+
except Exception as e:
|
| 41 |
+
logger.error(f"Error downloading {filename}: {e}")
|
| 42 |
+
return False
|
| 43 |
+
|
| 44 |
+
def setup_yolo():
|
| 45 |
+
"""Download and set up YOLO files."""
|
| 46 |
+
success = True
|
| 47 |
+
for file_info in YOLO_CONFIG.values():
|
| 48 |
+
if not download_file(file_info['url'], file_info['filename']):
|
| 49 |
+
success = False
|
| 50 |
+
|
| 51 |
+
if success:
|
| 52 |
+
logger.info("YOLO setup completed successfully!")
|
| 53 |
+
else:
|
| 54 |
+
logger.error("YOLO setup failed. Please check the errors above.")
|
| 55 |
+
|
| 56 |
+
if __name__ == "__main__":
|
| 57 |
+
setup_yolo()
|
app/services/ai_processor.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import pytesseract
|
| 3 |
+
from transformers import pipeline
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
import logging
|
| 6 |
+
|
| 7 |
+
logger = logging.getLogger(__name__)
|
| 8 |
+
|
| 9 |
+
class AIPipeline:
|
| 10 |
+
def __init__(self):
|
| 11 |
+
try:
|
| 12 |
+
self.nlp = pipeline("text-classification", model="roberta-base")
|
| 13 |
+
|
| 14 |
+
model_dir = Path("app/models")
|
| 15 |
+
weights_path = model_dir / "yolov4.weights"
|
| 16 |
+
config_path = model_dir / "yolov4.cfg"
|
| 17 |
+
|
| 18 |
+
if not (weights_path.exists() and config_path.exists()):
|
| 19 |
+
logger.warning("YOLOv4 files not found. Please run setup_yolo.py first.")
|
| 20 |
+
self.detector = None
|
| 21 |
+
else:
|
| 22 |
+
self.detector = cv2.dnn.readNet(str(weights_path), str(config_path))
|
| 23 |
+
|
| 24 |
+
except Exception as e:
|
| 25 |
+
logger.error(f"Error initializing AI Pipeline: {e}")
|
| 26 |
+
raise
|
| 27 |
+
|
| 28 |
+
def process_ad(self, ad):
|
| 29 |
+
try:
|
| 30 |
+
results = {
|
| 31 |
+
"sentiment": self._analyze_sentiment(ad.content),
|
| 32 |
+
"ocr": self._extract_ocr(ad.media) if hasattr(ad, 'media') else None,
|
| 33 |
+
"objects": self._detect_objects(ad.media) if hasattr(ad, 'media') else None
|
| 34 |
+
}
|
| 35 |
+
return results
|
| 36 |
+
except Exception as e:
|
| 37 |
+
logger.error(f"Error processing ad: {e}")
|
| 38 |
+
return {"error": str(e)}
|
| 39 |
+
|
| 40 |
+
def _analyze_sentiment(self, text):
|
| 41 |
+
try:
|
| 42 |
+
return self.nlp(text)[0] if text else None
|
| 43 |
+
except Exception as e:
|
| 44 |
+
logger.error(f"Sentiment analysis error: {e}")
|
| 45 |
+
return None
|
| 46 |
+
|
| 47 |
+
def _extract_ocr(self, media):
|
| 48 |
+
if not media or not hasattr(media, 'type') or media.type != "image":
|
| 49 |
+
return None
|
| 50 |
+
try:
|
| 51 |
+
return pytesseract.image_to_string(media.path)
|
| 52 |
+
except Exception as e:
|
| 53 |
+
logger.error(f"OCR error: {e}")
|
| 54 |
+
return None
|
| 55 |
+
|
| 56 |
+
def _detect_objects(self, media):
|
| 57 |
+
if not media or not hasattr(media, 'type') or media.type != "image" or not self.detector:
|
| 58 |
+
return None
|
| 59 |
+
try:
|
| 60 |
+
img = cv2.imread(media.path)
|
| 61 |
+
blob = cv2.dnn.blobFromImage(img, 1/255, (416, 416), swapRB=True, crop=False)
|
| 62 |
+
self.detector.setInput(blob)
|
| 63 |
+
return self.detector.forward()
|
| 64 |
+
except Exception as e:
|
| 65 |
+
logger.error(f"Object detection error: {e}")
|
| 66 |
+
return None
|
app/services/blockchain.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from hashlib import sha256
|
| 2 |
+
import time
|
| 3 |
+
|
| 4 |
+
class AdBlockchain:
|
| 5 |
+
def __init__(self):
|
| 6 |
+
self.chain = []
|
| 7 |
+
self.create_block(proof=1, previous_hash='0')
|
| 8 |
+
|
| 9 |
+
def create_block(self, proof, previous_hash, ad_data=None):
|
| 10 |
+
block = {
|
| 11 |
+
'index': len(self.chain) + 1,
|
| 12 |
+
'timestamp': str(time.time()),
|
| 13 |
+
'proof': proof,
|
| 14 |
+
'previous_hash': previous_hash,
|
| 15 |
+
'ad_hash': self.hash_ad(ad_data)
|
| 16 |
+
}
|
| 17 |
+
self.chain.append(block)
|
| 18 |
+
return block
|
| 19 |
+
|
| 20 |
+
def hash_ad(self, ad_data):
|
| 21 |
+
return sha256(str(ad_data).encode()).hexdigest()
|
| 22 |
+
|
| 23 |
+
def is_chain_valid(self):
|
| 24 |
+
for i in range(1, len(self.chain)):
|
| 25 |
+
current_block = self.chain[i]
|
| 26 |
+
previous_block = self.chain[i - 1]
|
| 27 |
+
if current_block['previous_hash'] != self.hash_ad(previous_block):
|
| 28 |
+
return False
|
| 29 |
+
return True
|
app/services/plugin_manager.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import importlib.util
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
class PluginManager:
|
| 5 |
+
def __init__(self):
|
| 6 |
+
self.plugins = {}
|
| 7 |
+
|
| 8 |
+
def load_plugins(self, directory="plugins"):
|
| 9 |
+
for plugin_file in Path(directory).glob("*.py"):
|
| 10 |
+
spec = importlib.util.spec_from_file_location(plugin_file.stem, plugin_file)
|
| 11 |
+
module = importlib.util.module_from_spec(spec)
|
| 12 |
+
spec.loader.exec_module(module)
|
| 13 |
+
self.plugins[plugin_file.stem] = module
|
| 14 |
+
|
| 15 |
+
def run_analysis(self, ad):
|
| 16 |
+
results = {}
|
| 17 |
+
for name, plugin in self.plugins.items():
|
| 18 |
+
if hasattr(plugin, "analyze"):
|
| 19 |
+
results[name] = plugin.analyze(ad)
|
| 20 |
+
return results
|
app/services/predictive.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from prophet import Prophet
|
| 2 |
+
import numpy as np
|
| 3 |
+
from sklearn.linear_model import LinearRegression
|
| 4 |
+
|
| 5 |
+
class AdPredictor:
|
| 6 |
+
def forecast_performance(self, historical_data):
|
| 7 |
+
model = Prophet()
|
| 8 |
+
model.fit(historical_data)
|
| 9 |
+
future = model.make_future_dataframe(periods=30)
|
| 10 |
+
return model.predict(future)
|
| 11 |
+
|
| 12 |
+
def simulate_budget(self, current_spend, current_performance, proposed_spend):
|
| 13 |
+
X = np.array([current_spend]).reshape(-1, 1)
|
| 14 |
+
y = np.array([current_performance])
|
| 15 |
+
model = LinearRegression().fit(X, y)
|
| 16 |
+
return model.predict([[proposed_spend]])
|
app/services/scraper.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from selenium import webdriver
|
| 2 |
+
from selenium.webdriver.common.by import By
|
| 3 |
+
from selenium.webdriver.chrome.service import Service
|
| 4 |
+
from webdriver_manager.chrome import ChromeDriverManager
|
| 5 |
+
import time
|
| 6 |
+
from selenium.common.exceptions import TimeoutException, WebDriverException
|
| 7 |
+
from contextlib import contextmanager
|
| 8 |
+
|
| 9 |
+
class FacebookScraper:
|
| 10 |
+
def __init__(self):
|
| 11 |
+
self.driver = None
|
| 12 |
+
|
| 13 |
+
def _setup_driver(self):
|
| 14 |
+
options = webdriver.ChromeOptions()
|
| 15 |
+
options.add_argument("--headless")
|
| 16 |
+
options.add_argument("--no-sandbox")
|
| 17 |
+
options.add_argument("--disable-dev-shm-usage")
|
| 18 |
+
return webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=options)
|
| 19 |
+
|
| 20 |
+
@contextmanager
|
| 21 |
+
def _get_driver(self):
|
| 22 |
+
try:
|
| 23 |
+
self.driver = self._setup_driver()
|
| 24 |
+
yield self.driver
|
| 25 |
+
finally:
|
| 26 |
+
if self.driver:
|
| 27 |
+
self.driver.quit()
|
| 28 |
+
|
| 29 |
+
def scrape_ads(self, search_query, num_scrolls=3):
|
| 30 |
+
with self._get_driver() as driver:
|
| 31 |
+
try:
|
| 32 |
+
url = f"https://www.facebook.com/ads/library/?active_status=all&ad_type=all&country=ALL&q={search_query}&search_type=keyword"
|
| 33 |
+
driver.get(url)
|
| 34 |
+
driver.implicitly_wait(5)
|
| 35 |
+
|
| 36 |
+
ads = []
|
| 37 |
+
for _ in range(num_scrolls):
|
| 38 |
+
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
|
| 39 |
+
time.sleep(3)
|
| 40 |
+
|
| 41 |
+
ad_elements = driver.find_elements(By.CSS_SELECTOR, "div.x1yztbdb")
|
| 42 |
+
return [ad.text for ad in ad_elements if ad.text]
|
| 43 |
+
|
| 44 |
+
except (TimeoutException, WebDriverException) as e:
|
| 45 |
+
print(f"Error during scraping: {e}")
|
| 46 |
+
return []
|
app/static/css/styles.css
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* General Styles */
|
| 2 |
+
body {
|
| 3 |
+
font-family: Arial, sans-serif;
|
| 4 |
+
margin: 0;
|
| 5 |
+
padding: 0;
|
| 6 |
+
background-color: #f4f4f9;
|
| 7 |
+
color: #333;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
header {
|
| 11 |
+
background-color: #0073e6;
|
| 12 |
+
color: white;
|
| 13 |
+
padding: 1rem;
|
| 14 |
+
text-align: center;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
header h1 {
|
| 18 |
+
margin: 0;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
nav {
|
| 22 |
+
margin-top: 1rem;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
nav a {
|
| 26 |
+
color: white;
|
| 27 |
+
margin: 0 1rem;
|
| 28 |
+
text-decoration: none;
|
| 29 |
+
font-weight: bold;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
nav a:hover {
|
| 33 |
+
text-decoration: underline;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
main {
|
| 37 |
+
padding: 2rem;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
footer {
|
| 41 |
+
background-color: #333;
|
| 42 |
+
color: white;
|
| 43 |
+
text-align: center;
|
| 44 |
+
padding: 1rem;
|
| 45 |
+
position: fixed;
|
| 46 |
+
bottom: 0;
|
| 47 |
+
width: 100%;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
/* Dashboard Styles */
|
| 51 |
+
.filters {
|
| 52 |
+
margin-bottom: 2rem;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
.filters input, .filters select {
|
| 56 |
+
padding: 0.5rem;
|
| 57 |
+
margin-right: 1rem;
|
| 58 |
+
border: 1px solid #ccc;
|
| 59 |
+
border-radius: 4px;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
.filters button {
|
| 63 |
+
padding: 0.5rem 1rem;
|
| 64 |
+
background-color: #0073e6;
|
| 65 |
+
color: white;
|
| 66 |
+
border: none;
|
| 67 |
+
border-radius: 4px;
|
| 68 |
+
cursor: pointer;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.filters button:hover {
|
| 72 |
+
background-color: #005bb5;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
.ads-list {
|
| 76 |
+
display: grid;
|
| 77 |
+
gap: 1rem;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
.ad-card {
|
| 81 |
+
background-color: white;
|
| 82 |
+
padding: 1rem;
|
| 83 |
+
border: 1px solid #ddd;
|
| 84 |
+
border-radius: 4px;
|
| 85 |
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.ad-card .sentiment {
|
| 89 |
+
display: inline-block;
|
| 90 |
+
padding: 0.25rem 0.5rem;
|
| 91 |
+
border-radius: 4px;
|
| 92 |
+
font-size: 0.875rem;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.ad-card .sentiment.Positive {
|
| 96 |
+
background-color: #e6f4ea;
|
| 97 |
+
color: #34a853;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
.ad-card .sentiment.Negative {
|
| 101 |
+
background-color: #fce8e6;
|
| 102 |
+
color: #d93025;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.pagination {
|
| 106 |
+
margin-top: 2rem;
|
| 107 |
+
text-align: center;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
.pagination a {
|
| 111 |
+
padding: 0.5rem 1rem;
|
| 112 |
+
margin: 0 0.25rem;
|
| 113 |
+
background-color: #0073e6;
|
| 114 |
+
color: white;
|
| 115 |
+
text-decoration: none;
|
| 116 |
+
border-radius: 4px;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
.pagination a.active {
|
| 120 |
+
background-color: #005bb5;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
/* Compliance Report Styles */
|
| 124 |
+
table {
|
| 125 |
+
width: 100%;
|
| 126 |
+
border-collapse: collapse;
|
| 127 |
+
margin-top: 1rem;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
table th, table td {
|
| 131 |
+
padding: 0.75rem;
|
| 132 |
+
border: 1px solid #ddd;
|
| 133 |
+
text-align: left;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
table th {
|
| 137 |
+
background-color: #0073e6;
|
| 138 |
+
color: white;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
table tr:nth-child(even) {
|
| 142 |
+
background-color: #f9f9f9;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
table tr:hover {
|
| 146 |
+
background-color: #f1f1f1;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
table button {
|
| 150 |
+
padding: 0.5rem 1rem;
|
| 151 |
+
background-color: #d93025;
|
| 152 |
+
color: white;
|
| 153 |
+
border: none;
|
| 154 |
+
border-radius: 4px;
|
| 155 |
+
cursor: pointer;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
table button:hover {
|
| 159 |
+
background-color: #b31412;
|
| 160 |
+
}
|
app/static/images/logo.png
ADDED
|
app/static/js/chart.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
document.addEventListener("DOMContentLoaded", function () {
|
| 2 |
+
const ctx = document.getElementById('sentimentChart').getContext('2d');
|
| 3 |
+
const sentimentData = JSON.parse(document.getElementById('sentimentData').textContent);
|
| 4 |
+
|
| 5 |
+
new Chart(ctx, {
|
| 6 |
+
type: 'line',
|
| 7 |
+
data: {
|
| 8 |
+
labels: sentimentData.dates,
|
| 9 |
+
datasets: [{
|
| 10 |
+
label: 'Sentiment Trend',
|
| 11 |
+
data: sentimentData.scores,
|
| 12 |
+
borderColor: '#0073e6',
|
| 13 |
+
fill: false
|
| 14 |
+
}]
|
| 15 |
+
},
|
| 16 |
+
options: {
|
| 17 |
+
scales: {
|
| 18 |
+
y: {
|
| 19 |
+
beginAtZero: true
|
| 20 |
+
}
|
| 21 |
+
}
|
| 22 |
+
}
|
| 23 |
+
});
|
| 24 |
+
});
|
app/templates/base.html
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Facebook Ad Analytics</title>
|
| 7 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<header>
|
| 11 |
+
<h1>Facebook Ad Analytics</h1>
|
| 12 |
+
<nav>
|
| 13 |
+
<a href="{{ url_for('dashboard.index') }}">Dashboard</a>
|
| 14 |
+
<a href="{{ url_for('compliance.compliance_report') }}">Compliance</a>
|
| 15 |
+
<a href="{{ url_for('auth.logout') }}">Logout</a>
|
| 16 |
+
</nav>
|
| 17 |
+
</header>
|
| 18 |
+
<main>
|
| 19 |
+
{% block content %}{% endblock %}
|
| 20 |
+
</main>
|
| 21 |
+
<footer>
|
| 22 |
+
<p>© 2023 Facebook Ad Analytics</p>
|
| 23 |
+
</footer>
|
| 24 |
+
</body>
|
| 25 |
+
</html>
|
app/templates/compliance_report.html
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block content %}
|
| 4 |
+
<h2>Compliance Report</h2>
|
| 5 |
+
<table>
|
| 6 |
+
<thead>
|
| 7 |
+
<tr>
|
| 8 |
+
<th>Ad ID</th>
|
| 9 |
+
<th>Content</th>
|
| 10 |
+
<th>Sentiment</th>
|
| 11 |
+
<th>Actions</th>
|
| 12 |
+
</tr>
|
| 13 |
+
</thead>
|
| 14 |
+
<tbody>
|
| 15 |
+
{% for ad in ads %}
|
| 16 |
+
<tr>
|
| 17 |
+
<td>{{ ad.id }}</td>
|
| 18 |
+
<td>{{ ad.content }}</td>
|
| 19 |
+
<td>{{ ad.sentiment }}</td>
|
| 20 |
+
<td>
|
| 21 |
+
<button onclick="anonymizeAd('{{ ad.id }}')">Anonymize</button>
|
| 22 |
+
</td>
|
| 23 |
+
</tr>
|
| 24 |
+
{% endfor %}
|
| 25 |
+
</tbody>
|
| 26 |
+
</table>
|
| 27 |
+
|
| 28 |
+
<script>
|
| 29 |
+
function anonymizeAd(adId) {
|
| 30 |
+
fetch(`/compliance/anonymize/${adId}`, {
|
| 31 |
+
method: 'POST',
|
| 32 |
+
headers: {
|
| 33 |
+
'Content-Type': 'application/json'
|
| 34 |
+
}
|
| 35 |
+
}).then(response => response.json())
|
| 36 |
+
.then(data => {
|
| 37 |
+
if (data.status === 'success') {
|
| 38 |
+
alert('Ad anonymized successfully!');
|
| 39 |
+
location.reload();
|
| 40 |
+
}
|
| 41 |
+
});
|
| 42 |
+
}
|
| 43 |
+
</script>
|
| 44 |
+
{% endblock %}
|
app/templates/dashboard.html
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block content %}
|
| 4 |
+
<div class="filters">
|
| 5 |
+
<input type="text" name="query" placeholder="Search ads..." value="{{ query }}">
|
| 6 |
+
<select name="sentiment">
|
| 7 |
+
<option value="">All Sentiments</option>
|
| 8 |
+
<option value="Positive" {% if sentiment_filter == "Positive" %}selected{% endif %}>Positive</option>
|
| 9 |
+
<option value="Negative" {% if sentiment_filter == "Negative" %}selected{% endif %}>Negative</option>
|
| 10 |
+
</select>
|
| 11 |
+
<button type="button" onclick="applyFilters()">Apply</button>
|
| 12 |
+
</div>
|
| 13 |
+
|
| 14 |
+
<div class="ads-list">
|
| 15 |
+
{% for ad in ads.items %}
|
| 16 |
+
<div class="ad-card">
|
| 17 |
+
<p>{{ ad.content }}</p>
|
| 18 |
+
<span class="sentiment">{{ ad.sentiment }}</span>
|
| 19 |
+
</div>
|
| 20 |
+
{% endfor %}
|
| 21 |
+
</div>
|
| 22 |
+
|
| 23 |
+
<div class="pagination">
|
| 24 |
+
{% for p in range(1, ads.pages + 1) %}
|
| 25 |
+
<a href="?page={{ p }}&query={{ query }}&sentiment={{ sentiment_filter }}"
|
| 26 |
+
class="{% if p == ads.page %}active{% endif %}">{{ p }}</a>
|
| 27 |
+
{% endfor %}
|
| 28 |
+
</div>
|
| 29 |
+
|
| 30 |
+
<script>
|
| 31 |
+
function applyFilters() {
|
| 32 |
+
const query = document.querySelector('input[name="query"]').value;
|
| 33 |
+
const sentiment = document.querySelector('select[name="sentiment"]').value;
|
| 34 |
+
window.location.href = `?query=${query}&sentiment=${sentiment}`;
|
| 35 |
+
}
|
| 36 |
+
</script>
|
| 37 |
+
{% endblock %}
|
app/templates/login.html
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block content %}
|
| 4 |
+
<h2>Login</h2>
|
| 5 |
+
<form method="POST" action="{{ url_for('auth.login') }}">
|
| 6 |
+
<label for="email">Email:</label>
|
| 7 |
+
<input type="email" id="email" name="email" required>
|
| 8 |
+
|
| 9 |
+
<label for="password">Password:</label>
|
| 10 |
+
<input type="password" id="password" name="password" required>
|
| 11 |
+
|
| 12 |
+
<button type="submit">Login</button>
|
| 13 |
+
</form>
|
| 14 |
+
<p>Don't have an account? <a href="{{ url_for('auth.register') }}">Register here</a>.</p>
|
| 15 |
+
{% endblock %}
|
app/tests/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Empty file to make the directory a package
|
app/tests/test_models.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
from app.models import User, Ad, AdVersion
|
| 3 |
+
from app import db
|
| 4 |
+
|
| 5 |
+
@pytest.fixture
|
| 6 |
+
def test_user():
|
| 7 |
+
user = User(email="test@example.com")
|
| 8 |
+
user.set_password("password123")
|
| 9 |
+
return user
|
| 10 |
+
|
| 11 |
+
@pytest.fixture
|
| 12 |
+
def test_ad(test_user):
|
| 13 |
+
return Ad(content="Test ad content", user_id=test_user.id)
|
| 14 |
+
|
| 15 |
+
def test_user_creation(test_user):
|
| 16 |
+
assert test_user.email == "test@example.com"
|
| 17 |
+
assert test_user.check_password("password123")
|
| 18 |
+
|
| 19 |
+
def test_ad_creation(test_ad):
|
| 20 |
+
assert test_ad.content == "Test ad content"
|
| 21 |
+
assert test_ad.user_id is not None
|
| 22 |
+
|
| 23 |
+
def test_ad_version_creation(test_ad):
|
| 24 |
+
version = AdVersion(content="Updated ad content", ad_id=test_ad.id)
|
| 25 |
+
assert version.content == "Updated ad content"
|
| 26 |
+
assert version.ad_id == test_ad.id
|
app/tests/test_routes.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
from flask import url_for
|
| 3 |
+
from app import create_app, db
|
| 4 |
+
from app.models import User, Ad
|
| 5 |
+
|
| 6 |
+
@pytest.fixture
|
| 7 |
+
def app():
|
| 8 |
+
app = create_app()
|
| 9 |
+
app.config.update({
|
| 10 |
+
'TESTING': True,
|
| 11 |
+
'SQLALCHEMY_DATABASE_URI': 'sqlite:///:memory:',
|
| 12 |
+
'WTF_CSRF_ENABLED': False
|
| 13 |
+
})
|
| 14 |
+
with app.app_context():
|
| 15 |
+
db.create_all()
|
| 16 |
+
yield app
|
| 17 |
+
db.drop_all()
|
| 18 |
+
|
| 19 |
+
@pytest.fixture
|
| 20 |
+
def client(app):
|
| 21 |
+
return app.test_client()
|
| 22 |
+
|
| 23 |
+
def test_dashboard_route(client):
|
| 24 |
+
response = client.get(url_for('dashboard.index'))
|
| 25 |
+
assert response.status_code == 200
|
| 26 |
+
|
| 27 |
+
def test_login_route(client):
|
| 28 |
+
response = client.get(url_for('auth.login'))
|
| 29 |
+
assert response.status_code == 200
|
| 30 |
+
|
| 31 |
+
def test_compliance_report_route(client):
|
| 32 |
+
response = client.get(url_for('compliance.compliance_report'))
|
| 33 |
+
assert response.status_code == 302 # Redirects to login if not authenticated
|
app/tests/test_services.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
from app.services.ai_processor import AIPipeline
|
| 3 |
+
from app.services.scraper import FacebookScraper
|
| 4 |
+
|
| 5 |
+
def test_ai_processor():
|
| 6 |
+
ai = AIPipeline()
|
| 7 |
+
result = ai.process_ad("This is a positive ad!")
|
| 8 |
+
assert "sentiment" in result
|
| 9 |
+
|
| 10 |
+
def test_scraper():
|
| 11 |
+
scraper = FacebookScraper()
|
| 12 |
+
ads = scraper.scrape_ads("test query", num_scrolls=1)
|
| 13 |
+
assert isinstance(ads, list)
|
| 14 |
+
|
| 15 |
+
def test_blockchain():
|
| 16 |
+
from app.services.blockchain import AdBlockchain
|
| 17 |
+
blockchain = AdBlockchain()
|
| 18 |
+
block = blockchain.create_block(proof=123, previous_hash="abc")
|
| 19 |
+
assert block["index"] == 2
|
| 20 |
+
assert blockchain.is_chain_valid()
|
app/utils/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Empty file to make the directory a package
|
app/utils/decorators.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from functools import wraps
|
| 2 |
+
from flask import redirect, url_for, flash
|
| 3 |
+
from flask_login import current_user
|
| 4 |
+
|
| 5 |
+
def admin_required(f):
|
| 6 |
+
"""Decorator to restrict access to admin users only."""
|
| 7 |
+
@wraps(f)
|
| 8 |
+
def decorated_function(*args, **kwargs):
|
| 9 |
+
if not current_user.is_authenticated or current_user.role != "admin":
|
| 10 |
+
flash("You do not have permission to access this page.", "error")
|
| 11 |
+
return redirect(url_for("dashboard.index"))
|
| 12 |
+
return f(*args, **kwargs)
|
| 13 |
+
return decorated_function
|
| 14 |
+
|
| 15 |
+
def login_required_json(f):
|
| 16 |
+
"""Decorator to require login for JSON API endpoints."""
|
| 17 |
+
@wraps(f)
|
| 18 |
+
def decorated_function(*args, **kwargs):
|
| 19 |
+
if not current_user.is_authenticated:
|
| 20 |
+
return jsonify({"error": "Login required"}), 401
|
| 21 |
+
return f(*args, **kwargs)
|
| 22 |
+
return decorated_function
|
| 23 |
+
|
| 24 |
+
def cache_response(timeout=60):
|
| 25 |
+
def decorator(f):
|
| 26 |
+
@wraps(f)
|
| 27 |
+
def decorated_function(*args, **kwargs):
|
| 28 |
+
from flask import request
|
| 29 |
+
from .. import cache
|
| 30 |
+
|
| 31 |
+
cache_key = f"{request.path}:{request.query_string}"
|
| 32 |
+
cached_response = cache.get(cache_key)
|
| 33 |
+
if cached_response:
|
| 34 |
+
return cached_response
|
| 35 |
+
response = f(*args, **kwargs)
|
| 36 |
+
cache.set(cache_key, response, timeout=timeout)
|
| 37 |
+
return response
|
| 38 |
+
return decorated_function
|
| 39 |
+
return decorator
|
app/utils/helpers.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
from datetime import datetime
|
| 3 |
+
|
| 4 |
+
def format_date(date_str):
|
| 5 |
+
"""Format a date string into a human-readable format."""
|
| 6 |
+
try:
|
| 7 |
+
date_obj = datetime.strptime(date_str, "%Y-%m-%d")
|
| 8 |
+
return date_obj.strftime("%b %d, %Y")
|
| 9 |
+
except ValueError:
|
| 10 |
+
return date_str
|
| 11 |
+
|
| 12 |
+
def extract_hashtags(text):
|
| 13 |
+
"""Extract hashtags from a given text."""
|
| 14 |
+
return re.findall(r"#\w+", text)
|
| 15 |
+
|
| 16 |
+
def calculate_sentiment_score(sentiment_data):
|
| 17 |
+
"""Calculate an overall sentiment score from sentiment data."""
|
| 18 |
+
if not sentiment_data:
|
| 19 |
+
return 0
|
| 20 |
+
return sum(sentiment_data.values()) / len(sentiment_data)
|
| 21 |
+
|
| 22 |
+
def paginate(query, page, per_page=10):
|
| 23 |
+
"""Paginate a SQLAlchemy query."""
|
| 24 |
+
return query.paginate(page=page, per_page=per_page, error_out=False)
|
app/utils/validators.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
from flask import request, jsonify
|
| 3 |
+
|
| 4 |
+
def validate_email(email):
|
| 5 |
+
"""Validate an email address."""
|
| 6 |
+
regex = r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"
|
| 7 |
+
return re.match(regex, email) is not None
|
| 8 |
+
|
| 9 |
+
def validate_password(password):
|
| 10 |
+
"""Validate a password (at least 8 characters, one uppercase, one number)."""
|
| 11 |
+
if len(password) < 8:
|
| 12 |
+
return False
|
| 13 |
+
if not re.search(r"[A-Z]", password):
|
| 14 |
+
return False
|
| 15 |
+
if not re.search(r"\d", password):
|
| 16 |
+
return False
|
| 17 |
+
return True
|
| 18 |
+
|
| 19 |
+
def validate_ad_content(content):
|
| 20 |
+
"""Validate ad content (non-empty and within length limits)."""
|
| 21 |
+
if not content or len(content) > 1000:
|
| 22 |
+
return False
|
| 23 |
+
return True
|
| 24 |
+
|
| 25 |
+
def validate_request_json(required_fields):
|
| 26 |
+
"""Validate JSON request payload for required fields."""
|
| 27 |
+
data = request.get_json()
|
| 28 |
+
if not data:
|
| 29 |
+
return jsonify({"error": "Request must be JSON"}), 400
|
| 30 |
+
for field in required_fields:
|
| 31 |
+
if field not in data:
|
| 32 |
+
return jsonify({"error": f"Missing required field: {field}"}), 400
|
| 33 |
+
return None
|
celery_worker.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app import create_app, celery
|
| 2 |
+
|
| 3 |
+
app = create_app()
|
| 4 |
+
app.app_context().push()
|
check_dependencies.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
"""
|
| 3 |
+
Dependency checker script for Facebook Ad Analytics.
|
| 4 |
+
This script checks if all required dependencies are installed.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import importlib
|
| 8 |
+
import sys
|
| 9 |
+
|
| 10 |
+
REQUIRED_PACKAGES = [
|
| 11 |
+
"flask",
|
| 12 |
+
"flask_sqlalchemy",
|
| 13 |
+
"flask_login",
|
| 14 |
+
"flask_wtf",
|
| 15 |
+
"flask_migrate",
|
| 16 |
+
"werkzeug",
|
| 17 |
+
"celery",
|
| 18 |
+
"redis",
|
| 19 |
+
"selenium",
|
| 20 |
+
"transformers",
|
| 21 |
+
"numpy",
|
| 22 |
+
"cv2", # OpenCV
|
| 23 |
+
"pytesseract",
|
| 24 |
+
"gunicorn",
|
| 25 |
+
"pytest",
|
| 26 |
+
"prophet",
|
| 27 |
+
"webdriver_manager",
|
| 28 |
+
"psycopg2",
|
| 29 |
+
"click",
|
| 30 |
+
"dotenv",
|
| 31 |
+
"ratelimit",
|
| 32 |
+
]
|
| 33 |
+
|
| 34 |
+
OPTIONAL_PACKAGES = [
|
| 35 |
+
"torch",
|
| 36 |
+
"tensorflow",
|
| 37 |
+
"flax",
|
| 38 |
+
]
|
| 39 |
+
|
| 40 |
+
def check_package(package_name):
|
| 41 |
+
"""Check if a package is installed."""
|
| 42 |
+
try:
|
| 43 |
+
importlib.import_module(package_name)
|
| 44 |
+
return True
|
| 45 |
+
except ImportError:
|
| 46 |
+
return False
|
| 47 |
+
|
| 48 |
+
def main():
|
| 49 |
+
"""Main function."""
|
| 50 |
+
print("Checking required dependencies...")
|
| 51 |
+
missing_packages = []
|
| 52 |
+
|
| 53 |
+
for package in REQUIRED_PACKAGES:
|
| 54 |
+
if not check_package(package):
|
| 55 |
+
missing_packages.append(package)
|
| 56 |
+
print(f"❌ {package} is not installed")
|
| 57 |
+
else:
|
| 58 |
+
print(f"✅ {package} is installed")
|
| 59 |
+
|
| 60 |
+
print("\nChecking optional dependencies...")
|
| 61 |
+
missing_optional = []
|
| 62 |
+
|
| 63 |
+
for package in OPTIONAL_PACKAGES:
|
| 64 |
+
if not check_package(package):
|
| 65 |
+
missing_optional.append(package)
|
| 66 |
+
print(f"⚠️ {package} is not installed (optional)")
|
| 67 |
+
else:
|
| 68 |
+
print(f"✅ {package} is installed")
|
| 69 |
+
|
| 70 |
+
if missing_packages:
|
| 71 |
+
print(f"\n❌ {len(missing_packages)} required packages are missing:")
|
| 72 |
+
print(" " + ", ".join(missing_packages))
|
| 73 |
+
print("\nPlease install them using:")
|
| 74 |
+
print(f"pip install {' '.join(missing_packages)}")
|
| 75 |
+
return 1
|
| 76 |
+
else:
|
| 77 |
+
print("\n✅ All required packages are installed!")
|
| 78 |
+
|
| 79 |
+
if missing_optional:
|
| 80 |
+
print(f"\n⚠️ {len(missing_optional)} optional packages are missing:")
|
| 81 |
+
print(" " + ", ".join(missing_optional))
|
| 82 |
+
print("\nYou may want to install them for full functionality:")
|
| 83 |
+
print(f"pip install {' '.join(missing_optional)}")
|
| 84 |
+
|
| 85 |
+
return 0
|
| 86 |
+
|
| 87 |
+
if __name__ == "__main__":
|
| 88 |
+
sys.exit(main())
|
config.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
|
| 3 |
+
class Config:
|
| 4 |
+
SECRET_KEY = os.getenv('SECRET_KEY', 'your-secret-key')
|
| 5 |
+
SQLALCHEMY_DATABASE_URI = os.getenv('DATABASE_URL', 'sqlite:////tmp/app.db')
|
| 6 |
+
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
| 7 |
+
CELERY_BROKER_URL = os.getenv('CELERY_BROKER_URL', 'redis://localhost:6379/0')
|
| 8 |
+
CELERY_RESULT_BACKEND = os.getenv('CELERY_RESULT_BACKEND', 'redis://localhost:6379/0')
|
| 9 |
+
OPENAI_API_KEY = os.getenv('OPENAI_API_KEY', 'your-openai-api-key')
|
| 10 |
+
INSTANCE_PATH = '/tmp/instance' # Set a writable instance path
|
| 11 |
+
|
| 12 |
+
class DevelopmentConfig(Config):
|
| 13 |
+
DEBUG = True
|
| 14 |
+
|
| 15 |
+
class ProductionConfig(Config):
|
| 16 |
+
DEBUG = False
|
| 17 |
+
|
| 18 |
+
class TestingConfig(Config):
|
| 19 |
+
TESTING = True
|
| 20 |
+
SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: '3.8'
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
web:
|
| 5 |
+
build: .
|
| 6 |
+
ports:
|
| 7 |
+
- "5000:5000"
|
| 8 |
+
environment:
|
| 9 |
+
- FLASK_ENV=production
|
| 10 |
+
- DATABASE_URL=postgresql://postgres:example@postgres:5432/facebook_ads
|
| 11 |
+
- CELERY_BROKER_URL=redis://redis:6379/0
|
| 12 |
+
- CELERY_RESULT_BACKEND=redis://redis:6379/0
|
| 13 |
+
- INSTANCE_PATH=/tmp/instance
|
| 14 |
+
- SECRET_KEY=${SECRET_KEY}
|
| 15 |
+
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
| 16 |
+
volumes:
|
| 17 |
+
- ./app:/app/app
|
| 18 |
+
- ./app/models:/app/models
|
| 19 |
+
depends_on:
|
| 20 |
+
- redis
|
| 21 |
+
- postgres
|
| 22 |
+
- selenium-hub
|
| 23 |
+
restart: unless-stopped
|
| 24 |
+
healthcheck:
|
| 25 |
+
test: ["CMD", "/app/healthcheck.sh"]
|
| 26 |
+
interval: 30s
|
| 27 |
+
timeout: 10s
|
| 28 |
+
retries: 3
|
| 29 |
+
start_period: 40s
|
| 30 |
+
|
| 31 |
+
celery_worker:
|
| 32 |
+
build: .
|
| 33 |
+
command: celery -A celery_worker.celery worker --loglevel=info
|
| 34 |
+
environment:
|
| 35 |
+
- FLASK_ENV=production
|
| 36 |
+
- DATABASE_URL=postgresql://postgres:example@postgres:5432/facebook_ads
|
| 37 |
+
- CELERY_BROKER_URL=redis://redis:6379/0
|
| 38 |
+
- CELERY_RESULT_BACKEND=redis://redis:6379/0
|
| 39 |
+
- INSTANCE_PATH=/tmp/instance
|
| 40 |
+
- SECRET_KEY=${SECRET_KEY}
|
| 41 |
+
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
| 42 |
+
volumes:
|
| 43 |
+
- ./app:/app/app
|
| 44 |
+
depends_on:
|
| 45 |
+
- web
|
| 46 |
+
- redis
|
| 47 |
+
- postgres
|
| 48 |
+
restart: unless-stopped
|
| 49 |
+
|
| 50 |
+
selenium-hub:
|
| 51 |
+
image: selenium/hub
|
| 52 |
+
ports:
|
| 53 |
+
- "4444:4444"
|
| 54 |
+
restart: unless-stopped
|
| 55 |
+
|
| 56 |
+
chrome-node:
|
| 57 |
+
image: selenium/node-chrome
|
| 58 |
+
shm_size: 2gb
|
| 59 |
+
depends_on:
|
| 60 |
+
- selenium-hub
|
| 61 |
+
environment:
|
| 62 |
+
- HUB_HOST=selenium-hub
|
| 63 |
+
- HUB_PORT=4444
|
| 64 |
+
restart: unless-stopped
|
| 65 |
+
|
| 66 |
+
redis:
|
| 67 |
+
image: redis:alpine
|
| 68 |
+
ports:
|
| 69 |
+
- "6379:6379"
|
| 70 |
+
volumes:
|
| 71 |
+
- redis_data:/data
|
| 72 |
+
restart: unless-stopped
|
| 73 |
+
healthcheck:
|
| 74 |
+
test: ["CMD", "redis-cli", "ping"]
|
| 75 |
+
interval: 30s
|
| 76 |
+
timeout: 10s
|
| 77 |
+
retries: 3
|
| 78 |
+
|
| 79 |
+
postgres:
|
| 80 |
+
image: postgres:14
|
| 81 |
+
environment:
|
| 82 |
+
- POSTGRES_PASSWORD=example
|
| 83 |
+
- POSTGRES_DB=facebook_ads
|
| 84 |
+
volumes:
|
| 85 |
+
- postgres_data:/var/lib/postgresql/data
|
| 86 |
+
ports:
|
| 87 |
+
- "5432:5432"
|
| 88 |
+
restart: unless-stopped
|
| 89 |
+
healthcheck:
|
| 90 |
+
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
| 91 |
+
interval: 30s
|
| 92 |
+
timeout: 10s
|
| 93 |
+
retries: 3
|
| 94 |
+
|
| 95 |
+
volumes:
|
| 96 |
+
postgres_data:
|
| 97 |
+
redis_data:
|
gunicorn.conf.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Force specific number of workers regardless of CPU count
|
| 2 |
+
def get_workers():
|
| 3 |
+
return 2
|
| 4 |
+
|
| 5 |
+
workers = get_workers()
|
| 6 |
+
|
| 7 |
+
# Worker configuration
|
| 8 |
+
worker_class = 'sync'
|
| 9 |
+
timeout = 120
|
| 10 |
+
graceful_timeout = 30
|
| 11 |
+
keep_alive = 5
|
| 12 |
+
|
| 13 |
+
# Request Handling
|
| 14 |
+
max_requests = 1000
|
| 15 |
+
max_requests_jitter = 50
|
| 16 |
+
|
| 17 |
+
# Server Socket
|
| 18 |
+
bind = '0.0.0.0:5000'
|
| 19 |
+
|
| 20 |
+
# Logging
|
| 21 |
+
loglevel = 'debug'
|
| 22 |
+
accesslog = '-'
|
| 23 |
+
errorlog = '-'
|
| 24 |
+
capture_output = True
|
| 25 |
+
|
| 26 |
+
# Ensure the configuration is not overridden
|
| 27 |
+
def post_worker_init(worker):
|
| 28 |
+
import logging
|
| 29 |
+
logging.debug(f"Worker initialized with configuration: workers={workers}")
|
hf_env/Lib/site-packages/PyYAML-6.0.2.dist-info/INSTALLER
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
pip
|
hf_env/Lib/site-packages/PyYAML-6.0.2.dist-info/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Copyright (c) 2017-2021 Ingy döt Net
|
| 2 |
+
Copyright (c) 2006-2016 Kirill Simonov
|
| 3 |
+
|
| 4 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
| 5 |
+
this software and associated documentation files (the "Software"), to deal in
|
| 6 |
+
the Software without restriction, including without limitation the rights to
|
| 7 |
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
| 8 |
+
of the Software, and to permit persons to whom the Software is furnished to do
|
| 9 |
+
so, subject to the following conditions:
|
| 10 |
+
|
| 11 |
+
The above copyright notice and this permission notice shall be included in all
|
| 12 |
+
copies or substantial portions of the Software.
|
| 13 |
+
|
| 14 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 15 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 16 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 17 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 18 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 19 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 20 |
+
SOFTWARE.
|
hf_env/Lib/site-packages/PyYAML-6.0.2.dist-info/METADATA
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Metadata-Version: 2.1
|
| 2 |
+
Name: PyYAML
|
| 3 |
+
Version: 6.0.2
|
| 4 |
+
Summary: YAML parser and emitter for Python
|
| 5 |
+
Home-page: https://pyyaml.org/
|
| 6 |
+
Download-URL: https://pypi.org/project/PyYAML/
|
| 7 |
+
Author: Kirill Simonov
|
| 8 |
+
Author-email: xi@resolvent.net
|
| 9 |
+
License: MIT
|
| 10 |
+
Project-URL: Bug Tracker, https://github.com/yaml/pyyaml/issues
|
| 11 |
+
Project-URL: CI, https://github.com/yaml/pyyaml/actions
|
| 12 |
+
Project-URL: Documentation, https://pyyaml.org/wiki/PyYAMLDocumentation
|
| 13 |
+
Project-URL: Mailing lists, http://lists.sourceforge.net/lists/listinfo/yaml-core
|
| 14 |
+
Project-URL: Source Code, https://github.com/yaml/pyyaml
|
| 15 |
+
Platform: Any
|
| 16 |
+
Classifier: Development Status :: 5 - Production/Stable
|
| 17 |
+
Classifier: Intended Audience :: Developers
|
| 18 |
+
Classifier: License :: OSI Approved :: MIT License
|
| 19 |
+
Classifier: Operating System :: OS Independent
|
| 20 |
+
Classifier: Programming Language :: Cython
|
| 21 |
+
Classifier: Programming Language :: Python
|
| 22 |
+
Classifier: Programming Language :: Python :: 3
|
| 23 |
+
Classifier: Programming Language :: Python :: 3.8
|
| 24 |
+
Classifier: Programming Language :: Python :: 3.9
|
| 25 |
+
Classifier: Programming Language :: Python :: 3.10
|
| 26 |
+
Classifier: Programming Language :: Python :: 3.11
|
| 27 |
+
Classifier: Programming Language :: Python :: 3.12
|
| 28 |
+
Classifier: Programming Language :: Python :: 3.13
|
| 29 |
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
| 30 |
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
| 31 |
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
| 32 |
+
Classifier: Topic :: Text Processing :: Markup
|
| 33 |
+
Requires-Python: >=3.8
|
| 34 |
+
License-File: LICENSE
|
| 35 |
+
|
| 36 |
+
YAML is a data serialization format designed for human readability
|
| 37 |
+
and interaction with scripting languages. PyYAML is a YAML parser
|
| 38 |
+
and emitter for Python.
|
| 39 |
+
|
| 40 |
+
PyYAML features a complete YAML 1.1 parser, Unicode support, pickle
|
| 41 |
+
support, capable extension API, and sensible error messages. PyYAML
|
| 42 |
+
supports standard YAML tags and provides Python-specific tags that
|
| 43 |
+
allow to represent an arbitrary Python object.
|
| 44 |
+
|
| 45 |
+
PyYAML is applicable for a broad range of tasks from complex
|
| 46 |
+
configuration files to object serialization and persistence.
|
hf_env/Lib/site-packages/PyYAML-6.0.2.dist-info/RECORD
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
PyYAML-6.0.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
| 2 |
+
PyYAML-6.0.2.dist-info/LICENSE,sha256=jTko-dxEkP1jVwfLiOsmvXZBAqcoKVQwfT5RZ6V36KQ,1101
|
| 3 |
+
PyYAML-6.0.2.dist-info/METADATA,sha256=9lwXqTOrXPts-jI2Lo5UwuaAYo0hiRA0BZqjch0WjAk,2106
|
| 4 |
+
PyYAML-6.0.2.dist-info/RECORD,,
|
| 5 |
+
PyYAML-6.0.2.dist-info/WHEEL,sha256=c7SWG1_hRvc9HXHEkmWlTu1Jr4WpzRucfzqTP-_8q0s,102
|
| 6 |
+
PyYAML-6.0.2.dist-info/top_level.txt,sha256=rpj0IVMTisAjh_1vG3Ccf9v5jpCQwAz6cD1IVU5ZdhQ,11
|
| 7 |
+
_yaml/__init__.py,sha256=04Ae_5osxahpJHa3XBZUAf4wi6XX32gR8D6X6p64GEA,1402
|
| 8 |
+
_yaml/__pycache__/__init__.cpython-312.pyc,,
|
| 9 |
+
yaml/__init__.py,sha256=N35S01HMesFTe0aRRMWkPj0Pa8IEbHpE9FK7cr5Bdtw,12311
|
| 10 |
+
yaml/__pycache__/__init__.cpython-312.pyc,,
|
| 11 |
+
yaml/__pycache__/composer.cpython-312.pyc,,
|
| 12 |
+
yaml/__pycache__/constructor.cpython-312.pyc,,
|
| 13 |
+
yaml/__pycache__/cyaml.cpython-312.pyc,,
|
| 14 |
+
yaml/__pycache__/dumper.cpython-312.pyc,,
|
| 15 |
+
yaml/__pycache__/emitter.cpython-312.pyc,,
|
| 16 |
+
yaml/__pycache__/error.cpython-312.pyc,,
|
| 17 |
+
yaml/__pycache__/events.cpython-312.pyc,,
|
| 18 |
+
yaml/__pycache__/loader.cpython-312.pyc,,
|
| 19 |
+
yaml/__pycache__/nodes.cpython-312.pyc,,
|
| 20 |
+
yaml/__pycache__/parser.cpython-312.pyc,,
|
| 21 |
+
yaml/__pycache__/reader.cpython-312.pyc,,
|
| 22 |
+
yaml/__pycache__/representer.cpython-312.pyc,,
|
| 23 |
+
yaml/__pycache__/resolver.cpython-312.pyc,,
|
| 24 |
+
yaml/__pycache__/scanner.cpython-312.pyc,,
|
| 25 |
+
yaml/__pycache__/serializer.cpython-312.pyc,,
|
| 26 |
+
yaml/__pycache__/tokens.cpython-312.pyc,,
|
| 27 |
+
yaml/_yaml.cp312-win_amd64.pyd,sha256=Bx7e_LEQx7cnd1_A9_nClp3X77g-_Lw1aoAAtYZbwWk,263680
|
| 28 |
+
yaml/composer.py,sha256=_Ko30Wr6eDWUeUpauUGT3Lcg9QPBnOPVlTnIMRGJ9FM,4883
|
| 29 |
+
yaml/constructor.py,sha256=kNgkfaeLUkwQYY_Q6Ff1Tz2XVw_pG1xVE9Ak7z-viLA,28639
|
| 30 |
+
yaml/cyaml.py,sha256=6ZrAG9fAYvdVe2FK_w0hmXoG7ZYsoYUwapG8CiC72H0,3851
|
| 31 |
+
yaml/dumper.py,sha256=PLctZlYwZLp7XmeUdwRuv4nYOZ2UBnDIUy8-lKfLF-o,2837
|
| 32 |
+
yaml/emitter.py,sha256=jghtaU7eFwg31bG0B7RZea_29Adi9CKmXq_QjgQpCkQ,43006
|
| 33 |
+
yaml/error.py,sha256=Ah9z-toHJUbE9j-M8YpxgSRM5CgLCcwVzJgLLRF2Fxo,2533
|
| 34 |
+
yaml/events.py,sha256=50_TksgQiE4up-lKo_V-nBy-tAIxkIPQxY5qDhKCeHw,2445
|
| 35 |
+
yaml/loader.py,sha256=UVa-zIqmkFSCIYq_PgSGm4NSJttHY2Rf_zQ4_b1fHN0,2061
|
| 36 |
+
yaml/nodes.py,sha256=gPKNj8pKCdh2d4gr3gIYINnPOaOxGhJAUiYhGRnPE84,1440
|
| 37 |
+
yaml/parser.py,sha256=ilWp5vvgoHFGzvOZDItFoGjD6D42nhlZrZyjAwa0oJo,25495
|
| 38 |
+
yaml/reader.py,sha256=0dmzirOiDG4Xo41RnuQS7K9rkY3xjHiVasfDMNTqCNw,6794
|
| 39 |
+
yaml/representer.py,sha256=IuWP-cAW9sHKEnS0gCqSa894k1Bg4cgTxaDwIcbRQ-Y,14190
|
| 40 |
+
yaml/resolver.py,sha256=9L-VYfm4mWHxUD1Vg4X7rjDRK_7VZd6b92wzq7Y2IKY,9004
|
| 41 |
+
yaml/scanner.py,sha256=YEM3iLZSaQwXcQRg2l2R4MdT0zGP2F9eHkKGKnHyWQY,51279
|
| 42 |
+
yaml/serializer.py,sha256=ChuFgmhU01hj4xgI8GaKv6vfM2Bujwa9i7d2FAHj7cA,4165
|
| 43 |
+
yaml/tokens.py,sha256=lTQIzSVw8Mg9wv459-TjiOQe6wVziqaRlqX2_89rp54,2573
|
hf_env/Lib/site-packages/PyYAML-6.0.2.dist-info/WHEEL
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Wheel-Version: 1.0
|
| 2 |
+
Generator: bdist_wheel (0.44.0)
|
| 3 |
+
Root-Is-Purelib: false
|
| 4 |
+
Tag: cp312-cp312-win_amd64
|
| 5 |
+
|
hf_env/Lib/site-packages/PyYAML-6.0.2.dist-info/top_level.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
_yaml
|
| 2 |
+
yaml
|
hf_env/Lib/site-packages/__pycache__/typing_extensions.cpython-312.pyc
ADDED
|
Binary file (139 kB). View file
|
|
|
hf_env/Lib/site-packages/_yaml/__init__.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# This is a stub package designed to roughly emulate the _yaml
|
| 2 |
+
# extension module, which previously existed as a standalone module
|
| 3 |
+
# and has been moved into the `yaml` package namespace.
|
| 4 |
+
# It does not perfectly mimic its old counterpart, but should get
|
| 5 |
+
# close enough for anyone who's relying on it even when they shouldn't.
|
| 6 |
+
import yaml
|
| 7 |
+
|
| 8 |
+
# in some circumstances, the yaml module we imoprted may be from a different version, so we need
|
| 9 |
+
# to tread carefully when poking at it here (it may not have the attributes we expect)
|
| 10 |
+
if not getattr(yaml, '__with_libyaml__', False):
|
| 11 |
+
from sys import version_info
|
| 12 |
+
|
| 13 |
+
exc = ModuleNotFoundError if version_info >= (3, 6) else ImportError
|
| 14 |
+
raise exc("No module named '_yaml'")
|
| 15 |
+
else:
|
| 16 |
+
from yaml._yaml import *
|
| 17 |
+
import warnings
|
| 18 |
+
warnings.warn(
|
| 19 |
+
'The _yaml extension module is now located at yaml._yaml'
|
| 20 |
+
' and its location is subject to change. To use the'
|
| 21 |
+
' LibYAML-based parser and emitter, import from `yaml`:'
|
| 22 |
+
' `from yaml import CLoader as Loader, CDumper as Dumper`.',
|
| 23 |
+
DeprecationWarning
|
| 24 |
+
)
|
| 25 |
+
del warnings
|
| 26 |
+
# Don't `del yaml` here because yaml is actually an existing
|
| 27 |
+
# namespace member of _yaml.
|
| 28 |
+
|
| 29 |
+
__name__ = '_yaml'
|
| 30 |
+
# If the module is top-level (i.e. not a part of any specific package)
|
| 31 |
+
# then the attribute should be set to ''.
|
| 32 |
+
# https://docs.python.org/3.8/library/types.html
|
| 33 |
+
__package__ = ''
|
hf_env/Lib/site-packages/_yaml/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (858 Bytes). View file
|
|
|
hf_env/Lib/site-packages/certifi-2025.1.31.dist-info/INSTALLER
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
pip
|