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
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -1,3 +1 @@
|
|||
node_modules
|
||||
backend/venv/lib/**
|
||||
backend/venv/lib64/*
|
||||
node_modules
|
||||
9
.vscode/settings.json
vendored
9
.vscode/settings.json
vendored
|
|
@ -2,9 +2,16 @@
|
|||
"cSpell.language": "en,de-DE",
|
||||
"cSpell.words": [
|
||||
"Chiara",
|
||||
"dotenv",
|
||||
"Hobbithöhle",
|
||||
"mailtrap",
|
||||
"Masala",
|
||||
"Pomorie",
|
||||
"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,
|
||||
items: posts.map(post => ({
|
||||
title: post.frontmatter.title,
|
||||
subtitle: post.frontmatter.subtitle,
|
||||
pubDate: Date.parse(post.frontmatter.date),
|
||||
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>`,
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue