Compare commits
41 commits
fa7953a763
...
6e24614c54
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e24614c54 | |||
| 842e8a12d4 | |||
| c0d2bb2f11 | |||
| bdfe36775c | |||
| 9132b2153f | |||
| 7bb09063a3 | |||
| ab1497b488 | |||
| c4d64f6ac7 | |||
| 91c13307aa | |||
| c51908ffeb | |||
| 97f0697502 | |||
| 8989a6f1b1 | |||
| 5d0d68c1f9 | |||
| 7aa59a9813 | |||
| 8f5849e97d | |||
| 2e28361c75 | |||
| 99e7b77bd1 | |||
| 53993111a4 | |||
| 26a16388f2 | |||
| 4b07853762 | |||
| 93de81cf86 | |||
| 0049defe8a | |||
| 149f34496f | |||
| 418e2a22c9 | |||
| 4acbb84c05 | |||
| 58e67fd956 | |||
| 174bd71413 | |||
| 51d7a468cf | |||
| b5aa03eb95 | |||
| 29b9dd9b84 | |||
| ba20dd7792 | |||
| d38b53096c | |||
| 6b164aa983 | |||
| cf3f121e77 | |||
| be9fdc0e98 | |||
| 8c5513d6c8 | |||
| dade6d5330 | |||
| 83116b2b2d | |||
| 492feaddae | |||
| 4645f124d7 | |||
| 0d5a5f3da8 |
80 changed files with 2576 additions and 497 deletions
BIN
.coverage
Normal file
BIN
.coverage
Normal file
Binary file not shown.
60
.forgejo/workflows/deploy_backend.yml
Normal file
60
.forgejo/workflows/deploy_backend.yml
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
name: Build and Deploy Backend
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- "backend/**"
|
||||||
|
- "pyproject.toml"
|
||||||
|
- "uv.lock"
|
||||||
|
- "deploy/deploy-backend.sh"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build_and_deploy:
|
||||||
|
runs-on: roadrunner
|
||||||
|
|
||||||
|
container:
|
||||||
|
volumes:
|
||||||
|
- /opt/blauwasser-api:/opt/blauwasser-api
|
||||||
|
- /var/lib/blauwasser-api:/var/lib/blauwasser-api
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Repository auschecken
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Python einrichten
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.14"
|
||||||
|
|
||||||
|
# - name: Backend testen
|
||||||
|
# working-directory: backend
|
||||||
|
# run: |
|
||||||
|
# python -m pip install -U pip
|
||||||
|
# python -m pip install -e .
|
||||||
|
# pytest
|
||||||
|
|
||||||
|
- name: Backend kopieren
|
||||||
|
run: |
|
||||||
|
rsync -a --delete \
|
||||||
|
--exclude '.venv' \
|
||||||
|
--exclude '__pycache__' \
|
||||||
|
--exclude '*.db' \
|
||||||
|
backend/ /opt/blauwasser-api/backend/
|
||||||
|
cp pyproject.toml /opt/blauwasser-api/
|
||||||
|
cp uv.lock /opt/blauwasser-api/
|
||||||
|
|
||||||
|
- name: Abhängigkeiten aktualisieren
|
||||||
|
run: |
|
||||||
|
cd /opt/blauwasser-api
|
||||||
|
uv sync --frozen
|
||||||
|
|
||||||
|
- name: Backend neu starten
|
||||||
|
run: |
|
||||||
|
systemctl restart blauwasser-api
|
||||||
|
systemctl is-active --quiet blauwasser-api
|
||||||
|
|
||||||
|
- name: Deployment prüfen
|
||||||
|
run: |
|
||||||
|
curl --fail https://api.blauwasser-mausis.de/health
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,3 +1 @@
|
||||||
node_modules
|
node_modules
|
||||||
backend/venv/lib/**
|
|
||||||
backend/venv/lib64/*
|
|
||||||
9
.vscode/settings.json
vendored
9
.vscode/settings.json
vendored
|
|
@ -2,9 +2,16 @@
|
||||||
"cSpell.language": "en,de-DE",
|
"cSpell.language": "en,de-DE",
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"Chiara",
|
"Chiara",
|
||||||
|
"dotenv",
|
||||||
"Hobbithöhle",
|
"Hobbithöhle",
|
||||||
|
"mailtrap",
|
||||||
"Masala",
|
"Masala",
|
||||||
"Pomorie",
|
"Pomorie",
|
||||||
"Reinke"
|
"Reinke"
|
||||||
]
|
],
|
||||||
|
"python.testing.pytestArgs": [
|
||||||
|
"backend"
|
||||||
|
],
|
||||||
|
"python.testing.unittestEnabled": false,
|
||||||
|
"python.testing.pytestEnabled": true
|
||||||
}
|
}
|
||||||
3
backend/.gitignore
vendored
Normal file
3
backend/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
.venv/lib/**
|
||||||
|
.venv/lib64/*
|
||||||
|
dev.db
|
||||||
1
backend/.python-version
Normal file
1
backend/.python-version
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
3.14
|
||||||
5
backend/README.md
Normal file
5
backend/README.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
Start Backend
|
||||||
|
|
||||||
|
```
|
||||||
|
uv run fastapi dev
|
||||||
|
```
|
||||||
Binary file not shown.
BIN
backend/cron_state.db
Normal file
BIN
backend/cron_state.db
Normal file
Binary file not shown.
BIN
backend/cron_state.db-shm
Normal file
BIN
backend/cron_state.db-shm
Normal file
Binary file not shown.
BIN
backend/cron_state.db-wal
Normal file
BIN
backend/cron_state.db-wal
Normal file
Binary file not shown.
BIN
backend/dev.db
BIN
backend/dev.db
Binary file not shown.
|
|
@ -1,21 +0,0 @@
|
||||||
from nexios import NexiosApp
|
|
||||||
import uvicorn
|
|
||||||
|
|
||||||
# Create a new Nexios application
|
|
||||||
app = NexiosApp()
|
|
||||||
|
|
||||||
# Define a simple route
|
|
||||||
@app.get("/")
|
|
||||||
async def hello_world(request, response):
|
|
||||||
return response.json({
|
|
||||||
"message": "Hello from Nexios!",
|
|
||||||
"status": "success"
|
|
||||||
})
|
|
||||||
|
|
||||||
# If you forget to use async def for your handler, Nexios will run the handler in a threadpool
|
|
||||||
|
|
||||||
# If you define two routes with the same path and method, Nexios will raise a conflict error at startup.
|
|
||||||
|
|
||||||
# Run the application
|
|
||||||
if __name__ == "__main__":
|
|
||||||
uvicorn.run("main:app")
|
|
||||||
31
backend/pyproject.toml
Normal file
31
backend/pyproject.toml
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
[project]
|
||||||
|
name = "backend"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Add your description here"
|
||||||
|
readme = "README.md"
|
||||||
|
authors = [
|
||||||
|
{ name = "ebby", email = "ebbyplay@proton.me" }
|
||||||
|
]
|
||||||
|
requires-python = ">=3.14"
|
||||||
|
dependencies = [
|
||||||
|
"fastapi[standard]==0.136.3",
|
||||||
|
"httpx2>=2.12.0",
|
||||||
|
"pytest>=9.1.1",
|
||||||
|
"sqlmodel>=0.0.42",
|
||||||
|
"mailtrap==2.10.0",
|
||||||
|
"fastapi-limiter==0.2.0",
|
||||||
|
"fastapi-crons==2.5.0",
|
||||||
|
"feedparser==6.0.14",
|
||||||
|
"PyJWT==2.15.0",
|
||||||
|
"python-dotenv>=1.0.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
backend = "backend:main"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["uv_build>=0.12.9,<0.13.0"]
|
||||||
|
build-backend = "uv_build"
|
||||||
|
|
||||||
|
[tool.fastapi]
|
||||||
|
entrypoint = "backend.main:app"
|
||||||
5
backend/pyrefly.toml
Normal file
5
backend/pyrefly.toml
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
[tool.pyrefly]
|
||||||
|
|
||||||
|
project-includes = [
|
||||||
|
"./**/**/*.py",
|
||||||
|
]
|
||||||
6
backend/src/backend/.env
Normal file
6
backend/src/backend/.env
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
BACKEND_URL="http://localhost:8000"
|
||||||
|
FRONTEND_URL="http://localhost:4321"
|
||||||
|
JWT_SECRET="IamALocalSecretAndMustBeReplacedInProduction"
|
||||||
|
ENVIRONMENT="local"
|
||||||
|
MAILTRAP_API_TOKEN="FakedyFake"
|
||||||
|
DATABASE_URL="sqlite:///dev.db"
|
||||||
BIN
backend/src/backend/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
backend/src/backend/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/src/backend/__pycache__/config.cpython-314.pyc
Normal file
BIN
backend/src/backend/__pycache__/config.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/src/backend/__pycache__/main.cpython-314.pyc
Normal file
BIN
backend/src/backend/__pycache__/main.cpython-314.pyc
Normal file
Binary file not shown.
8
backend/src/backend/config.py
Normal file
8
backend/src/backend/config.py
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
ENVIRONMENT = os.environ["ENVIRONMENT"]
|
||||||
|
JWT_SECRET = os.environ["JWT_SECRET"]
|
||||||
|
FRONTEND_URL = os.environ["FRONTEND_URL"]
|
||||||
|
BACKEND_URL = os.environ["BACKEND_URL"]
|
||||||
|
MAILTRAP_API_TOKEN = os.environ["MAILTRAP_API_TOKEN"]
|
||||||
|
DATABASE_URL = os.environ["DATABASE_URL"]
|
||||||
13
backend/src/backend/dependencies.py
Normal file
13
backend/src/backend/dependencies.py
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from fastapi import Header, HTTPException
|
||||||
|
|
||||||
|
|
||||||
|
async def get_token_header(x_token: Annotated[str, Header()]):
|
||||||
|
if x_token != "fake-super-secret-token":
|
||||||
|
raise HTTPException(status_code=400, detail="X-Token header invalid")
|
||||||
|
|
||||||
|
|
||||||
|
async def get_query_token(token: str):
|
||||||
|
if token != "jessica":
|
||||||
|
raise HTTPException(status_code=400, detail="No Jessica token provided")
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
51
backend/src/backend/internal/authentication.py
Normal file
51
backend/src/backend/internal/authentication.py
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
|
||||||
|
from backend.config import JWT_SECRET
|
||||||
|
from fastapi.security import HTTPBasicCredentials
|
||||||
|
from fastapi import Security
|
||||||
|
from fastapi.security import HTTPBearer
|
||||||
|
from fastapi import Header
|
||||||
|
from jwt.exceptions import PyJWTError
|
||||||
|
from datetime import timezone
|
||||||
|
import jwt
|
||||||
|
from sqlmodel import select
|
||||||
|
from backend.models.user import User
|
||||||
|
from backend.internal.database import get_session
|
||||||
|
from sqlmodel import Session
|
||||||
|
from fastapi import HTTPException
|
||||||
|
from fastapi import Depends
|
||||||
|
from datetime import datetime
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
|
||||||
|
ALGORITHM = "HS256"
|
||||||
|
ACCESS_TOKEN_EXPIRE_DAYS = 180
|
||||||
|
|
||||||
|
def create_access_token(data: dict, expires_delta: timedelta | None = None):
|
||||||
|
to_encode = data.copy()
|
||||||
|
expire = datetime.now(timezone.utc) + (expires_delta or timedelta(days=ACCESS_TOKEN_EXPIRE_DAYS))
|
||||||
|
to_encode.update({"exp": expire})
|
||||||
|
return jwt.encode(to_encode, JWT_SECRET, algorithm=ALGORITHM)
|
||||||
|
|
||||||
|
security = HTTPBearer()
|
||||||
|
|
||||||
|
def get_token(credentials = Security(security)) -> str:
|
||||||
|
parts = credentials.credentials.split()
|
||||||
|
if len(parts) == 2 and parts[0].lower() == "bearer":
|
||||||
|
return parts[1]
|
||||||
|
raise HTTPException(status_code=403, detail="Invalid authorization format")
|
||||||
|
|
||||||
|
def decode_token(token: str)-> str | None:
|
||||||
|
payload = jwt.decode(token, JWT_SECRET, algorithms=[ALGORITHM])
|
||||||
|
return payload.get("sub")
|
||||||
|
|
||||||
|
async def get_current_user(token: str = Depends(get_token), session: Session = Depends(get_session)):
|
||||||
|
try:
|
||||||
|
user_code: str | None = decode_token(token)
|
||||||
|
if user_code is None:
|
||||||
|
raise HTTPException(status_code=401, detail="Invalid token")
|
||||||
|
except PyJWTError:
|
||||||
|
raise HTTPException(status_code=401, detail="Invalid token")
|
||||||
|
user = session.exec(select(User).where(User.user_code == user_code)).first()
|
||||||
|
if user is None:
|
||||||
|
raise HTTPException(status_code=401, detail="User not found")
|
||||||
|
return user
|
||||||
16
backend/src/backend/internal/cors.py
Normal file
16
backend/src/backend/internal/cors.py
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
from backend.main import app
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
|
origins = [
|
||||||
|
"http://localhost",
|
||||||
|
]
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=[
|
||||||
|
"https://blauwasser-mausis.de",
|
||||||
|
],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
17
backend/src/backend/internal/database.py
Normal file
17
backend/src/backend/internal/database.py
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
from backend.config import DATABASE_URL
|
||||||
|
from collections.abc import Generator
|
||||||
|
from sqlmodel import Session
|
||||||
|
from sqlmodel import SQLModel
|
||||||
|
from sqlmodel import create_engine
|
||||||
|
|
||||||
|
sqlite_url = DATABASE_URL
|
||||||
|
|
||||||
|
connect_args = {"check_same_thread": False}
|
||||||
|
engine = create_engine(sqlite_url, connect_args=connect_args)
|
||||||
|
|
||||||
|
def create_db_and_tables():
|
||||||
|
SQLModel.metadata.create_all(engine)
|
||||||
|
|
||||||
|
def get_session() -> Generator[Session, None, None]:
|
||||||
|
with Session(engine) as session:
|
||||||
|
yield session
|
||||||
63
backend/src/backend/internal/jinja_templates/approval.jinja
Normal file
63
backend/src/backend/internal/jinja_templates/approval.jinja
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Newsletter</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
<h1>Neuer Artikel:</h1>
|
||||||
|
<h3>{{data.title}}</h3>
|
||||||
|
<h4>{{data.subtitle}}</h4>
|
||||||
|
<p>{{data.description}}</p>
|
||||||
|
|
||||||
|
<a href={{data.link}}>
|
||||||
|
<button>Zum Artikel</button>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="https://blauwasser-mausis.de/blog/bootsuche-england">
|
||||||
|
<span>Abbestellen</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a id="admin" href="https://backend.blauwasser-mausis.de/?secret={{data.secret}}">Approve</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 32px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: lightgrey;
|
||||||
|
padding: 32px 64px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3,
|
||||||
|
h4 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 12px;
|
||||||
|
background-color: azure;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#admin {
|
||||||
|
margin-top: 64px;
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Newsletter</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
<h1>Neuer Artikel:</h1>
|
||||||
|
<h3>{{data.title}}</h3>
|
||||||
|
<h4>{{data.subtitle}}</h4>
|
||||||
|
<p>{{data.description}}</p>
|
||||||
|
|
||||||
|
<a href={{data.link}}>
|
||||||
|
<button>Zum Artikel</button>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="https://blauwasser-mausis.de/blog/bootsuche-england">
|
||||||
|
<span>Abbestellen</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 32px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: lightgrey;
|
||||||
|
padding: 32px 64px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3,
|
||||||
|
h4 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 12px;
|
||||||
|
background-color: azure;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
40
backend/src/backend/internal/jinja_templates/welcome.jinja
Normal file
40
backend/src/backend/internal/jinja_templates/welcome.jinja
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Newsletter</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
<h1>Willkommen, {{data.name}}</h1>
|
||||||
|
<p>Du hast dich bei Blauwasser-Mausis.de angemeldet. Solltest du das nicht gewesen sein, dann lösche die Daten einfach über den untenstehenden Link.</p>
|
||||||
|
|
||||||
|
<p>Dein persönlicher Code, um alle Funktionen nutzen zu können: {{data.user_code}}</p>
|
||||||
|
|
||||||
|
<a href="https://blauwasser-mausis.de/">
|
||||||
|
<span>Daten löschen</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 32px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: lightgrey;
|
||||||
|
padding: 32px 64px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
68
backend/src/backend/internal/mail_util.py
Normal file
68
backend/src/backend/internal/mail_util.py
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
from backend.config import MAILTRAP_API_TOKEN
|
||||||
|
from mailtrap import BatchEmailRequest
|
||||||
|
from mailtrap import BatchSendEmailParams
|
||||||
|
from backend.config import ENVIRONMENT
|
||||||
|
from mailtrap import Mail
|
||||||
|
import mailtrap as mt
|
||||||
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
|
||||||
|
class ArticleData():
|
||||||
|
def __init__(self, title, subtitle, description, link):
|
||||||
|
self.title=title
|
||||||
|
self.subtitle=subtitle
|
||||||
|
self.description=description
|
||||||
|
self.link=link
|
||||||
|
|
||||||
|
title: str
|
||||||
|
subtitle: str
|
||||||
|
description: str
|
||||||
|
link: str
|
||||||
|
|
||||||
|
class AdminApprovalData(ArticleData):
|
||||||
|
def __init__(self,title, subtitle, description, link, secret):
|
||||||
|
ArticleData.__init__(self, title, subtitle, description, link)
|
||||||
|
self.secret = secret
|
||||||
|
|
||||||
|
secret: str
|
||||||
|
|
||||||
|
def create_html_from_template(template_name: str, data):
|
||||||
|
env = Environment(loader = FileSystemLoader('src/backend/internal/jinja_templates'))
|
||||||
|
template = env.get_template(f"{template_name}.jinja")
|
||||||
|
return template.render(data=data)
|
||||||
|
|
||||||
|
def send_newsletter_admin_approval(data: ArticleData ):
|
||||||
|
html = create_html_from_template("approval", data)
|
||||||
|
|
||||||
|
mail = mt.Mail(
|
||||||
|
sender=mt.Address(email="newsletter@blauwasser-mausis.de", name="Flaschenpost"),
|
||||||
|
to=[mt.Address(email="blauwasser-mausis@proton.me")],
|
||||||
|
subject=f"[Approval] {data.title}",
|
||||||
|
html=html,
|
||||||
|
)
|
||||||
|
send_mail(mail)
|
||||||
|
|
||||||
|
def send_mail(mail: Mail):
|
||||||
|
if ENVIRONMENT == "local":
|
||||||
|
print(f"Mail sent: {mail.subject}")
|
||||||
|
print(mail.html)
|
||||||
|
else:
|
||||||
|
client = mt.MailtrapClient(token=MAILTRAP_API_TOKEN)
|
||||||
|
client.send(mail)
|
||||||
|
|
||||||
|
def send_batch_mail(mails: list[BatchEmailRequest]):
|
||||||
|
if ENVIRONMENT == "local":
|
||||||
|
for mail in mails:
|
||||||
|
print(f"Mail sent: {mail.subject} to {mail.to}")
|
||||||
|
else:
|
||||||
|
client = mt.MailtrapClient(token=MAILTRAP_API_TOKEN)
|
||||||
|
params = BatchSendEmailParams(
|
||||||
|
base={
|
||||||
|
"from": {
|
||||||
|
"name": "Blauwasser Mausis",
|
||||||
|
"email": "newsletter@blauwasser-mausis.de"
|
||||||
|
},
|
||||||
|
"subject": "Flaschenpost | Blauwasser Mausis",
|
||||||
|
},
|
||||||
|
requests=mails
|
||||||
|
)
|
||||||
|
client.batch_send(params)
|
||||||
32
backend/src/backend/internal/rss_reader.py
Normal file
32
backend/src/backend/internal/rss_reader.py
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
|
||||||
|
from feedparser import FeedParserDict
|
||||||
|
from backend.config import FRONTEND_URL
|
||||||
|
from backend.internal.authentication import create_access_token
|
||||||
|
from backend.internal.mail_util import AdminApprovalData
|
||||||
|
from backend.internal.mail_util import send_newsletter_admin_approval
|
||||||
|
from datetime import timedelta
|
||||||
|
from datetime import datetime
|
||||||
|
import feedparser
|
||||||
|
|
||||||
|
def check_blog_rss_and_send_admin_mail():
|
||||||
|
feed = feedparser.parse(f"{FRONTEND_URL}/rss.xml")
|
||||||
|
for article in feed.entries:
|
||||||
|
date_string = str(article["published"])
|
||||||
|
date_object = datetime.strptime(date_string, '%a, %d %b %Y %H:%M:%S %Z')
|
||||||
|
|
||||||
|
if date_object > (datetime.now() - timedelta(days=16)):
|
||||||
|
title = str(article["title"])
|
||||||
|
description = str(article["summary"])
|
||||||
|
subtitle = str(article["subtitle"])
|
||||||
|
link = str(article["link"])
|
||||||
|
secret = create_access_token({"sub": title})
|
||||||
|
|
||||||
|
data = AdminApprovalData(title, subtitle, description, link, secret)
|
||||||
|
send_newsletter_admin_approval(data)
|
||||||
|
|
||||||
|
def find_article_by_title(title: str) -> FeedParserDict | None:
|
||||||
|
feed = feedparser.parse(f"{FRONTEND_URL}/rss.xml")
|
||||||
|
for article in feed.entries:
|
||||||
|
if title == str(article["title"]):
|
||||||
|
return article
|
||||||
|
return None
|
||||||
5
backend/src/backend/internal/user_code_gen.py
Normal file
5
backend/src/backend/internal/user_code_gen.py
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import string
|
||||||
|
import random
|
||||||
|
|
||||||
|
def generate_user_code() -> str:
|
||||||
|
return ''.join(random.choices(string.ascii_uppercase, k=4))
|
||||||
31
backend/src/backend/main.py
Normal file
31
backend/src/backend/main.py
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
|
||||||
|
from dotenv.main import load_dotenv
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
from .routers import users, comments, newsletters, health
|
||||||
|
from backend.internal.database import create_db_and_tables
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi_crons import Crons
|
||||||
|
from backend.internal.rss_reader import check_blog_rss_and_send_admin_mail
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: TLS
|
||||||
|
# TODO: Deploy
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
crons = Crons(app)
|
||||||
|
|
||||||
|
@crons.cron("0 14 * * *", name="RSS-Check")
|
||||||
|
async def check_for_new_article():
|
||||||
|
check_blog_rss_and_send_admin_mail()
|
||||||
|
|
||||||
|
|
||||||
|
@app.on_event("startup")
|
||||||
|
def on_startup():
|
||||||
|
create_db_and_tables()
|
||||||
|
|
||||||
|
|
||||||
|
app.include_router(users.router)
|
||||||
|
app.include_router(comments.router)
|
||||||
|
app.include_router(newsletters.router)
|
||||||
|
app.include_router(health.router)
|
||||||
2
backend/src/backend/models/__init__.py
Normal file
2
backend/src/backend/models/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
from .user import User
|
||||||
|
from .comment import Comment
|
||||||
BIN
backend/src/backend/models/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
backend/src/backend/models/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/src/backend/models/__pycache__/article.cpython-314.pyc
Normal file
BIN
backend/src/backend/models/__pycache__/article.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/src/backend/models/__pycache__/comment.cpython-314.pyc
Normal file
BIN
backend/src/backend/models/__pycache__/comment.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/src/backend/models/__pycache__/user.cpython-314.pyc
Normal file
BIN
backend/src/backend/models/__pycache__/user.cpython-314.pyc
Normal file
Binary file not shown.
22
backend/src/backend/models/comment.py
Normal file
22
backend/src/backend/models/comment.py
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
import uuid
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
from sqlmodel import Relationship
|
||||||
|
from sqlmodel import Field
|
||||||
|
from sqlmodel import SQLModel
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from backend.models.user import User
|
||||||
|
|
||||||
|
class CommentBase(SQLModel):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Comment(CommentBase, table=True):
|
||||||
|
id: uuid.UUID | None = Field(default_factory=uuid.uuid7, primary_key=True)
|
||||||
|
slug: str = Field(index=True)
|
||||||
|
content: str = Field()
|
||||||
|
date_time: datetime = Field(default_factory=datetime.now)
|
||||||
|
user_id: uuid.UUID = Field(foreign_key="user.id")
|
||||||
|
user: "User" = Relationship(back_populates="comments")
|
||||||
5
backend/src/backend/models/reaction.py
Normal file
5
backend/src/backend/models/reaction.py
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
|
||||||
|
# Reaction: slug and visitor_hash[(UUID or IP from Header) and salt] (foreign_key)
|
||||||
|
|
||||||
|
#Reference: https://isaacfei.com/posts/simple-like-system/
|
||||||
|
|
||||||
63
backend/src/backend/models/user.py
Normal file
63
backend/src/backend/models/user.py
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
from sqlmodel._compat import ConfigDict
|
||||||
|
from uuid import uuid7
|
||||||
|
from datetime import datetime
|
||||||
|
from backend.internal.user_code_gen import generate_user_code
|
||||||
|
import uuid
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
from sqlmodel import Field
|
||||||
|
from sqlmodel import Relationship
|
||||||
|
from sqlmodel import SQLModel
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from backend.models.comment import Comment
|
||||||
|
|
||||||
|
class UserBase(SQLModel):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class User(UserBase, table=True):
|
||||||
|
id: uuid.UUID = Field(default_factory=uuid.uuid7, primary_key=True)
|
||||||
|
email: str = Field(unique=True)
|
||||||
|
name: str = Field(unique=True)
|
||||||
|
user_code: str = Field(default_factory=generate_user_code,unique=True)
|
||||||
|
subscription: bool = Field()
|
||||||
|
created_at: datetime = Field(default_factory=datetime.now)
|
||||||
|
|
||||||
|
comments: list["Comment"] = Relationship(back_populates="user")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def factory(cls,
|
||||||
|
id: uuid.UUID | None = None,
|
||||||
|
email: str = "Email@Test.mail",
|
||||||
|
name: str = "TestName",
|
||||||
|
user_code: str = "WYZZ",
|
||||||
|
subscription: bool = False,
|
||||||
|
) -> "User":
|
||||||
|
return cls(
|
||||||
|
id=id or uuid.uuid7(),
|
||||||
|
email=email,
|
||||||
|
name=name,
|
||||||
|
user_code=user_code,
|
||||||
|
subscription=subscription,
|
||||||
|
)
|
||||||
|
|
||||||
|
class NewUser(UserBase):
|
||||||
|
email: str
|
||||||
|
name: str
|
||||||
|
subscription: bool
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def factory(cls,
|
||||||
|
email: str = "Email@Test.mail",
|
||||||
|
name: str = "TestName",
|
||||||
|
subscription: bool = False
|
||||||
|
) -> "NewUser":
|
||||||
|
return cls(
|
||||||
|
email = email,
|
||||||
|
name = name,
|
||||||
|
subscription = subscription
|
||||||
|
)
|
||||||
|
|
||||||
|
class UserInfo(UserBase):
|
||||||
|
name: str
|
||||||
|
email: str
|
||||||
|
subscription: bool
|
||||||
0
backend/src/backend/routers/__init__.py
Normal file
0
backend/src/backend/routers/__init__.py
Normal file
BIN
backend/src/backend/routers/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
backend/src/backend/routers/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
BIN
backend/src/backend/routers/__pycache__/comments.cpython-314.pyc
Normal file
BIN
backend/src/backend/routers/__pycache__/comments.cpython-314.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
backend/src/backend/routers/__pycache__/users.cpython-314.pyc
Normal file
BIN
backend/src/backend/routers/__pycache__/users.cpython-314.pyc
Normal file
Binary file not shown.
18
backend/src/backend/routers/comments.py
Normal file
18
backend/src/backend/routers/comments.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
|
||||||
|
# add comment
|
||||||
|
# delete comment
|
||||||
|
# get comments
|
||||||
|
|
||||||
|
from backend.internal.database import get_session
|
||||||
|
from fastapi import Depends
|
||||||
|
from sqlmodel import Session
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from ..models.comment import Comment
|
||||||
|
|
||||||
|
router = APIRouter(
|
||||||
|
prefix="/comments",
|
||||||
|
tags=["comments"],
|
||||||
|
dependencies=[],
|
||||||
|
responses={404: {"description": "Not found"}},
|
||||||
|
)
|
||||||
|
|
||||||
8
backend/src/backend/routers/health.py
Normal file
8
backend/src/backend/routers/health.py
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/health", include_in_schema=False)
|
||||||
|
def health():
|
||||||
|
return {"status": "ok"}
|
||||||
44
backend/src/backend/routers/newsletters.py
Normal file
44
backend/src/backend/routers/newsletters.py
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
from mailtrap import Address
|
||||||
|
from backend.internal.mail_util import create_html_from_template
|
||||||
|
from mailtrap import BatchEmailRequest
|
||||||
|
from mailtrap import BatchMail
|
||||||
|
from backend.internal.mail_util import send_batch_mail
|
||||||
|
from starlette.status import HTTP_400_BAD_REQUEST
|
||||||
|
from fastapi import HTTPException
|
||||||
|
from sqlmodel import select
|
||||||
|
from backend.models.user import User
|
||||||
|
from sqlmodel import Session
|
||||||
|
from backend.internal.database import get_session
|
||||||
|
from fastapi import Depends
|
||||||
|
from backend.internal.rss_reader import find_article_by_title
|
||||||
|
from backend.internal.authentication import decode_token
|
||||||
|
from fastapi import Form
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
router = APIRouter(
|
||||||
|
prefix="/newsletter",
|
||||||
|
dependencies=[],
|
||||||
|
responses={404: {"description": "Not found"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
path="/approve",
|
||||||
|
status_code=201
|
||||||
|
)
|
||||||
|
def approve_newsletter(secret: str = Form(), session: Session = Depends(get_session)):
|
||||||
|
title = decode_token(secret)
|
||||||
|
if title is not None:
|
||||||
|
article = find_article_by_title(title)
|
||||||
|
if article is not None:
|
||||||
|
subscribed_users = session.exec(select(User).where(User.subscription == True))
|
||||||
|
mails: list[BatchEmailRequest] = []
|
||||||
|
for user in subscribed_users:
|
||||||
|
html = create_html_from_template("newsletter", user)
|
||||||
|
mail = BatchEmailRequest(to=[Address(email=user.email,name=user.name)],html=html)
|
||||||
|
mails.append(mail)
|
||||||
|
send_batch_mail(mails)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise HTTPException(HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
5
backend/src/backend/routers/reactions.py
Normal file
5
backend/src/backend/routers/reactions.py
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
|
||||||
|
|
||||||
|
# addReaction
|
||||||
|
# delete Reaction
|
||||||
|
# get Reactions by slug
|
||||||
102
backend/src/backend/routers/users.py
Normal file
102
backend/src/backend/routers/users.py
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
from mailtrap import Address
|
||||||
|
from backend.internal.mail_util import send_mail
|
||||||
|
from backend.internal.mail_util import create_html_from_template
|
||||||
|
from mailtrap import Mail
|
||||||
|
from pyrate_limiter import Duration
|
||||||
|
from pyrate_limiter import Rate
|
||||||
|
from pyrate_limiter import Limiter
|
||||||
|
from backend.models.user import UserInfo
|
||||||
|
from backend.internal.authentication import get_current_user
|
||||||
|
from backend.internal.authentication import create_access_token
|
||||||
|
from starlette.status import HTTP_400_BAD_REQUEST,HTTP_401_UNAUTHORIZED
|
||||||
|
from fastapi import Form
|
||||||
|
from sqlmodel import select
|
||||||
|
from fastapi import HTTPException
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
from backend.models.user import User
|
||||||
|
from backend.models.user import NewUser
|
||||||
|
from backend.internal.database import get_session
|
||||||
|
from fastapi import Depends
|
||||||
|
from sqlmodel import Session
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from fastapi_limiter.depends import RateLimiter
|
||||||
|
|
||||||
|
router = APIRouter(
|
||||||
|
prefix="/users",
|
||||||
|
dependencies=[],
|
||||||
|
responses={404: {"description": "Not found"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
path="/",
|
||||||
|
status_code=201,
|
||||||
|
dependencies=[Depends(RateLimiter(limiter=Limiter(Rate(1, Duration.MINUTE))))]
|
||||||
|
)
|
||||||
|
def register_user(new_user: NewUser, session: Session = Depends(get_session)):
|
||||||
|
user = User.model_validate(new_user)
|
||||||
|
|
||||||
|
try:
|
||||||
|
session.add(user)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(user)
|
||||||
|
|
||||||
|
mail = Mail(
|
||||||
|
to=[Address(email=user.email,name=user.name)],
|
||||||
|
sender=Address(email="noreply@blauwasser-mausis.de",name="Blauwasser Mausis"),
|
||||||
|
subject=f"[Registrierung] Willkommen {user.name} | Blauwasser Mausis",
|
||||||
|
html=create_html_from_template("welcome", user)
|
||||||
|
)
|
||||||
|
send_mail(mail)
|
||||||
|
|
||||||
|
except IntegrityError as e:
|
||||||
|
session.rollback()
|
||||||
|
raise HTTPException(HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
path="/login",
|
||||||
|
status_code=201,
|
||||||
|
dependencies=[Depends(RateLimiter(limiter=Limiter(Rate(3, Duration.SECOND * 30))))]
|
||||||
|
)
|
||||||
|
def login(user_code: str = Form(), session: Session = Depends(get_session)) -> str:
|
||||||
|
user = session.exec(select(User).where(User.user_code == user_code)).first()
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(HTTP_401_UNAUTHORIZED)
|
||||||
|
return create_access_token({"sub": user.user_code})
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
path="/forgot",
|
||||||
|
dependencies=[Depends(RateLimiter(limiter=Limiter(Rate(1, Duration.MINUTE))))]
|
||||||
|
)
|
||||||
|
def forgot_user_code(email: str = Form(), session: Session = Depends(get_session)):
|
||||||
|
user = session.exec(select(User).where(User.email == email)).first()
|
||||||
|
if user is not None:
|
||||||
|
pass # TODO: send Mail
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(path="/me", status_code=201)
|
||||||
|
def get_current_user_info(current_user: User = Depends(get_current_user)) -> UserInfo:
|
||||||
|
return UserInfo.model_validate(current_user)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete(path="/me", status_code=201)
|
||||||
|
def delete(current_user: User = Depends(get_current_user), session: Session = Depends(get_session)) :
|
||||||
|
session.delete(current_user)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@router.put(path="/me/subscribe", status_code=201)
|
||||||
|
def change_subscription(
|
||||||
|
subscription: bool = Form(),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
session: Session = Depends(get_session)
|
||||||
|
):
|
||||||
|
current_user.subscription = subscription
|
||||||
|
session.add(current_user)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
# TODO:
|
||||||
|
# change mail <- token, new mail
|
||||||
Binary file not shown.
Binary file not shown.
84
backend/src/test/routers/test_users.py
Normal file
84
backend/src/test/routers/test_users.py
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
from backend.models.user import NewUser
|
||||||
|
from sqlmodel import select
|
||||||
|
from backend.models.user import User
|
||||||
|
from backend.internal.database import get_session
|
||||||
|
from sqlmodel import Session
|
||||||
|
from sqlmodel import StaticPool
|
||||||
|
from sqlmodel import create_engine
|
||||||
|
import pytest
|
||||||
|
from backend.internal.database import engine
|
||||||
|
from sqlmodel import SQLModel
|
||||||
|
from backend.main import app
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
@pytest.fixture(name="session")
|
||||||
|
def session_fixture():
|
||||||
|
engine = create_engine(
|
||||||
|
"sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool
|
||||||
|
)
|
||||||
|
SQLModel.metadata.create_all(engine)
|
||||||
|
with Session(engine) as session:
|
||||||
|
yield session
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="client")
|
||||||
|
def client_fixture(session: Session):
|
||||||
|
def get_session_override():
|
||||||
|
return session
|
||||||
|
|
||||||
|
app.dependency_overrides[get_session] = get_session_override
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
yield client
|
||||||
|
app.dependency_overrides.clear()
|
||||||
|
|
||||||
|
def test_register_user_saved(session:Session, client: TestClient):
|
||||||
|
new_user = {
|
||||||
|
"email": "Test@Test.test",
|
||||||
|
"name": "Test1",
|
||||||
|
"subscription": True
|
||||||
|
}
|
||||||
|
response = client.post(url="/users/", json=new_user)
|
||||||
|
assert response.status_code == 201
|
||||||
|
|
||||||
|
db_user = session.exec(select(User).where(User.email == "Test@Test.test")).first()
|
||||||
|
assert db_user
|
||||||
|
assert db_user.email == "Test@Test.test"
|
||||||
|
assert db_user.name == "Test1"
|
||||||
|
assert db_user.subscription == True
|
||||||
|
|
||||||
|
def test_email_not_unique_returns_400(session: Session, client: TestClient):
|
||||||
|
new_user = NewUser.factory(email="NotUnique")
|
||||||
|
|
||||||
|
user = User().factory(email="NotUnique")
|
||||||
|
session.add(user)
|
||||||
|
session.commit()
|
||||||
|
response = client.post(url="/users/", json=new_user.model_dump())
|
||||||
|
assert response.status_code == 400
|
||||||
|
|
||||||
|
def test_name_not_unique_returns_400(session: Session, client: TestClient):
|
||||||
|
new_user = NewUser.factory(email="other@other")
|
||||||
|
|
||||||
|
user = User().factory()
|
||||||
|
session.add(user)
|
||||||
|
session.commit()
|
||||||
|
response = client.post(url="/users/", json=new_user.model_dump())
|
||||||
|
assert response.status_code == 400
|
||||||
|
|
||||||
|
def test_login_successful(session: Session, client: TestClient):
|
||||||
|
user = User().factory()
|
||||||
|
session.add(user)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(user)
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"user_code": user.user_code
|
||||||
|
}
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
url="/login",
|
||||||
|
data=payload
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 201
|
||||||
|
assert response.json()[:2] == "ey"
|
||||||
1694
backend/uv.lock
generated
Normal file
1694
backend/uv.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,247 +0,0 @@
|
||||||
<#
|
|
||||||
.Synopsis
|
|
||||||
Activate a Python virtual environment for the current PowerShell session.
|
|
||||||
|
|
||||||
.Description
|
|
||||||
Pushes the python executable for a virtual environment to the front of the
|
|
||||||
$Env:PATH environment variable and sets the prompt to signify that you are
|
|
||||||
in a Python virtual environment. Makes use of the command line switches as
|
|
||||||
well as the `pyvenv.cfg` file values present in the virtual environment.
|
|
||||||
|
|
||||||
.Parameter VenvDir
|
|
||||||
Path to the directory that contains the virtual environment to activate. The
|
|
||||||
default value for this is the parent of the directory that the Activate.ps1
|
|
||||||
script is located within.
|
|
||||||
|
|
||||||
.Parameter Prompt
|
|
||||||
The prompt prefix to display when this virtual environment is activated. By
|
|
||||||
default, this prompt is the name of the virtual environment folder (VenvDir)
|
|
||||||
surrounded by parentheses and followed by a single space (ie. '(.venv) ').
|
|
||||||
|
|
||||||
.Example
|
|
||||||
Activate.ps1
|
|
||||||
Activates the Python virtual environment that contains the Activate.ps1 script.
|
|
||||||
|
|
||||||
.Example
|
|
||||||
Activate.ps1 -Verbose
|
|
||||||
Activates the Python virtual environment that contains the Activate.ps1 script,
|
|
||||||
and shows extra information about the activation as it executes.
|
|
||||||
|
|
||||||
.Example
|
|
||||||
Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
|
|
||||||
Activates the Python virtual environment located in the specified location.
|
|
||||||
|
|
||||||
.Example
|
|
||||||
Activate.ps1 -Prompt "MyPython"
|
|
||||||
Activates the Python virtual environment that contains the Activate.ps1 script,
|
|
||||||
and prefixes the current prompt with the specified string (surrounded in
|
|
||||||
parentheses) while the virtual environment is active.
|
|
||||||
|
|
||||||
.Notes
|
|
||||||
On Windows, it may be required to enable this Activate.ps1 script by setting the
|
|
||||||
execution policy for the user. You can do this by issuing the following PowerShell
|
|
||||||
command:
|
|
||||||
|
|
||||||
PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
|
||||||
|
|
||||||
For more information on Execution Policies:
|
|
||||||
https://go.microsoft.com/fwlink/?LinkID=135170
|
|
||||||
|
|
||||||
#>
|
|
||||||
Param(
|
|
||||||
[Parameter(Mandatory = $false)]
|
|
||||||
[String]
|
|
||||||
$VenvDir,
|
|
||||||
[Parameter(Mandatory = $false)]
|
|
||||||
[String]
|
|
||||||
$Prompt
|
|
||||||
)
|
|
||||||
|
|
||||||
<# Function declarations --------------------------------------------------- #>
|
|
||||||
|
|
||||||
<#
|
|
||||||
.Synopsis
|
|
||||||
Remove all shell session elements added by the Activate script, including the
|
|
||||||
addition of the virtual environment's Python executable from the beginning of
|
|
||||||
the PATH variable.
|
|
||||||
|
|
||||||
.Parameter NonDestructive
|
|
||||||
If present, do not remove this function from the global namespace for the
|
|
||||||
session.
|
|
||||||
|
|
||||||
#>
|
|
||||||
function global:deactivate ([switch]$NonDestructive) {
|
|
||||||
# Revert to original values
|
|
||||||
|
|
||||||
# The prior prompt:
|
|
||||||
if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
|
|
||||||
Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
|
|
||||||
Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
|
|
||||||
}
|
|
||||||
|
|
||||||
# The prior PYTHONHOME:
|
|
||||||
if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
|
|
||||||
Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
|
|
||||||
Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
|
|
||||||
}
|
|
||||||
|
|
||||||
# The prior PATH:
|
|
||||||
if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
|
|
||||||
Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
|
|
||||||
Remove-Item -Path Env:_OLD_VIRTUAL_PATH
|
|
||||||
}
|
|
||||||
|
|
||||||
# Just remove the VIRTUAL_ENV altogether:
|
|
||||||
if (Test-Path -Path Env:VIRTUAL_ENV) {
|
|
||||||
Remove-Item -Path env:VIRTUAL_ENV
|
|
||||||
}
|
|
||||||
|
|
||||||
# Just remove VIRTUAL_ENV_PROMPT altogether.
|
|
||||||
if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
|
|
||||||
Remove-Item -Path env:VIRTUAL_ENV_PROMPT
|
|
||||||
}
|
|
||||||
|
|
||||||
# Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
|
|
||||||
if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
|
|
||||||
Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
|
|
||||||
}
|
|
||||||
|
|
||||||
# Leave deactivate function in the global namespace if requested:
|
|
||||||
if (-not $NonDestructive) {
|
|
||||||
Remove-Item -Path function:deactivate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
<#
|
|
||||||
.Description
|
|
||||||
Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
|
|
||||||
given folder, and returns them in a map.
|
|
||||||
|
|
||||||
For each line in the pyvenv.cfg file, if that line can be parsed into exactly
|
|
||||||
two strings separated by `=` (with any amount of whitespace surrounding the =)
|
|
||||||
then it is considered a `key = value` line. The left hand string is the key,
|
|
||||||
the right hand is the value.
|
|
||||||
|
|
||||||
If the value starts with a `'` or a `"` then the first and last character is
|
|
||||||
stripped from the value before being captured.
|
|
||||||
|
|
||||||
.Parameter ConfigDir
|
|
||||||
Path to the directory that contains the `pyvenv.cfg` file.
|
|
||||||
#>
|
|
||||||
function Get-PyVenvConfig(
|
|
||||||
[String]
|
|
||||||
$ConfigDir
|
|
||||||
) {
|
|
||||||
Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
|
|
||||||
|
|
||||||
# Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
|
|
||||||
$pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
|
|
||||||
|
|
||||||
# An empty map will be returned if no config file is found.
|
|
||||||
$pyvenvConfig = @{ }
|
|
||||||
|
|
||||||
if ($pyvenvConfigPath) {
|
|
||||||
|
|
||||||
Write-Verbose "File exists, parse `key = value` lines"
|
|
||||||
$pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
|
|
||||||
|
|
||||||
$pyvenvConfigContent | ForEach-Object {
|
|
||||||
$keyval = $PSItem -split "\s*=\s*", 2
|
|
||||||
if ($keyval[0] -and $keyval[1]) {
|
|
||||||
$val = $keyval[1]
|
|
||||||
|
|
||||||
# Remove extraneous quotations around a string value.
|
|
||||||
if ("'""".Contains($val.Substring(0, 1))) {
|
|
||||||
$val = $val.Substring(1, $val.Length - 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
$pyvenvConfig[$keyval[0]] = $val
|
|
||||||
Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $pyvenvConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
<# Begin Activate script --------------------------------------------------- #>
|
|
||||||
|
|
||||||
# Determine the containing directory of this script
|
|
||||||
$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
|
||||||
$VenvExecDir = Get-Item -Path $VenvExecPath
|
|
||||||
|
|
||||||
Write-Verbose "Activation script is located in path: '$VenvExecPath'"
|
|
||||||
Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
|
|
||||||
Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
|
|
||||||
|
|
||||||
# Set values required in priority: CmdLine, ConfigFile, Default
|
|
||||||
# First, get the location of the virtual environment, it might not be
|
|
||||||
# VenvExecDir if specified on the command line.
|
|
||||||
if ($VenvDir) {
|
|
||||||
Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
|
|
||||||
$VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
|
|
||||||
Write-Verbose "VenvDir=$VenvDir"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Next, read the `pyvenv.cfg` file to determine any required value such
|
|
||||||
# as `prompt`.
|
|
||||||
$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
|
|
||||||
|
|
||||||
# Next, set the prompt from the command line, or the config file, or
|
|
||||||
# just use the name of the virtual environment folder.
|
|
||||||
if ($Prompt) {
|
|
||||||
Write-Verbose "Prompt specified as argument, using '$Prompt'"
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
|
|
||||||
if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
|
|
||||||
Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
|
|
||||||
$Prompt = $pyvenvCfg['prompt'];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)"
|
|
||||||
Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
|
|
||||||
$Prompt = Split-Path -Path $venvDir -Leaf
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Verbose "Prompt = '$Prompt'"
|
|
||||||
Write-Verbose "VenvDir='$VenvDir'"
|
|
||||||
|
|
||||||
# Deactivate any currently active virtual environment, but leave the
|
|
||||||
# deactivate function in place.
|
|
||||||
deactivate -nondestructive
|
|
||||||
|
|
||||||
# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
|
|
||||||
# that there is an activated venv.
|
|
||||||
$env:VIRTUAL_ENV = $VenvDir
|
|
||||||
|
|
||||||
if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
|
|
||||||
|
|
||||||
Write-Verbose "Setting prompt to '$Prompt'"
|
|
||||||
|
|
||||||
# Set the prompt to include the env name
|
|
||||||
# Make sure _OLD_VIRTUAL_PROMPT is global
|
|
||||||
function global:_OLD_VIRTUAL_PROMPT { "" }
|
|
||||||
Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
|
|
||||||
New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
|
|
||||||
|
|
||||||
function global:prompt {
|
|
||||||
Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
|
|
||||||
_OLD_VIRTUAL_PROMPT
|
|
||||||
}
|
|
||||||
$env:VIRTUAL_ENV_PROMPT = $Prompt
|
|
||||||
}
|
|
||||||
|
|
||||||
# Clear PYTHONHOME
|
|
||||||
if (Test-Path -Path Env:PYTHONHOME) {
|
|
||||||
Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
|
|
||||||
Remove-Item -Path Env:PYTHONHOME
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add the venv to the PATH
|
|
||||||
Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
|
|
||||||
$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
# This file must be used with "source bin/activate" *from bash*
|
|
||||||
# You cannot run it directly
|
|
||||||
|
|
||||||
deactivate () {
|
|
||||||
# reset old environment variables
|
|
||||||
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
|
|
||||||
PATH="${_OLD_VIRTUAL_PATH:-}"
|
|
||||||
export PATH
|
|
||||||
unset _OLD_VIRTUAL_PATH
|
|
||||||
fi
|
|
||||||
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
|
|
||||||
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
|
|
||||||
export PYTHONHOME
|
|
||||||
unset _OLD_VIRTUAL_PYTHONHOME
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Call hash to forget past commands. Without forgetting
|
|
||||||
# past commands the $PATH changes we made may not be respected
|
|
||||||
hash -r 2> /dev/null
|
|
||||||
|
|
||||||
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
|
|
||||||
PS1="${_OLD_VIRTUAL_PS1:-}"
|
|
||||||
export PS1
|
|
||||||
unset _OLD_VIRTUAL_PS1
|
|
||||||
fi
|
|
||||||
|
|
||||||
unset VIRTUAL_ENV
|
|
||||||
unset VIRTUAL_ENV_PROMPT
|
|
||||||
if [ ! "${1:-}" = "nondestructive" ] ; then
|
|
||||||
# Self destruct!
|
|
||||||
unset -f deactivate
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# unset irrelevant variables
|
|
||||||
deactivate nondestructive
|
|
||||||
|
|
||||||
# on Windows, a path can contain colons and backslashes and has to be converted:
|
|
||||||
if [ "${OSTYPE:-}" = "cygwin" ] || [ "${OSTYPE:-}" = "msys" ] ; then
|
|
||||||
# transform D:\path\to\venv to /d/path/to/venv on MSYS
|
|
||||||
# and to /cygdrive/d/path/to/venv on Cygwin
|
|
||||||
export VIRTUAL_ENV=$(cygpath /home/ebby/Desktop/Bluewaterblog/backend/venv)
|
|
||||||
else
|
|
||||||
# use the path as-is
|
|
||||||
export VIRTUAL_ENV=/home/ebby/Desktop/Bluewaterblog/backend/venv
|
|
||||||
fi
|
|
||||||
|
|
||||||
_OLD_VIRTUAL_PATH="$PATH"
|
|
||||||
PATH="$VIRTUAL_ENV/"bin":$PATH"
|
|
||||||
export PATH
|
|
||||||
|
|
||||||
# unset PYTHONHOME if set
|
|
||||||
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
|
|
||||||
# could use `if (set -u; : $PYTHONHOME) ;` in bash
|
|
||||||
if [ -n "${PYTHONHOME:-}" ] ; then
|
|
||||||
_OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
|
|
||||||
unset PYTHONHOME
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
|
|
||||||
_OLD_VIRTUAL_PS1="${PS1:-}"
|
|
||||||
PS1='(venv) '"${PS1:-}"
|
|
||||||
export PS1
|
|
||||||
VIRTUAL_ENV_PROMPT='(venv) '
|
|
||||||
export VIRTUAL_ENV_PROMPT
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Call hash to forget past commands. Without forgetting
|
|
||||||
# past commands the $PATH changes we made may not be respected
|
|
||||||
hash -r 2> /dev/null
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
# This file must be used with "source bin/activate.csh" *from csh*.
|
|
||||||
# You cannot run it directly.
|
|
||||||
|
|
||||||
# Created by Davide Di Blasi <davidedb@gmail.com>.
|
|
||||||
# Ported to Python 3.3 venv by Andrew Svetlov <andrew.svetlov@gmail.com>
|
|
||||||
|
|
||||||
alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate'
|
|
||||||
|
|
||||||
# Unset irrelevant variables.
|
|
||||||
deactivate nondestructive
|
|
||||||
|
|
||||||
setenv VIRTUAL_ENV /home/ebby/Desktop/Bluewaterblog/backend/venv
|
|
||||||
|
|
||||||
set _OLD_VIRTUAL_PATH="$PATH"
|
|
||||||
setenv PATH "$VIRTUAL_ENV/"bin":$PATH"
|
|
||||||
|
|
||||||
|
|
||||||
set _OLD_VIRTUAL_PROMPT="$prompt"
|
|
||||||
|
|
||||||
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
|
|
||||||
set prompt = '(venv) '"$prompt"
|
|
||||||
setenv VIRTUAL_ENV_PROMPT '(venv) '
|
|
||||||
endif
|
|
||||||
|
|
||||||
alias pydoc python -m pydoc
|
|
||||||
|
|
||||||
rehash
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
# This file must be used with "source <venv>/bin/activate.fish" *from fish*
|
|
||||||
# (https://fishshell.com/). You cannot run it directly.
|
|
||||||
|
|
||||||
function deactivate -d "Exit virtual environment and return to normal shell environment"
|
|
||||||
# reset old environment variables
|
|
||||||
if test -n "$_OLD_VIRTUAL_PATH"
|
|
||||||
set -gx PATH $_OLD_VIRTUAL_PATH
|
|
||||||
set -e _OLD_VIRTUAL_PATH
|
|
||||||
end
|
|
||||||
if test -n "$_OLD_VIRTUAL_PYTHONHOME"
|
|
||||||
set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
|
|
||||||
set -e _OLD_VIRTUAL_PYTHONHOME
|
|
||||||
end
|
|
||||||
|
|
||||||
if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
|
|
||||||
set -e _OLD_FISH_PROMPT_OVERRIDE
|
|
||||||
# prevents error when using nested fish instances (Issue #93858)
|
|
||||||
if functions -q _old_fish_prompt
|
|
||||||
functions -e fish_prompt
|
|
||||||
functions -c _old_fish_prompt fish_prompt
|
|
||||||
functions -e _old_fish_prompt
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
set -e VIRTUAL_ENV
|
|
||||||
set -e VIRTUAL_ENV_PROMPT
|
|
||||||
if test "$argv[1]" != "nondestructive"
|
|
||||||
# Self-destruct!
|
|
||||||
functions -e deactivate
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Unset irrelevant variables.
|
|
||||||
deactivate nondestructive
|
|
||||||
|
|
||||||
set -gx VIRTUAL_ENV /home/ebby/Desktop/Bluewaterblog/backend/venv
|
|
||||||
|
|
||||||
set -gx _OLD_VIRTUAL_PATH $PATH
|
|
||||||
set -gx PATH "$VIRTUAL_ENV/"bin $PATH
|
|
||||||
|
|
||||||
# Unset PYTHONHOME if set.
|
|
||||||
if set -q PYTHONHOME
|
|
||||||
set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
|
|
||||||
set -e PYTHONHOME
|
|
||||||
end
|
|
||||||
|
|
||||||
if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
|
|
||||||
# fish uses a function instead of an env var to generate the prompt.
|
|
||||||
|
|
||||||
# Save the current fish_prompt function as the function _old_fish_prompt.
|
|
||||||
functions -c fish_prompt _old_fish_prompt
|
|
||||||
|
|
||||||
# With the original prompt function renamed, we can override with our own.
|
|
||||||
function fish_prompt
|
|
||||||
# Save the return status of the last command.
|
|
||||||
set -l old_status $status
|
|
||||||
|
|
||||||
# Output the venv prompt; color taken from the blue of the Python logo.
|
|
||||||
printf "%s%s%s" (set_color 4B8BBE) '(venv) ' (set_color normal)
|
|
||||||
|
|
||||||
# Restore the return status of the previous command.
|
|
||||||
echo "exit $old_status" | .
|
|
||||||
# Output the original/"old" prompt.
|
|
||||||
_old_fish_prompt
|
|
||||||
end
|
|
||||||
|
|
||||||
set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
|
|
||||||
set -gx VIRTUAL_ENV_PROMPT '(venv) '
|
|
||||||
end
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
#!/home/ebby/Desktop/Bluewaterblog/backend/venv/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
from idna.cli import main
|
|
||||||
if __name__ == '__main__':
|
|
||||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
|
||||||
sys.exit(main())
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
#!/home/ebby/Desktop/Bluewaterblog/backend/venv/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
from nexios.cli import cli
|
|
||||||
if __name__ == '__main__':
|
|
||||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
|
||||||
sys.exit(cli())
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
#!/home/ebby/Desktop/Bluewaterblog/backend/venv/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
from pip._internal.cli.main import main
|
|
||||||
if __name__ == '__main__':
|
|
||||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
|
||||||
sys.exit(main())
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
#!/home/ebby/Desktop/Bluewaterblog/backend/venv/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
from pip._internal.cli.main import main
|
|
||||||
if __name__ == '__main__':
|
|
||||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
|
||||||
sys.exit(main())
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
#!/home/ebby/Desktop/Bluewaterblog/backend/venv/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
from pip._internal.cli.main import main
|
|
||||||
if __name__ == '__main__':
|
|
||||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
|
||||||
sys.exit(main())
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
/usr/bin/python
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
python
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
python
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
#!/home/ebby/Desktop/Bluewaterblog/backend/venv/bin/python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
from uvicorn.main import main
|
|
||||||
if __name__ == '__main__':
|
|
||||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
|
||||||
sys.exit(main())
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
lib
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
home = /usr/bin
|
|
||||||
include-system-site-packages = false
|
|
||||||
version = 3.12.3
|
|
||||||
executable = /usr/bin/python3.12
|
|
||||||
command = /usr/bin/python -m venv /home/ebby/Desktop/Bluewaterblog/backend/venv
|
|
||||||
|
|
@ -12,10 +12,17 @@ export async function GET(context) {
|
||||||
site: context.site,
|
site: context.site,
|
||||||
items: posts.map(post => ({
|
items: posts.map(post => ({
|
||||||
title: post.frontmatter.title,
|
title: post.frontmatter.title,
|
||||||
subtitle: post.frontmatter.subtitle,
|
|
||||||
pubDate: Date.parse(post.frontmatter.date),
|
pubDate: Date.parse(post.frontmatter.date),
|
||||||
description: post.frontmatter.description,
|
description: post.frontmatter.description,
|
||||||
link: `/blog/${post.frontmatter.slug}`
|
link: `/blog/${post.frontmatter.slug}`,
|
||||||
|
|
||||||
|
|
||||||
|
customData: `
|
||||||
|
<subtitle>
|
||||||
|
<![CDATA[${post.frontmatter.subtitle}]]>
|
||||||
|
</subtitle>
|
||||||
|
|
||||||
|
`,
|
||||||
})),
|
})),
|
||||||
customData: `<language>de-de</language>`,
|
customData: `<language>de-de</language>`,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue