9 Commits

Author SHA1 Message Date
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
6 changed files with 255 additions and 12 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

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

View File

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

View File

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

View File

@ -5,4 +5,4 @@ from app_creator import create_app
if __name__ == '__main__': if __name__ == '__main__':
app = create_app() 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 fastapi import Response, status, APIRouter
from pydantic import BaseModel from pydantic import BaseModel
import psycopg2 import psycopg2
from json import dumps
import base64 import base64
@ -31,7 +30,7 @@ async def ping():
@router.get('/rates') @router.get('/rates')
async def get_rates(): async def get_rates():
result = dumps(DBwork.get_all_entries(db.connection)) result = DBwork.get_all_entries(db.connection)
return result return result
@ -69,14 +68,14 @@ async def remove_rating(entry: Entry, response: Response):
async def get_article_html(article: Article, response: Response = None): async def get_article_html(article: Article, response: Response = None):
html_string = await scraper.get_article_html(article.url) html_string = await scraper.get_article_html(article.url)
b64_string = base64.b64encode(html_string.encode('utf-8')).decode('utf-8') 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') @router.post('/article/get/md')
async def get_article_md(article: Article, response: Response = None): async def get_article_md(article: Article, response: Response = None):
md_string = await scraper.get_article_html(article.url, md=True) md_string = await scraper.get_article_html(article.url, md=True)
b64_string = base64.b64encode(md_string.encode('utf-8')).decode('utf-8') 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') @router.post('/articles/get/html')