Removed circular import
This commit is contained in:
1 parent
a6befcf99b
commit
ee21a77a69
3 files changed
+87
-34
No files matched your search
+15
-4
@@ -1,10 +1,11 @@
|
|||||||
import time
|
import time
|
||||||
|
from typing import assert_never
|
||||||
|
|
||||||
import feedparser
|
import feedparser
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from feed_errors import FeedCheckFailedError, FeedNotFoundError
|
from feed_errors import FeedCheckFailedError, FeedNotFoundError
|
||||||
from models import FeedUpdate
|
from models import FeedEntryType, FeedUpdate
|
||||||
from simple_logger_handler import setup_logger
|
from simple_logger_handler import setup_logger
|
||||||
|
|
||||||
logger = setup_logger(__name__)
|
logger = setup_logger(__name__)
|
||||||
@@ -68,6 +69,7 @@ def grab_gitea_release_information(rss_feed_url: str) -> FeedUpdate:
|
|||||||
entry_id = feed.entries[0]["link"],
|
entry_id = feed.entries[0]["link"],
|
||||||
feed_title = feed.feed.title,
|
feed_title = feed.feed.title,
|
||||||
body_title = feed.entries[0]["title"],
|
body_title = feed.entries[0]["title"],
|
||||||
|
link = feed.entries[0]["link"],
|
||||||
)
|
)
|
||||||
logger.info(f"[Feed] Latest release for feed '{rss_feed_url}': {feed_update.feed_title}")
|
logger.info(f"[Feed] Latest release for feed '{rss_feed_url}': {feed_update.feed_title}")
|
||||||
logger.debug(f"[Feed] Latest release for feed '{rss_feed_url}': {feed_update.feed_title} -> {feed_update.link}")
|
logger.debug(f"[Feed] Latest release for feed '{rss_feed_url}': {feed_update.feed_title} -> {feed_update.link}")
|
||||||
@@ -114,7 +116,7 @@ def build_gitea_runner(version: str, gitea_token: str) -> bool:
|
|||||||
payload = {
|
payload = {
|
||||||
"ref": "main",
|
"ref": "main",
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"version": version.lstrip("v"),
|
"version": version.removeprefix("v"),
|
||||||
"revision": "1",
|
"revision": "1",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -141,10 +143,19 @@ def ensure_feed_exists(url: str, timeout: tuple[float, float] = (3.0, 5.0)) -> N
|
|||||||
if status != 200:
|
if status != 200:
|
||||||
raise FeedCheckFailedError(url)
|
raise FeedCheckFailedError(url)
|
||||||
|
|
||||||
|
def fetch_latest(feed_type: FeedEntryType, url: str) -> FeedUpdate:
|
||||||
|
match feed_type:
|
||||||
|
case FeedEntryType.ROYALROAD:
|
||||||
|
return grab_royal_road_chapter_information(url)
|
||||||
|
case FeedEntryType.GITEA:
|
||||||
|
return grab_gitea_release_information(url)
|
||||||
|
case _:
|
||||||
|
assert_never(feed_type)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
#rsssss="https://gitea.com/gitea/runner/releases.rss"
|
rsssss="https://gitea.com/gitea/runner/releases.rss"
|
||||||
#print(get_rss_feed_data(rsssss))
|
#print(get_rss_feed_data(rsssss))
|
||||||
#print(grab_gitea_release_information(rsssss))
|
print(grab_gitea_release_information(rsssss))
|
||||||
id="118891"
|
id="118891"
|
||||||
rss_feed_url = f"https://www.royalroad.com/fiction/syndication/{id}"
|
rss_feed_url = f"https://www.royalroad.com/fiction/syndication/{id}"
|
||||||
print(grab_royal_road_chapter_information(rss_feed_url))
|
print(grab_royal_road_chapter_information(rss_feed_url))
|
||||||
+71
-19
@@ -4,22 +4,26 @@ from contextlib import asynccontextmanager
|
|||||||
|
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from fastapi import APIRouter, Depends, FastAPI, HTTPException, Request
|
from fastapi import APIRouter, Depends, FastAPI, HTTPException, Request
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
from auth_config import Caller, auth
|
from auth_config import Caller, auth
|
||||||
from db_module import close_connection_pool, create_connection_pool, get_db, start_healthcheck_thread
|
from db_module import close_connection_pool, create_connection_pool, get_db, start_healthcheck_thread
|
||||||
|
from feed_errors import FeedError
|
||||||
from feed_handler import (
|
from feed_handler import (
|
||||||
build_gitea_runner,
|
build_gitea_runner,
|
||||||
|
ensure_feed_exists,
|
||||||
grab_gitea_release_information,
|
grab_gitea_release_information,
|
||||||
grab_royal_road_chapter_information,
|
grab_royal_road_chapter_information,
|
||||||
)
|
)
|
||||||
from metrics_server import REQUEST_COUNTER, metrics_api
|
from metrics_server import REQUEST_COUNTER, metrics_api
|
||||||
|
from models import FeedEntry, fetch_latest
|
||||||
from secret_manager import cleanup_secret_files, get_secret
|
from secret_manager import cleanup_secret_files, get_secret
|
||||||
from send_notifications import NotificationClient, NotificationError, NotificationUnavailableError
|
from send_notifications import NotificationClient, NotificationError, NotificationUnavailableError
|
||||||
|
from service_auth import UserId
|
||||||
from simple_logger_handler import LOG_LEVEL, setup_logger
|
from simple_logger_handler import LOG_LEVEL, setup_logger
|
||||||
|
|
||||||
logger = setup_logger(__name__)
|
logger = setup_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
logger.info("[FastAPI] Starting application...")
|
logger.info("[FastAPI] Starting application...")
|
||||||
@@ -53,6 +57,10 @@ api = FastAPI(
|
|||||||
dependencies=[Depends(auth.authenticate)],
|
dependencies=[Depends(auth.authenticate)],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@api.exception_handler(FeedError)
|
||||||
|
async def feed_error_handler(request: Request, exc: FeedError):
|
||||||
|
return JSONResponse(status_code=exc.status_code, content={"detail":str(exc)})
|
||||||
|
|
||||||
@api.middleware("http")
|
@api.middleware("http")
|
||||||
async def prometheus_middleware(request: Request, call_next):
|
async def prometheus_middleware(request: Request, call_next):
|
||||||
logger.debug(f"[FastAPI] Incoming request: {request.method} {request.url.path}")
|
logger.debug(f"[FastAPI] Incoming request: {request.method} {request.url.path}")
|
||||||
@@ -103,13 +111,13 @@ def get_release_information(request: Request, db = Depends(get_db)):
|
|||||||
for feed_id, rss_feed, last_entry in feeds:
|
for feed_id, rss_feed, last_entry in feeds:
|
||||||
results["checked"] += 1
|
results["checked"] += 1
|
||||||
try:
|
try:
|
||||||
release_title, link = grab_gitea_release_information(rss_feed)
|
gitea_release_information = grab_gitea_release_information(rss_feed)
|
||||||
logger.debug(f"[FastAPI] ID:{feed_id},RSS Feed {rss_feed}: db={last_entry}, online={link}")
|
logger.debug(f"[FastAPI] ID:{feed_id},RSS Feed {rss_feed}: db={last_entry}, online={gitea_release_information.link}")
|
||||||
|
|
||||||
if link == last_entry:
|
if gitea_release_information.link == last_entry:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
logger.info(f"[FastAPI] Release change for ID {feed_id}: {release_title}")
|
logger.info(f"[FastAPI] Release change for ID {feed_id}: {gitea_release_information.release_title}")
|
||||||
|
|
||||||
with db.cursor() as cursor:
|
with db.cursor() as cursor:
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
@@ -124,11 +132,11 @@ def get_release_information(request: Request, db = Depends(get_db)):
|
|||||||
failed = 0
|
failed = 0
|
||||||
for user_id,feed_name in subscribers:
|
for user_id,feed_name in subscribers:
|
||||||
try:
|
try:
|
||||||
repo = link.split("/releases")[0].removeprefix("https://")
|
repo = gitea_release_information.link.split("/releases")[0].removeprefix("https://")
|
||||||
request.app.state.notifier.send(
|
request.app.state.notifier.send(
|
||||||
title=feed_name or repo,
|
title=feed_name or repo,
|
||||||
body=release_title,
|
body=gitea_release_information.release_title,
|
||||||
link=link,
|
link=gitea_release_information.link,
|
||||||
category="utility",
|
category="utility",
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
)
|
)
|
||||||
@@ -149,14 +157,14 @@ def get_release_information(request: Request, db = Depends(get_db)):
|
|||||||
with db.cursor() as cursor:
|
with db.cursor() as cursor:
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"UPDATE rss_feeds SET last_entry = %s WHERE id = %s",
|
"UPDATE rss_feeds SET last_entry = %s WHERE id = %s",
|
||||||
(link, feed_id),
|
(gitea_release_information.link, feed_id),
|
||||||
)
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
logger.debug(f"[FastAPI] Notification sent and database updated for Gitea feed ID {feed_id}")
|
logger.debug(f"[FastAPI] Notification sent and database updated for Gitea feed ID {feed_id}")
|
||||||
results["new"] += 1
|
results["new"] += 1
|
||||||
|
|
||||||
if link.startswith("https://gitea.com/gitea/runner/releases/tag/v"):
|
if gitea_release_information.link.startswith("https://gitea.com/gitea/runner/releases/tag/v"):
|
||||||
gitea_build_success = build_gitea_runner(release_title, request.app.state.gitea_token)
|
gitea_build_success = build_gitea_runner(gitea_release_information.release_title, request.app.state.gitea_token)
|
||||||
logger.info(f"[FastAPI] Gitea build ran successfully:{gitea_build_success}")
|
logger.info(f"[FastAPI] Gitea build ran successfully:{gitea_build_success}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -205,12 +213,12 @@ def get_chapters(request: Request, db = Depends(get_db)):
|
|||||||
results["checked"] += 1
|
results["checked"] += 1
|
||||||
try:
|
try:
|
||||||
story = grab_royal_road_chapter_information(rss_feed)
|
story = grab_royal_road_chapter_information(rss_feed)
|
||||||
logger.debug(f"[FastAPI] ID:{feed_id},RSS Feed {rss_feed}: db={last_entry}, online={story.chapter_id}")
|
logger.debug(f"[FastAPI] ID:{feed_id},RSS Feed {rss_feed}: db={last_entry}, online={story.entry_id}")
|
||||||
|
|
||||||
if story.chapter_id == last_entry:
|
if story.entry_id == last_entry:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
logger.info(f"[FastAPI] New chapter detected for story ID {feed_id}: {story.story_title}")
|
logger.info(f"[FastAPI] New chapter detected for story ID {feed_id}: {story.feed_title}")
|
||||||
with db.cursor() as cursor:
|
with db.cursor() as cursor:
|
||||||
cursor.execute (
|
cursor.execute (
|
||||||
"SELECT user_id, feed_name "
|
"SELECT user_id, feed_name "
|
||||||
@@ -225,8 +233,8 @@ def get_chapters(request: Request, db = Depends(get_db)):
|
|||||||
for user_id, feed_name in subscribers:
|
for user_id, feed_name in subscribers:
|
||||||
try:
|
try:
|
||||||
request.app.state.notifier.send(
|
request.app.state.notifier.send(
|
||||||
title=feed_name or story.story_title,
|
title=feed_name or story.feed_title,
|
||||||
body=story.chapter_title,
|
body=story.body_title,
|
||||||
link=story.link,
|
link=story.link,
|
||||||
category="royal-road",
|
category="royal-road",
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
@@ -235,11 +243,11 @@ def get_chapters(request: Request, db = Depends(get_db)):
|
|||||||
except NotificationUnavailableError:
|
except NotificationUnavailableError:
|
||||||
failed = len(subscribers) - sent
|
failed = len(subscribers) - sent
|
||||||
logger.error (f"[FastAPI] Notifications backend unavailable, skipping "
|
logger.error (f"[FastAPI] Notifications backend unavailable, skipping "
|
||||||
f"{failed} remaining user(s) for story={story.story_title}")
|
f"{failed} remaining user(s) for story={story.feed_title}")
|
||||||
break
|
break
|
||||||
except NotificationError as e:
|
except NotificationError as e:
|
||||||
failed += 1
|
failed += 1
|
||||||
logger.error(f"[FastAPI] Failed to send notification to user_id={user_id} for story={story.story_title} "
|
logger.error(f"[FastAPI] Failed to send notification to user_id={user_id} for story={story.feed_title} "
|
||||||
f"status={getattr(e, 'status_code',None)}")
|
f"status={getattr(e, 'status_code',None)}")
|
||||||
if subscribers and sent == 0:
|
if subscribers and sent == 0:
|
||||||
results["errors"] += 1
|
results["errors"] += 1
|
||||||
@@ -248,7 +256,7 @@ def get_chapters(request: Request, db = Depends(get_db)):
|
|||||||
with db.cursor() as cursor:
|
with db.cursor() as cursor:
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"UPDATE rss_feeds SET last_entry = %s WHERE id = %s",
|
"UPDATE rss_feeds SET last_entry = %s WHERE id = %s",
|
||||||
(story.chapter_id, feed_id),
|
(story.entry_id, feed_id),
|
||||||
)
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
logger.debug(f"[FastAPI] Notification sent and database updated for story ID {feed_id}")
|
logger.debug(f"[FastAPI] Notification sent and database updated for story ID {feed_id}")
|
||||||
@@ -269,6 +277,50 @@ def get_chapters(request: Request, db = Depends(get_db)):
|
|||||||
logger.info(f"[FastAPI] Royalroad check complete: {results}")
|
logger.info(f"[FastAPI] Royalroad check complete: {results}")
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
@user_router.post("/add/feed")
|
||||||
|
def add_feed(
|
||||||
|
user_id: UserId,
|
||||||
|
feed_entry: FeedEntry,
|
||||||
|
db = Depends(get_db)
|
||||||
|
):
|
||||||
|
url = feed_entry.rss_feed_url
|
||||||
|
logger.info(f"[FastAPI] Subscribe request: RSS URL={url}, user_id={user_id}")
|
||||||
|
with db.cursor() as cursor:
|
||||||
|
cursor.execute("SELECT id FROM rss_feeds WHERE rss_feed = %s", (feed_entry.rss_feed_url,),)
|
||||||
|
known = cursor.fetchone()
|
||||||
|
|
||||||
|
last_entry = None
|
||||||
|
if known is None:
|
||||||
|
ensure_feed_exists(url)
|
||||||
|
logger.info(f"[FastApi] Adding new RSS feed, type={feed_entry.type}, rss_feed_url={url}")
|
||||||
|
try:
|
||||||
|
last_entry = fetch_latest(feed_entry.type, url)
|
||||||
|
except ValueError:
|
||||||
|
logger.warning(f"[FastAPI] Could not read latest entry for {url}, storing none")
|
||||||
|
try:
|
||||||
|
with db.cursor() as cursor:
|
||||||
|
cursor.execute(
|
||||||
|
"INSERT into rss_feeds (type,rss_feed,last_entry) VALUES (%s,%s,%s) "
|
||||||
|
"ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id), active = 1",
|
||||||
|
(feed_entry.type, url, last_entry.entry_id),
|
||||||
|
)
|
||||||
|
feed_id = cursor.lastrowid
|
||||||
|
|
||||||
|
cursor.execute (
|
||||||
|
"INSERT INTO rss_feed_subscriptions (user_id, rss_feed_id, feed_name) "
|
||||||
|
"VALUES (%s, %s, %s) "
|
||||||
|
"ON DUPLICATE KEY UPDATE active = 1, feed_name = VALUES(feed_name)",
|
||||||
|
(user_id, feed_id, feed_entry.custom_name),
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
except Exception as e:
|
||||||
|
db.rollback()
|
||||||
|
logger.exception(f"[FastAPI] Failed to subscribe to user_id={user_id} to {url}")
|
||||||
|
raise HTTPException(status_code=500) from e
|
||||||
|
|
||||||
|
logger.info(f"[FastAPI] user_id={user_id} subscribed to feed_id={feed_id}, url={url}")
|
||||||
|
return {"status":"ok","detail":"Subscribed to feed","feed_id":{feed_id}}
|
||||||
|
|
||||||
api.include_router(user_router)
|
api.include_router(user_router)
|
||||||
api.include_router(scheduler_router)
|
api.include_router(scheduler_router)
|
||||||
|
|
||||||
|
|||||||
+1
-11
@@ -1,12 +1,11 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
from typing import Self, assert_never
|
from typing import Self
|
||||||
from urllib.parse import urlsplit
|
from urllib.parse import urlsplit
|
||||||
|
|
||||||
from pydantic import BaseModel, Field, field_validator, model_validator
|
from pydantic import BaseModel, Field, field_validator, model_validator
|
||||||
|
|
||||||
from feed_errors import InvalidFeedUrlError, UnsupportedPathError, WrongHostError
|
from feed_errors import InvalidFeedUrlError, UnsupportedPathError, WrongHostError
|
||||||
from feed_handler import grab_gitea_release_information, grab_royal_road_chapter_information
|
|
||||||
|
|
||||||
REPO_CHARS = frozenset("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.-")
|
REPO_CHARS = frozenset("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.-")
|
||||||
|
|
||||||
@@ -21,15 +20,6 @@ class FeedEntryType(StrEnum):
|
|||||||
ROYALROAD = "royalroad"
|
ROYALROAD = "royalroad"
|
||||||
GITEA = "gitea"
|
GITEA = "gitea"
|
||||||
|
|
||||||
def fetch_latest(feed_type: FeedEntryType, url: str) -> FeedUpdate:
|
|
||||||
match feed_type:
|
|
||||||
case FeedEntryType.ROYALROAD:
|
|
||||||
return grab_royal_road_chapter_information(url)
|
|
||||||
case FeedEntryType.GITEA:
|
|
||||||
return grab_gitea_release_information(url)
|
|
||||||
case _:
|
|
||||||
assert_never(feed_type)
|
|
||||||
|
|
||||||
|
|
||||||
def _path_segments(raw: str, feed_type: FeedEntryType, host: str) -> list[str]:
|
def _path_segments(raw: str, feed_type: FeedEntryType, host: str) -> list[str]:
|
||||||
"""Check scheme and host, then return the non-empty path segments."""
|
"""Check scheme and host, then return the non-empty path segments."""
|
||||||
|
|||||||
Reference in new issue
Block a user