rastof9 commited on
Commit
d488241
·
1 Parent(s): a104983

Saving local changes before rebase

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env.example +6 -0
  2. .gitignore +3 -0
  3. Dockerfile +56 -0
  4. README.md +135 -8
  5. app.py +6 -0
  6. app/__init__.py +37 -0
  7. app/__pycache__/__init__.cpython-312.pyc +0 -0
  8. app/forms.py +15 -0
  9. app/models.py +22 -0
  10. app/routes/__init__.py +1 -0
  11. app/routes/api.py +61 -0
  12. app/routes/auth.py +35 -0
  13. app/routes/compliance.py +27 -0
  14. app/routes/dashboard.py +21 -0
  15. app/scripts/setup_yolo.py +57 -0
  16. app/services/ai_processor.py +66 -0
  17. app/services/blockchain.py +29 -0
  18. app/services/plugin_manager.py +20 -0
  19. app/services/predictive.py +16 -0
  20. app/services/scraper.py +46 -0
  21. app/static/css/styles.css +160 -0
  22. app/static/images/logo.png +0 -0
  23. app/static/js/chart.js +24 -0
  24. app/templates/base.html +25 -0
  25. app/templates/compliance_report.html +44 -0
  26. app/templates/dashboard.html +37 -0
  27. app/templates/login.html +15 -0
  28. app/tests/__init__.py +1 -0
  29. app/tests/test_models.py +26 -0
  30. app/tests/test_routes.py +33 -0
  31. app/tests/test_services.py +20 -0
  32. app/utils/__init__.py +1 -0
  33. app/utils/decorators.py +39 -0
  34. app/utils/helpers.py +24 -0
  35. app/utils/validators.py +33 -0
  36. celery_worker.py +4 -0
  37. check_dependencies.py +88 -0
  38. config.py +20 -0
  39. docker-compose.yml +97 -0
  40. gunicorn.conf.py +29 -0
  41. hf_env/Lib/site-packages/PyYAML-6.0.2.dist-info/INSTALLER +1 -0
  42. hf_env/Lib/site-packages/PyYAML-6.0.2.dist-info/LICENSE +20 -0
  43. hf_env/Lib/site-packages/PyYAML-6.0.2.dist-info/METADATA +46 -0
  44. hf_env/Lib/site-packages/PyYAML-6.0.2.dist-info/RECORD +43 -0
  45. hf_env/Lib/site-packages/PyYAML-6.0.2.dist-info/WHEEL +5 -0
  46. hf_env/Lib/site-packages/PyYAML-6.0.2.dist-info/top_level.txt +2 -0
  47. hf_env/Lib/site-packages/__pycache__/typing_extensions.cpython-312.pyc +0 -0
  48. hf_env/Lib/site-packages/_yaml/__init__.py +33 -0
  49. hf_env/Lib/site-packages/_yaml/__pycache__/__init__.cpython-312.pyc +0 -0
  50. 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
- short_description: ads analitics
 
 
 
 
 
 
10
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
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>&copy; 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