Skip to content

Commit ceba3bc

Browse files
committed
init commit
0 parents  commit ceba3bc

12 files changed

Lines changed: 517 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
7+
jobs:
8+
build:
9+
10+
runs-on: ubuntu-latest
11+
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- name: Build Docker Compose
16+
run: docker compose build

Dockerfile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
FROM python:3.12-slim
2+
3+
WORKDIR /app
4+
5+
COPY requirements.txt .
6+
RUN pip install -r requirements.txt
7+
8+
COPY . .
9+
10+
CMD ["python", "app/main.py"]

app/main.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
from flask import Flask, redirect, url_for, render_template
2+
from sqlalchemy import create_engine, text
3+
import subprocess
4+
5+
app = Flask(__name__)
6+
7+
DATABASE_URL = "postgresql://admin:admin@postgres:5432/newsdb"
8+
9+
engine = create_engine(DATABASE_URL)
10+
11+
12+
@app.route('/')
13+
def home():
14+
15+
with engine.connect() as conn:
16+
17+
result = conn.execute(text("""
18+
SELECT *
19+
FROM news
20+
ORDER BY id DESC
21+
"""))
22+
23+
rows = result.mappings().all()
24+
25+
return render_template(
26+
"index.html",
27+
rows=rows
28+
)
29+
30+
31+
@app.route('/run-parser', methods=['POST'])
32+
def run_parser():
33+
34+
subprocess.Popen([
35+
"python",
36+
"parsers/load_data.py"
37+
])
38+
39+
return redirect(url_for('home'))
40+
41+
42+
@app.route('/update-scores', methods=['POST'])
43+
def update_scores():
44+
45+
subprocess.Popen([
46+
"python",
47+
"parsers/update_scores.py"
48+
])
49+
50+
return redirect(url_for('home'))
51+
52+
53+
@app.route('/delete/<int:news_id>', methods=['POST'])
54+
def delete_news(news_id):
55+
56+
with engine.begin() as conn:
57+
58+
conn.execute(
59+
text("""
60+
DELETE FROM news
61+
WHERE id = :id
62+
"""),
63+
{
64+
"id": news_id
65+
}
66+
)
67+
68+
return redirect(url_for('home'))
69+
70+
71+
@app.route('/health')
72+
def health():
73+
return {"status": "ok"}
74+
75+
76+
if __name__ == '__main__':
77+
78+
app.run(
79+
host='0.0.0.0',
80+
port=5000
81+
)

app/templates/index.html

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
<!doctype html>
2+
<html lang="ru">
3+
4+
<head>
5+
<meta charset="UTF-8">
6+
<title>News Dashboard</title>
7+
8+
<link
9+
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
10+
rel="stylesheet"
11+
>
12+
</head>
13+
14+
<body class="bg-light">
15+
16+
<nav class="navbar navbar-dark bg-dark px-3">
17+
18+
<span class="navbar-brand">
19+
News Parser
20+
</span>
21+
22+
<div class="d-flex gap-2">
23+
24+
<form action="/run-parser" method="post">
25+
26+
<button class="btn btn-success btn-sm">
27+
Парсинг
28+
</button>
29+
30+
</form>
31+
32+
<form action="/update-scores" method="post">
33+
34+
<button class="btn btn-warning btn-sm">
35+
Обновить Scores
36+
</button>
37+
38+
</form>
39+
40+
<a
41+
href="http://localhost:3001"
42+
target="_blank"
43+
class="btn btn-primary btn-sm"
44+
>
45+
Metabase
46+
</a>
47+
48+
<a
49+
href="http://localhost:5050"
50+
target="_blank"
51+
class="btn btn-secondary btn-sm"
52+
>
53+
pgAdmin
54+
</a>
55+
56+
</div>
57+
58+
</nav>
59+
60+
<div class="container mt-4">
61+
62+
<h3 class="mb-3">Последние новости</h3>
63+
64+
<div class="row">
65+
66+
{% for row in rows %}
67+
68+
<div class="col-md-6 mb-3">
69+
70+
<div class="card shadow-sm h-100">
71+
72+
<div class="card-body">
73+
74+
<h5 class="card-title">
75+
{{ row.title }}
76+
</h5>
77+
78+
<p class="card-text">
79+
80+
<b>Источник:</b>
81+
{{ row.source }}
82+
83+
<br>
84+
85+
<b>Автор:</b>
86+
{{ row.author }}
87+
88+
<br>
89+
90+
<b>Score:</b>
91+
{{ row.metric }}
92+
93+
</p>
94+
95+
<div class="d-flex gap-2">
96+
97+
<a
98+
href="{{ row.url }}"
99+
target="_blank"
100+
class="btn btn-outline-primary btn-sm"
101+
>
102+
Открыть
103+
</a>
104+
105+
<form
106+
action="/delete/{{ row.id }}"
107+
method="post"
108+
>
109+
110+
<button
111+
type="submit"
112+
class="btn btn-danger btn-sm"
113+
>
114+
Удалить
115+
</button>
116+
117+
</form>
118+
119+
</div>
120+
121+
</div>
122+
123+
</div>
124+
125+
</div>
126+
127+
{% endfor %}
128+
129+
</div>
130+
131+
</div>
132+
133+
</body>
134+
</html>

