Skip to content

Commit 44861d1

Browse files
committed
airflow added
1 parent abfb84a commit 44861d1

10 files changed

Lines changed: 383 additions & 74 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ __pycache__/
22
.venv
33
.env
44
env
5+
Evently_notes.md
6+
struct_events/coords_cache.json

Dockerfile.airflow

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
FROM apache/airflow:2.8.1
2+
COPY requirements.airflow.txt .
3+
RUN pip install --no-cache-dir -r requirements.airflow.txt

README.md

Lines changed: 140 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,32 @@
1-
# Evently – Event Scraper + FastAPI + Elasticsearch + GitHub Actions Cron
1+
# Evently – Event Scraper + FastAPI + Elasticsearch + Airflow / Github Actions
22

3-
Evently is a platform that scrapes event data from multiple Moroccan sources, enriches each event with nearby bus and tramway transport info, indexes everything into Elasticsearch, and serves it through a FastAPI backend with a frontend UI.
4-
All scraping and indexing are automated using GitHub Actions on a scheduled cron job.
3+
Evently is a platform that scrapes event data from multiple Moroccan sources, geocodes each event venue using **Nominatim (OpenStreetMap)**, enriches each event with nearby bus and tramway transport info, indexes everything into Elasticsearch, and serves it through a FastAPI backend with a frontend UI.
4+
5+
Scraping and indexing are automated on a daily scheduled cron job at midnight,
6+
with **Slack alerts** on failure.
7+
8+
## Scheduling
9+
- **Apache Airflow** — used for local/self-hosted deployments (runs inside Docker)
10+
- **GitHub Actions** — alternative for cloud deployments using Elastic Cloud
511

612
---
713

814
## 📌 Features
915

1016
### 🔍 Scraping
11-
- Collects events in parallel from multiple sources (Casaevents, Eventbrite, Events.ma, Guichet)
17+
- Collects events in parallel from multiple Moroccan sources
1218
- Parallel scraping for faster data collection
1319

20+
### 📍 Geocoding
21+
- Each event venue is geocoded using the **Nominatim API** (OpenStreetMap) via `geopy`
22+
- Converts venue names and addresses into **coordinates (longitude, latitude)**
23+
- Coordinates are used downstream for transport enrichment via KDTree spatial indexing
24+
1425
### 🚌 Transport Integration
1526
- Each event is automatically enriched with nearby **Casabus** and **Tramway** lines at indexing time
16-
- Uses a **KDTree spatial index** for near-instant transport lookups
17-
- Transport data is precomputed and stored directly in the event document — no per-request calculation
27+
- Uses a **KDTree spatial index** built from precomputed local transport data to find nearest lines
28+
- Transport data is fetched **once** from the **Overpass API** (OpenStreetMap), stored locally as JSON, and reused at every indexing run — no live API calls during the pipeline
29+
- Transport lines are stored directly in the event document — no per-request spatial computation
1830

1931
### 🧠 Elasticsearch
2032
- Events are indexed with duplicate prevention using hashed unique IDs (MD5)
@@ -43,10 +55,20 @@ All scraping and indexing are automated using GitHub Actions on a scheduled cron
4355

4456
### 🎨 Frontend
4557
- Dynamic event loading from API
46-
- Pagination, search, and filter support
47-
48-
### 🔄 GitHub Actions Automation
49-
A scheduled workflow runs all scrapers and Elasticsearch indexing automatically on a cron schedule.
58+
- Search bar with debounce and clear button
59+
- Filters panel: city, category, date, upcoming only, sort
60+
- Grid and list view toggle
61+
- Event detail modal with transport info
62+
- Favorites system with save/remove
63+
- Share events via Twitter, Facebook, WhatsApp, or copy link
64+
- Pagination with ellipsis
65+
66+
### 🔄 Airflow Scheduling
67+
- Daily scraping and indexing triggered automatically at **midnight**
68+
- DAG: `evently_daily_scraping`
69+
- Retries up to **2 times** on failure with a 5 minute delay
70+
- **Slack notifications** on both success and failure
71+
- Airflow UI accessible at `http://localhost:8080`
5072

5173
---
5274

@@ -56,85 +78,112 @@ A scheduled workflow runs all scrapers and Elasticsearch indexing automatically
5678
Events_Project/
5779
5880
├── elastic/
59-
│ ├── elastic_script.py # indexing + transport enrichment
60-
│ └── elastic_client.py # Elasticsearch client setup
81+
│ ├── elastic_script.py # indexing + transport enrichment
82+
│ └── elastic_client.py # Elasticsearch client setup
6183
62-
├── main.py # FastAPI routes + lifespan startup
84+
├── main.py # FastAPI routes + lifespan startup
6385
6486
├── static/
6587
│ ├── script.js
6688
│ └── style.css
6789
6890
├── templates/
69-
│ └── index.html # Frontend UI
91+
│ └── index.html # Frontend UI
7092
71-
├── requirements.txt
72-
├── .env # Environment variables (not committed)
93+
├── requirements.txt # FastAPI app dependencies
94+
├── requirements.airflow.txt # Airflow container dependencies
95+
├── .env # Environment variables (not committed)
7396
74-
├── .github/
75-
│ └── workflows/
76-
│ └── build.yml # GitHub Actions cron workflow
97+
├── Dockerfile # FastAPI container
98+
├── Dockerfile.airflow # Airflow container with scraping deps
99+
── docker-compose.yml # All services
77100
78-
├── Dockerfile
79-
── docker-compose.yml
101+
├── dags/
102+
│ └── evently_scraping.py # Airflow DAG — daily scraping pipeline
80103
81-
├── scrape/ # Scrapers (run in parallel)
104+
├── scrape/ # Scrapers (run in parallel)
82105
│ ├── casaevents.py
83106
│ ├── eventbrit.py
84107
│ ├── eventsma.py
85108
│ └── guichet.py
86109
87110
├── struct_events/
88-
│ ├── get_coords.py # Geocoding utility
89-
│ └── models.py # Events Pydantic model
111+
│ ├── get_coords.py # Nominatim geocoding (venue → coordinates)
112+
│ └── models.py # Events Pydantic model
90113
91114
├── transport/
92-
│ ├── bus.py # KDTree transport lookup
93-
│ ├── cleaned_lines/
94-
│ └── overpass/
115+
│ ├── bus.py # KDTree transport lookup
116+
│ ├── cleaned_lines/ # Preprocessed transport data (JSON)
117+
│ └── overpass/ # Raw Overpass API data (one-time fetch)
95118
```
96119

97120
---
98121

99122
## ⚙️ Installation & Setup
100123

101-
### 1️⃣ Pull the Docker image OR Clone the repo
124+
### 1️⃣ Clone the repo
102125

103-
```bash
104-
docker pull chaimaaeljerrar/scraping-image:latest
105-
```
106-
OR
107126
```bash
108127
git clone <repo_url>
128+
cd Events_Project
109129
```
110130

111-
### 2️⃣ (Optional) Create a `.env` file
112-
113-
Only needed if connecting to Elastic Cloud or if security is enabled:
131+
### 2️⃣ Create a `.env` file
114132

115-
```
133+
```env
116134
ELASTIC_PASSWORD=your_password_here
117-
CLOUD_ID=your_cloud_id_here
135+
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/XXX/YYY/ZZZ
118136
```
119137

120-
> When running locally with Docker Compose, Elasticsearch security is disabled by default — no `.env` is required.
138+
> `SLACK_WEBHOOK_URL` is required for Airflow failure alerts. Get it from https://api.slack.com/apps
121139
122-
### 3️⃣ Build and run the container
140+
### 3️⃣ Build and run all services
123141

124142
```bash
125-
docker compose up
143+
docker compose up --build
126144
```
127145

128-
The app will:
129-
1. Wait for Elasticsearch to be healthy
130-
2. Automatically start scraping and indexing events in the background
131-
3. Serve the frontend at `http://localhost:8000`
146+
This will start:
147+
- **Elasticsearch** at `http://localhost:9200`
148+
- **FastAPI** at `http://localhost:8000`
149+
- **Airflow webserver** at `http://localhost:8080`
150+
- **Airflow scheduler** running the daily DAG
151+
- **Postgres** for Airflow metadata (internal)
152+
153+
> Wait about 60 seconds on first run for all services to initialize.
132154
133155
### 4️⃣ Access the app
134156

135-
```
136-
http://localhost:8000
137-
```
157+
| Service | URL |
158+
|---------|-----|
159+
| Frontend | http://localhost:8000 |
160+
| Airflow UI | http://localhost:8080 |
161+
| Elasticsearch | http://localhost:9200 |
162+
163+
---
164+
165+
## 🔄 Airflow Setup
166+
167+
### Login to Airflow UI
168+
- URL: `http://localhost:8080`
169+
- Username: `admin`
170+
- Password: `admin123`
171+
172+
### Activate the DAG
173+
1. Find `evently_daily_scraping` in the DAGs list
174+
2. Toggle it **ON** (it's paused by default)
175+
3. Click ▶️ to trigger a manual run first to verify everything works
176+
4. After that it runs automatically every night at **midnight**
177+
178+
### DAG Details
179+
180+
| Property | Value |
181+
|----------|-------|
182+
| DAG ID | `evently_daily_scraping` |
183+
| Schedule | Daily at midnight (`0 0 * * *`) |
184+
| Retries | 2 (5 min delay between retries) |
185+
| Slack alert on success ||
186+
| Slack alert on failure ||
138187

139188
---
140189

@@ -152,15 +201,53 @@ http://localhost:8000/health
152201

153202
# Manually trigger reindex
154203
http://localhost:8000/reindex
204+
205+
# Check Airflow
206+
http://localhost:8080
155207
```
156208

157209
---
158210

159-
## 🔧 How Transport Works
211+
## 🔧 How the Pipeline Works
160212

161-
At indexing time, each event's coordinates are used to query a **KDTree** built from all bus and tramway stop coordinates. The nearest lines (within 0.5km) are stored directly on the event document in Elasticsearch.
213+
```
214+
Scrapers (parallel)
215+
216+
Nominatim API — geocode venue name → coordinates
217+
218+
KDTree spatial index — coordinates → nearest bus/tramway lines
219+
(from precomputed local JSON, originally fetched from Overpass API)
220+
221+
Elasticsearch — event + coordinates + transport lines indexed together
222+
223+
FastAPI — serves events, search, filters, transport, favorites
224+
225+
Frontend UI — search, modal, pagination, favorites, share
226+
```
227+
228+
At indexing time, each event's venue is geocoded to coordinates, which are used to query a **KDTree** built from all bus and tramway stop coordinates. The nearest lines within 0.5km are stored directly on the event document in Elasticsearch.
162229

163-
This means `/transport/{event_id}` is just a simple document read — no spatial computation happens at request time.
230+
This means `/transport/{event_id}` is just a simple document read — no spatial computation or API call happens at request time.
231+
232+
---
233+
234+
## 🐳 Docker Commands Reference
235+
236+
```bash
237+
# First run or after changing Dockerfile/requirements
238+
docker compose up --build
239+
240+
# After changing only Python/JS/HTML files
241+
docker compose up
242+
243+
# Full clean restart (wipes data volumes)
244+
docker compose down -v
245+
docker compose up --build
246+
247+
# View logs for a specific service
248+
docker compose logs airflow-scheduler
249+
docker compose logs fastapi
250+
```
164251

165252
---
166253

@@ -170,4 +257,6 @@ This means `/transport/{event_id}` is just a simple document read — no spatial
170257
2. Store favorites in Elasticsearch instead of in-memory
171258
3. Add AI-based event deduplication
172259
4. Add user authentication
173-
5. Deploy to cloud with security enabled
260+
5. Deploy to cloud with security enabled
261+
6. Add map view with Leaflet.js
262+
7. Add more Moroccan event sources

dags/evently_scraping.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from airflow import DAG
2+
from airflow.operators.python import PythonOperator
3+
from airflow.hooks.base import BaseHook
4+
from datetime import datetime, timedelta
5+
import requests
6+
import os
7+
import sys
8+
9+
sys.path.insert(0, '/opt/airflow')
10+
11+
default_args = {
12+
'owner': 'evently',
13+
'retries': 2,
14+
'retry_delay': timedelta(minutes=5),
15+
}
16+
17+
def run_indexing():
18+
from elastic.elastic_script import indexing
19+
result = indexing()
20+
if not result:
21+
raise ValueError("Indexing returned no results — possible scraping failure")
22+
return result
23+
24+
def send_slack_success(context):
25+
webhook_url = os.environ.get('SLACK_WEBHOOK_URL')
26+
if not webhook_url:
27+
return
28+
requests.post(webhook_url, json={
29+
"text": f"*Evently scraping succeeded!*\nRun: `{context['ds']}`\nAll sources indexed successfully."
30+
})
31+
32+
def send_slack_failure(context):
33+
webhook_url = os.environ.get('SLACK_WEBHOOK_URL')
34+
if not webhook_url:
35+
return
36+
exception = context.get('exception', 'Unknown error')
37+
requests.post(webhook_url, json={
38+
"text": f"*Evently scraping FAILED!!!!*\nRun: `{context['ds']}`\nError: `{exception}`\nCheck Airflow at http://localhost:8080"
39+
})
40+
41+
with DAG(
42+
dag_id='evently_daily_scraping',
43+
default_args=default_args,
44+
description='Daily scraping and indexing of Evently events',
45+
schedule_interval='0 0 * * *',
46+
start_date=datetime(2026, 1, 1),
47+
catchup=False,
48+
on_success_callback=send_slack_success,
49+
on_failure_callback=send_slack_failure,
50+
tags=['evently', 'scraping']
51+
) as dag:
52+
53+
scrape_and_index = PythonOperator(
54+
task_id='scrape_and_index_events',
55+
python_callable=run_indexing,
56+
)

0 commit comments

Comments
 (0)