13 Commits

Author SHA1 Message Date
9424276474 fixed typo in dockerfile
All checks were successful
Build and Push Docker Image / build-and-push (release) Successful in 1m57s
2025-09-17 15:56:17 +03:00
7949312f9a Added username parameter to json load for /rate and /remove_rate
Some checks failed
Build and Push Docker Image / build-and-push (release) Failing after 42s
2025-09-17 15:47:22 +03:00
f814a1ba00 Update dockerfile 2025-09-15 15:22:41 +03:00
a910bffcc9 Update compose.yml
All checks were successful
Build and Push Docker Image / build-and-push (release) Successful in 2m7s
2025-09-15 15:05:35 +03:00
06e24e35e1 Changed 127.0.0.1 to 0.0.0.0
All checks were successful
Build and Push Docker Image / build-and-push (release) Successful in 1m59s
2025-09-15 14:36:54 +03:00
523ac2228d Implemented force exit on connection failure 2025-09-09 17:37:36 +03:00
95a232fb78 Merge branch 'main' of https://git.frik.su/n0one/habr-article-API 2025-09-09 15:24:21 +03:00
355aff8cf3 Table and schema names are now no longer pulled from .env 2025-09-09 15:22:20 +03:00
2ecf7ae56d Update README.MD 2025-09-06 03:37:55 +03:00
a2ab535256 Add README.MD 2025-09-06 03:37:07 +03:00
c3788356c7 Something i'm too ashamed to even write 2025-09-06 03:24:44 +03:00
fa012c3161 Merge branch 'main' of https://git.frik.su/n0one/habr-article-API 2025-09-06 02:47:25 +03:00
1c7e95b119 Fixes
- /api/rates endpoint now returns a list of entries in a json form
2025-09-06 02:45:47 +03:00
8 changed files with 264 additions and 17 deletions

238
README.MD Normal file
View File

@ -0,0 +1,238 @@
# Habr article API
This is a simple API that can be deployed on your server to access habr.com's articles content, as well as keeping a record of articles and their ratings which you can manage by connecting to corresponding endpoints.
From here on on out we will call a pair "article_url" - "rating" an **entry**.
## API Reference
### Ping
```http
GET /api/ping
```
A basic ping endpoint.
#### Response on success
`application/json`
```json
{
"message": "pong"
}
```
### See current entries
```http
GET /api/rates
```
Returns all entries in the PostreSQL DB.
#### Response on success
`application/json`
```json
{
"article_url_1": rating(0 or 1),
"article_url_2": rating(0 or 1),
...
}
```
### Make a new entry
```http
POST /api/article/rate
```
Save a new entry to the DB.
#### Request body
`application/json`
```json
{
"url": {article_url},
"rating": {integer, 0 or 1}
}
```
#### Response on success
`application/json`
```json
{
"message": "success",
"url": "{article_url}",
"rating": {integer, 0 or 1}
}
```
### Delete an entry
```http
POST /api/article/remove_rate
```
Delete an existing entry from the DB.
#### Request body
`application/json`
```json
{
"url": "{article_url}"
}
```
#### Response on success
`application/json`
```json
{
"message": "success"
}
```
### Get article html
```http
POST /api/article/api/article/get/html
```
Get hmtl of a desired habr article body encoded in base64.
#### Request body
`application/json`
```json
{
"url": "{article_url}"
}
```
#### Response on success
`text/plain`
```
{article_url}
{b64 encoded html}
```
### Get article MD
```http
POST /api/article/api/article/get/md
```
Get md of a desired habr article body encoded in base64.
#### Request body
`application/json`
```json
{
"url": "{article_url}"
}
```
#### Response on success
`text/plain`
```
{article_url}
{b64 encoded md}
```
### Get html of N articles from habr.com/feed
```http
POST /api/article/api/articles/get/html
```
Get html bodies of N last articles from [habr.com/feed](habr.com/feed)
#### Request body
`application/json`
```json
{
"amount": {articles_amount}
}
```
#### Response on success
`application/json`
```json
{
"{article_url_1}": "{b64_encoded_html}",
"{article_url_2}": "{b64_encoded_html}",
...
"{article_url_n}": "{b64_encoded_html}"
}
```
### Get MD of N articles from habr.com/feed
```http
POST /api/article/api/articles/get/md
```
Get MD of N last articles from [habr.com/feed](habr.com/feed)
#### Request body
`application/json`
```json
{
"amount": {articles_amount}
}
```
#### Response on success
`application/json`
```json
{
"{article_url_1}": "{b64_encoded_md}",
"{article_url_2}": "{b64_encoded_md}",
...
"{article_url_n}": "{b64_encoded_md}"
}
```

View File

@ -8,8 +8,8 @@ services:
DB_NAME: postgres
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
HOST_NAME: "localhost"
PG_PORT: "8000"
HOST_NAME: postgres
PG_PORT: 5432
LOGGING_LEVEL: "INFO"
ENABLE_API_DOCS: "True"
UVI_LOGGING_LEVEL: "info"
@ -27,7 +27,7 @@ services:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- "4001:5432"
- :5432
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
interval: 5s

View File

@ -2,10 +2,12 @@ FROM python:3.13-slim
WORKDIR /app
COPY . .
RUN apt-get update && apt-get install -y --no-install-recommends libpq-dev build-essential
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "src/main.py"]

View File

@ -17,6 +17,7 @@ def set_connection():
logger.info('Connection to PostreSQL DB set successfully')
except psycopg2.Error as e:
logger.error(f'Failed to set connection to the PostgreSQL DB: {e.pgerror}')
exit()
def close_connection(connection):
@ -53,9 +54,14 @@ def delete_entry(article_url, connection):
def get_all_entries(connection):
try:
cursor = connection.cursor()
cursor.execute('SELECT article_url, rating FROM harticle.articles;')
entries = cursor.fetchall()
cursor.execute('SELECT article_url FROM harticle.articles;')
urls = cursor.fetchall()
cursor.execute('SELECT rating FROM harticle.articles;')
ratings = cursor.fetchall()
logger.info('All entry pairs have been retrieved successfully')
entries = {}
for i in range(len(urls)):
entries[urls[i][0]] = ratings[i][0]
return entries
except psycopg2.Error as e:
logger.error(f'Failed to fetch DB entries: {e.pgerror}')
@ -93,4 +99,3 @@ def table_creator(schema_name, table_name, connection):
logger.info(f'Successfully created table {table_name} in schema {schema_name} if it didn\'t exist yet')
except psycopg2.Error as e:
logger.error(f'Error during table creation: {e}')

View File

@ -12,12 +12,15 @@ if config.enable_api_docs:
else:
docs_url = None
schema_name = 'harticle'
table_name = 'articles'
@asynccontextmanager
async def lifespan(app: FastAPI):
DBwork.set_connection()
DBwork.schema_creator(config.schema_name, db.connection)
DBwork.table_creator(config.schema_name, config.table_name, db.connection)
DBwork.schema_creator(schema_name, db.connection)
DBwork.table_creator(schema_name, table_name, db.connection)
yield
DBwork.close_connection(db.connection)

View File

@ -6,8 +6,6 @@ postgres_user = config('POSTGRES_USER')
postgres_password = config('POSTGRES_PASSWORD')
host_name = config('HOST_NAME')
port = config('PG_PORT')
schema_name = config('SCHEMA_NAME')
table_name = config('TABLE_NAME')
enable_api_docs = config('ENABLE_API_DOCS', cast=bool)

View File

@ -5,4 +5,4 @@ from app_creator import create_app
if __name__ == '__main__':
app = create_app()
uvicorn.run(app=app, host="127.0.0.1", port=8000, log_level=uvicorn_logging_level.lower())
uvicorn.run(app=app, host="0.0.0.0", port=8000, log_level=uvicorn_logging_level.lower())

View File

@ -4,7 +4,6 @@ import scraper
from fastapi import Response, status, APIRouter
from pydantic import BaseModel
import psycopg2
from json import dumps
import base64
@ -12,6 +11,7 @@ router = APIRouter(prefix='/api')
class Entry(BaseModel):
username: str
url: str
rating: int | None = None
@ -31,7 +31,7 @@ async def ping():
@router.get('/rates')
async def get_rates():
result = dumps(DBwork.get_all_entries(db.connection))
result = DBwork.get_all_entries(db.connection)
return result
@ -48,6 +48,7 @@ async def save_rating(entry: Entry, response: Response):
message = 'internal server error'
finally:
return {'message': message,
'username': entry.username,
'url': entry.url,
'rating': entry.rating
}
@ -69,14 +70,14 @@ async def remove_rating(entry: Entry, response: Response):
async def get_article_html(article: Article, response: Response = None):
html_string = await scraper.get_article_html(article.url)
b64_string = base64.b64encode(html_string.encode('utf-8')).decode('utf-8')
return Response(content=b64_string, media_type='text/plain')
return Response(content=article.url + '\r\n' + b64_string, media_type='text/plain')
@router.post('/article/get/md')
async def get_article_md(article: Article, response: Response = None):
md_string = await scraper.get_article_html(article.url, md=True)
b64_string = base64.b64encode(md_string.encode('utf-8')).decode('utf-8')
return Response(content=b64_string, media_type='text/plain')
return Response(content=article.url + '\r\n' + b64_string, media_type='text/plain')
@router.post('/articles/get/html')