database/init.sql

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
CREATE TABLE IF NOT EXISTS news (
2+
id SERIAL PRIMARY KEY,
3+
source VARCHAR(50) NOT NULL,
4+
title TEXT NOT NULL,
5+
author VARCHAR(255),
6+
published_at TIMESTAMP,
7+
metric INTEGER,
8+
url TEXT UNIQUE,
9+
created_at TIMESTAMP DEFAULT NOW()
10+
);

docker-compose.yml

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
version: '3.9'
2+
3+
services:
4+
5+
app:
6+
build: .
7+
container_name: app
8+
ports:
9+
- "5000:5000"
10+
depends_on:
11+
- postgres
12+
13+
postgres:
14+
image: postgres:16
15+
container_name: postgres
16+
restart: always
17+
environment:
18+
POSTGRES_USER: admin
19+
POSTGRES_PASSWORD: admin
20+
POSTGRES_DB: newsdb
21+
ports:
22+
- "5432:5432"
23+
volumes:
24+
- postgres_data:/var/lib/postgresql/data
25+
- ./database/init.sql:/docker-entrypoint-initdb.d/init.sql
26+
27+
pgadmin:
28+
image: dpage/pgadmin4
29+
container_name: pgadmin
30+
restart: always
31+
environment:
32+
PGADMIN_DEFAULT_EMAIL: admin@example.com
33+
PGADMIN_DEFAULT_PASSWORD: admin
34+
ports:
35+
- "5050:80"
36+
volumes:
37+
- pgadmin_data:/var/lib/pgadmin
38+
39+
metabase:
40+
image: metabase/metabase
41+
container_name: metabase
42+
ports:
43+
- "3001:3000"
44+
volumes:
45+
- metabase_data:/metabase.db
46+
environment:
47+
MB_DB_FILE: /metabase.db/metabase.db
48+
49+
nginx:
50+
image: nginx:latest
51+
container_name: nginx
52+
ports:
53+
- "80:80"
54+
volumes:
55+
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
56+
depends_on:
57+
- app
58+
59+
volumes:
60+
postgres_data:
61+
pgadmin_data:
62+
metabase_data:

nginx/nginx.conf

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
events {}
2+
3+
http {
4+
5+
server {
6+
7+
listen 80;
8+
9+
location / {
10+
proxy_pass http://app:5000;
11+
}
12+
13+
location /pgadmin/ {
14+
proxy_pass http://pgadmin:80/;
15+
}
16+
17+
location /metabase/ {
18+
proxy_pass http://metabase:3000/;
19+
}
20+
}
21+
}

parsers/habr_parser.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import feedparser
2+
3+
4+
def get_habr_news():
5+
6+
url = "https://habr.com/ru/rss/all/all/"
7+
feed = feedparser.parse(url)
8+
9+
result = []
10+
11+
for entry in feed.entries:
12+
13+
# Хабр иногда кладёт тип в tags
14+
tags = []
15+
if hasattr(entry, "tags"):
16+
tags = [t.term.lower() for t in entry.tags]
17+
18+
title = entry.title.lower()
19+
20+
# ФИЛЬТР "новости"
21+
is_news = (
22+
"новость" in title or
23+
"news" in title or
24+
"digest" in tags or
25+
"news" in tags
26+
)
27+
28+
if not is_news:
29+
continue
30+
31+
result.append({
32+
"source": "habr",
33+
"title": entry.title,
34+
"author": getattr(entry, "author", "unknown"),
35+
"published_at": entry.published,
36+
"metric": 0,
37+
"url": entry.link
38+
})
39+
40+
return result

0 commit comments

Comments
 (0)