mirror of
https://github.com/revanced/revanced-api.git
synced 2025-04-29 22:24:31 +02:00
feat: Add announcements endpoints (#91)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Alexandre Teles (afterSt0rm) <alexandre.teles@ufba.br>
This commit is contained in:
parent
c65b43aff3
commit
8583e2a2bb
54
.github/workflows/codeql.yml
vendored
Normal file
54
.github/workflows/codeql.yml
vendored
Normal file
@ -0,0 +1,54 @@
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [dev]
|
||||
pull_request:
|
||||
types: [opened, reopened, edited, synchronize]
|
||||
schedule:
|
||||
- cron: "29 5 * * 5"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
|
||||
timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: ["python"]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.11.6"
|
||||
|
||||
- name: Install project dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
if [ -f requirements.txt ];
|
||||
then pip install -r requirements.txt;
|
||||
fi
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: security-and-quality
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
2
.github/workflows/pytest.yml
vendored
2
.github/workflows/pytest.yml
vendored
@ -26,7 +26,7 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.11.4"
|
||||
python-version: "3.11.6"
|
||||
|
||||
- name: Install project dependencies
|
||||
run: |
|
||||
|
45
.github/workflows/qodana.yml
vendored
45
.github/workflows/qodana.yml
vendored
@ -1,45 +0,0 @@
|
||||
name: "Qodana | Code Quality Scan and Static Analysis"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [dev]
|
||||
pull_request:
|
||||
types: [opened, reopened, edited, synchronize]
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
default_branch: dev
|
||||
|
||||
jobs:
|
||||
qodana:
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.11.4"
|
||||
|
||||
- name: Install project dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
if [ -f requirements.txt ];
|
||||
then pip install -r requirements.txt;
|
||||
fi
|
||||
|
||||
- name: "Qodana Scan"
|
||||
uses: JetBrains/qodana-action@v2023.2.8
|
||||
env:
|
||||
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}
|
||||
with:
|
||||
args: --baseline,qodana.sarif.json
|
||||
|
||||
- name: "Upload Qodana Report"
|
||||
uses: github/codeql-action/upload-sarif@v2
|
||||
with:
|
||||
sarif_file: ${{ runner.temp }}/qodana/results/qodana.sarif.json
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -161,3 +161,4 @@ cython_debug/
|
||||
|
||||
# custom
|
||||
env.sh
|
||||
persistance/database.db
|
||||
|
@ -16,11 +16,5 @@ repos:
|
||||
hooks:
|
||||
- id: black
|
||||
language_version: python3.11
|
||||
- repo: https://github.com/pryorda/dockerfilelint-precommit-hooks
|
||||
rev: v0.1.0
|
||||
hooks:
|
||||
- id: dockerfilelint
|
||||
stages: [commit]
|
||||
|
||||
ci:
|
||||
autoupdate_branch: "dev"
|
||||
|
@ -4,7 +4,7 @@ FROM python:3.11-slim as dependencies
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends gcc \
|
||||
apt-get install -y --no-install-recommends gcc git \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN python -m venv /opt/venv
|
||||
@ -28,6 +28,8 @@ ENV PATH="/opt/venv/bin:$PATH"
|
||||
COPY --from=dependencies /opt/venv /opt/venv
|
||||
COPY . .
|
||||
|
||||
VOLUME persistance
|
||||
|
||||
CMD docker/run-backend.sh
|
||||
HEALTHCHECK CMD docker/run-healthcheck.sh
|
||||
|
||||
|
@ -20,7 +20,11 @@ To run this API, you need Python 3.11.x. You can install the dependencies with p
|
||||
poetry install
|
||||
```
|
||||
|
||||
Create an environment variable called `GITHUB_TOKEN` with a valid GitHub token with read access to public repositories.
|
||||
Create the following environment variables:
|
||||
|
||||
- `GITHUB_TOKEN` with a valid GitHub token with read access to public repositories
|
||||
- `SECRET_KEY` to salt login sessions
|
||||
- `USERNAME` & `PASSWORD` to initialize the database with a user to login with to authenticated endpoints
|
||||
|
||||
Then, you can run the API in development mode with:
|
||||
|
||||
|
@ -7,5 +7,9 @@ from api.socials import socials
|
||||
from api.info import info
|
||||
from api.compat import github as compat
|
||||
from api.donations import donations
|
||||
from api.announcements import announcements
|
||||
from api.login import login
|
||||
|
||||
api = Blueprint.group(ping, github, info, socials, donations, compat, url_prefix="/")
|
||||
api = Blueprint.group(
|
||||
login, ping, github, info, socials, donations, announcements, compat, url_prefix="/"
|
||||
)
|
||||
|
242
api/announcements.py
Normal file
242
api/announcements.py
Normal file
@ -0,0 +1,242 @@
|
||||
"""
|
||||
This module provides a blueprint for the announcements endpoint.
|
||||
|
||||
Routes:
|
||||
- GET /announcements: Get a list of announcements from all channels.
|
||||
- GET /announcements/<channel:str>: Get a list of announcement from a channel.
|
||||
- GET /announcements/latest: Get the latest announcement.
|
||||
- GET /announcements/<channel:str>/latest: Get the latest announcement from a channel.
|
||||
- POST /announcements/<channel:str>: Create an announcement.
|
||||
- DELETE /announcements/<announcement_id:int>: Delete an announcement.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
from sanic import Blueprint, Request
|
||||
from sanic.response import JSONResponse, json
|
||||
from sanic_ext import openapi
|
||||
from data.database import Session
|
||||
from data.models import AnnouncementDbModel, AttachmentDbModel
|
||||
|
||||
import sanic_beskar
|
||||
|
||||
from api.models.announcements import AnnouncementResponseModel
|
||||
from config import api_version
|
||||
from limiter import limiter
|
||||
|
||||
announcements: Blueprint = Blueprint("announcements", version=api_version)
|
||||
|
||||
|
||||
@announcements.get("/announcements")
|
||||
@openapi.definition(
|
||||
summary="Get a list of announcements",
|
||||
response=[[AnnouncementResponseModel]],
|
||||
)
|
||||
async def get_announcements(request: Request) -> JSONResponse:
|
||||
"""
|
||||
Retrieve a list of announcements.
|
||||
|
||||
**Returns:**
|
||||
- JSONResponse: A Sanic JSONResponse object containing a list of announcements from all channels.
|
||||
"""
|
||||
|
||||
session = Session()
|
||||
|
||||
announcements = [
|
||||
AnnouncementResponseModel.to_response(announcement)
|
||||
for announcement in session.query(AnnouncementDbModel).all()
|
||||
]
|
||||
|
||||
session.close()
|
||||
|
||||
return json(announcements, status=200)
|
||||
|
||||
|
||||
@announcements.get("/announcements/<channel:str>")
|
||||
@openapi.definition(
|
||||
summary="Get a list of announcements from a channel",
|
||||
response=[[AnnouncementResponseModel]],
|
||||
)
|
||||
async def get_announcements_for_channel(request: Request, channel: str) -> JSONResponse:
|
||||
"""
|
||||
Retrieve a list of announcements from a channel.
|
||||
|
||||
**Args:**
|
||||
- channel (str): The channel to retrieve announcements from.
|
||||
|
||||
**Returns:**
|
||||
- JSONResponse: A Sanic JSONResponse object containing a list of announcements from a channel.
|
||||
"""
|
||||
|
||||
session = Session()
|
||||
|
||||
announcements = [
|
||||
AnnouncementResponseModel.to_response(announcement)
|
||||
for announcement in session.query(AnnouncementDbModel)
|
||||
.filter_by(channel=channel)
|
||||
.all()
|
||||
]
|
||||
|
||||
session.close()
|
||||
|
||||
return json(announcements, status=200)
|
||||
|
||||
|
||||
@announcements.get("/announcements/latest")
|
||||
@openapi.definition(
|
||||
summary="Get the latest announcement",
|
||||
response=AnnouncementResponseModel,
|
||||
)
|
||||
async def get_latest_announcement(request: Request) -> JSONResponse:
|
||||
"""
|
||||
Retrieve the latest announcement.
|
||||
|
||||
**Returns:**
|
||||
- JSONResponse: A Sanic JSONResponse object containing the latest announcement.
|
||||
"""
|
||||
|
||||
session = Session()
|
||||
|
||||
announcement = (
|
||||
session.query(AnnouncementDbModel)
|
||||
.order_by(AnnouncementDbModel.id.desc())
|
||||
.first()
|
||||
)
|
||||
|
||||
if not announcement:
|
||||
return json({"error": "No announcement found"}, status=404)
|
||||
|
||||
announcement_response = AnnouncementResponseModel.to_response(announcement)
|
||||
|
||||
session.close()
|
||||
|
||||
return json(announcement_response, status=200)
|
||||
|
||||
|
||||
# for specific channel
|
||||
|
||||
|
||||
@announcements.get("/announcements/<channel:str>/latest")
|
||||
@openapi.definition(
|
||||
summary="Get the latest announcement from a channel",
|
||||
response=AnnouncementResponseModel,
|
||||
)
|
||||
async def get_latest_announcement_for_channel(
|
||||
request: Request, channel: str
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
Retrieve the latest announcement from a channel.
|
||||
|
||||
**Args:**
|
||||
- channel (str): The channel to retrieve the latest announcement from.
|
||||
|
||||
**Returns:**
|
||||
- JSONResponse: A Sanic JSONResponse object containing the latest announcement from a channel.
|
||||
"""
|
||||
|
||||
session = Session()
|
||||
|
||||
announcement = (
|
||||
session.query(AnnouncementDbModel)
|
||||
.filter_by(channel=channel)
|
||||
.order_by(AnnouncementDbModel.id.desc())
|
||||
.first()
|
||||
)
|
||||
|
||||
if not announcement:
|
||||
return json({"error": "No announcement found"}, status=404)
|
||||
|
||||
announcement_response = AnnouncementResponseModel.to_response(announcement)
|
||||
|
||||
session.close()
|
||||
|
||||
return json(announcement_response, status=200)
|
||||
|
||||
|
||||
@announcements.post("/announcements/<channel:str>")
|
||||
@limiter.limit("16 per hour")
|
||||
@sanic_beskar.auth_required
|
||||
@openapi.definition(
|
||||
summary="Create an announcement",
|
||||
body=AnnouncementResponseModel,
|
||||
response=AnnouncementResponseModel,
|
||||
)
|
||||
async def post_announcement(request: Request, channel: str) -> JSONResponse:
|
||||
"""
|
||||
Create an announcement.
|
||||
|
||||
**Args:**
|
||||
- author (str | None): The author of the announcement.
|
||||
- title (str): The title of the announcement.
|
||||
- content (ContentFields | None): The content of the announcement.
|
||||
- channel (str): The channel to create the announcement in.
|
||||
- nevel (int | None): The severity of the announcement.
|
||||
"""
|
||||
session = Session()
|
||||
|
||||
if not request.json:
|
||||
return json({"error": "Missing request body"}, status=400)
|
||||
|
||||
content = request.json.get("content", None)
|
||||
|
||||
author = request.json.get("author", None)
|
||||
title = request.json.get("title")
|
||||
message = content["message"] if content and "message" in content else None
|
||||
attachments = (
|
||||
list(
|
||||
map(
|
||||
lambda url: AttachmentDbModel(attachment_url=url),
|
||||
content["attachments"],
|
||||
)
|
||||
)
|
||||
if content and "attachments" in content
|
||||
else []
|
||||
)
|
||||
level = request.json.get("level", None)
|
||||
created_at = datetime.datetime.now()
|
||||
|
||||
announcement = AnnouncementDbModel(
|
||||
author=author,
|
||||
title=title,
|
||||
message=message,
|
||||
attachments=attachments,
|
||||
channel=channel,
|
||||
created_at=created_at,
|
||||
level=level,
|
||||
)
|
||||
|
||||
session.add(announcement)
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
return json({}, status=200)
|
||||
|
||||
|
||||
@announcements.delete("/announcements/<announcement_id:int>")
|
||||
@sanic_beskar.auth_required
|
||||
@openapi.definition(
|
||||
summary="Delete an announcement",
|
||||
)
|
||||
async def delete_announcement(request: Request, announcement_id: int) -> JSONResponse:
|
||||
"""
|
||||
Delete an announcement.
|
||||
|
||||
**Args:**
|
||||
- announcement_id (int): The ID of the announcement to delete.
|
||||
|
||||
**Exceptions:**
|
||||
- 404: Announcement not found.
|
||||
"""
|
||||
session = Session()
|
||||
|
||||
announcement = (
|
||||
session.query(AnnouncementDbModel).filter_by(id=announcement_id).first()
|
||||
)
|
||||
|
||||
if not announcement:
|
||||
return json({"error": "Announcement not found"}, status=404)
|
||||
|
||||
session.delete(announcement)
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
return json({}, status=200)
|
54
api/login.py
Normal file
54
api/login.py
Normal file
@ -0,0 +1,54 @@
|
||||
"""
|
||||
This module provides a blueprint for the login endpoint.
|
||||
|
||||
Routes:
|
||||
- POST /login: Login to the API
|
||||
"""
|
||||
|
||||
from sanic import Blueprint, Request
|
||||
from sanic.response import JSONResponse, json
|
||||
from sanic_ext import openapi
|
||||
from sanic_beskar.exceptions import AuthenticationError
|
||||
|
||||
from auth import beskar
|
||||
from limiter import limiter
|
||||
|
||||
from config import api_version
|
||||
|
||||
|
||||
login: Blueprint = Blueprint("login", version=api_version)
|
||||
|
||||
|
||||
@login.post("/login")
|
||||
@openapi.definition(
|
||||
summary="Login to the API",
|
||||
)
|
||||
@limiter.limit("3 per hour")
|
||||
async def login_user(request: Request) -> JSONResponse:
|
||||
"""
|
||||
Login to the API.
|
||||
|
||||
**Args:**
|
||||
- username (str): The username of the user to login.
|
||||
- password (str): The password of the user to login.
|
||||
|
||||
**Returns:**
|
||||
- JSONResponse: A Sanic JSONResponse object containing the access token.
|
||||
"""
|
||||
|
||||
req = request.json
|
||||
username = req.get("username", None)
|
||||
password = req.get("password", None)
|
||||
if not username or not password:
|
||||
return json({"error": "Missing username or password"}, status=400)
|
||||
|
||||
try:
|
||||
user = await beskar.authenticate(username, password)
|
||||
except AuthenticationError:
|
||||
return json({"error": "Invalid username or password"}, status=403)
|
||||
|
||||
if not user:
|
||||
return json({"error": "Invalid username or password"}, status=403)
|
||||
|
||||
ret = {"access_token": await beskar.encode_token(user)}
|
||||
return json(ret, status=200)
|
37
api/models/announcements.py
Normal file
37
api/models/announcements.py
Normal file
@ -0,0 +1,37 @@
|
||||
from data.models import AnnouncementDbModel
|
||||
|
||||
|
||||
class ContentFields(dict):
|
||||
message: str | None
|
||||
attachment_urls: list[str] | None
|
||||
|
||||
|
||||
class AnnouncementResponseModel(dict):
|
||||
id: int
|
||||
author: str | None
|
||||
title: str
|
||||
content: ContentFields | None
|
||||
channel: str
|
||||
created_at: str
|
||||
level: int | None
|
||||
|
||||
@staticmethod
|
||||
def to_response(announcement: AnnouncementDbModel):
|
||||
response = AnnouncementResponseModel(
|
||||
id=announcement.id,
|
||||
author=announcement.author,
|
||||
title=announcement.title,
|
||||
content=ContentFields(
|
||||
message=announcement.message,
|
||||
attachment_urls=[
|
||||
attachment.attachment_url for attachment in announcement.attachments
|
||||
],
|
||||
)
|
||||
if announcement.message or announcement.attachments
|
||||
else None,
|
||||
channel=announcement.channel,
|
||||
created_at=str(announcement.created_at),
|
||||
level=announcement.level,
|
||||
)
|
||||
|
||||
return response
|
10
app.py
10
app.py
@ -6,6 +6,9 @@ from sanic_ext import Config
|
||||
from api import api
|
||||
from config import *
|
||||
|
||||
from limiter import configure_limiter
|
||||
from auth import configure_auth
|
||||
|
||||
REDIRECTS = {
|
||||
"/": "/docs/swagger",
|
||||
}
|
||||
@ -25,8 +28,13 @@ app.config.CORS_SUPPORTS_CREDENTIALS = True
|
||||
app.config.CORS_SEND_WILDCARD = True
|
||||
app.config.CORS_ORIGINS = "*"
|
||||
|
||||
app.blueprint(api)
|
||||
# sanic-beskar
|
||||
configure_auth(app)
|
||||
|
||||
# sanic-limiter
|
||||
configure_limiter(app)
|
||||
|
||||
app.blueprint(api)
|
||||
|
||||
# https://sanic.dev/en/guide/how-to/static-redirects.html
|
||||
|
||||
|
40
auth.py
Normal file
40
auth.py
Normal file
@ -0,0 +1,40 @@
|
||||
import os
|
||||
import secrets
|
||||
import string
|
||||
from data.database import Session
|
||||
|
||||
from sanic_beskar import Beskar
|
||||
|
||||
from data.models import UserDbModel
|
||||
|
||||
beskar = Beskar()
|
||||
|
||||
|
||||
def configure_auth(app):
|
||||
app.config.SECRET_KEY = os.environ.get("SECRET_KEY").join(
|
||||
secrets.choice(string.ascii_letters) for i in range(15)
|
||||
)
|
||||
app.config["TOKEN_ACCESS_LIFESPAN"] = {"hours": 24}
|
||||
app.config["TOKEN_REFRESH_LIFESPAN"] = {"days": 30}
|
||||
beskar.init_app(app, UserDbModel)
|
||||
|
||||
_init_default_user()
|
||||
|
||||
|
||||
def _init_default_user():
|
||||
username = os.environ.get("USERNAME")
|
||||
password = os.environ.get("PASSWORD")
|
||||
|
||||
if not username or not password:
|
||||
raise Exception("Missing USERNAME or PASSWORD environment variables")
|
||||
|
||||
session = Session()
|
||||
|
||||
existing_user = session.query(UserDbModel).filter_by(username=username).first()
|
||||
if not existing_user:
|
||||
session.add(
|
||||
UserDbModel(username=username, password=beskar.hash_password(password))
|
||||
)
|
||||
session.commit()
|
||||
|
||||
session.close()
|
6
data/database.py
Normal file
6
data/database.py
Normal file
@ -0,0 +1,6 @@
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
engine = create_engine("sqlite:///persistance/database.db")
|
||||
|
||||
Session = sessionmaker(bind=engine)
|
77
data/models.py
Normal file
77
data/models.py
Normal file
@ -0,0 +1,77 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy import ForeignKey
|
||||
|
||||
from data.database import Session, engine
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
class AnnouncementDbModel(Base):
|
||||
__tablename__ = "announcements"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
author = Column(String, nullable=True)
|
||||
title = Column(String, nullable=False)
|
||||
message = Column(String, nullable=True)
|
||||
attachments = relationship("AttachmentDbModel", back_populates="announcements")
|
||||
channel = Column(String, nullable=True)
|
||||
created_at = Column(DateTime, nullable=False)
|
||||
level = Column(Integer, nullable=True)
|
||||
|
||||
|
||||
class AttachmentDbModel(Base):
|
||||
__tablename__ = "attachments"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
announcement_id = Column(Integer, ForeignKey("announcements.id"))
|
||||
attachment_url = Column(String, nullable=False)
|
||||
|
||||
announcements = relationship("AnnouncementDbModel", back_populates="attachments")
|
||||
|
||||
|
||||
class UserDbModel(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
username = Column(String, nullable=False)
|
||||
password = Column(String, nullable=False)
|
||||
|
||||
# Required by sanic-beskar
|
||||
@property
|
||||
def rolenames(self):
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
async def lookup(cls, username=None):
|
||||
try:
|
||||
session = Session()
|
||||
|
||||
user = session.query(UserDbModel).filter_by(username=username).first()
|
||||
|
||||
session.close()
|
||||
|
||||
return user
|
||||
except:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
async def identify(cls, id):
|
||||
try:
|
||||
session = Session()
|
||||
|
||||
user = session.query(UserDbModel).filter_by(id=id).first()
|
||||
|
||||
session.close()
|
||||
|
||||
return user
|
||||
except:
|
||||
return None
|
||||
|
||||
@property
|
||||
def identity(self):
|
||||
return self.id
|
||||
|
||||
|
||||
Base.metadata.create_all(engine)
|
@ -4,8 +4,13 @@ services:
|
||||
revanced-api:
|
||||
container_name: revanced-api
|
||||
image: ghcr.io/revanced/revanced-api:latest
|
||||
volumes:
|
||||
- /data/revanced-api:/usr/src/app/persistence
|
||||
environment:
|
||||
- GITHUB_TOKEN=YOUR_GITHUB_TOKEN
|
||||
- SECRET_KEY=YOUR_SECRET_KEY
|
||||
- USERNAME=YOUR_USERNAME
|
||||
- PASSWORD=YOUR_PASSWORD
|
||||
ports:
|
||||
- 127.0.0.1:7934:8000
|
||||
restart: unless-stopped
|
||||
|
7
limiter.py
Normal file
7
limiter.py
Normal file
@ -0,0 +1,7 @@
|
||||
from sanic_limiter import Limiter, get_remote_address
|
||||
|
||||
limiter = Limiter(key_func=get_remote_address)
|
||||
|
||||
|
||||
def configure_limiter(app):
|
||||
limiter.init_app(app)
|
0
persistance/.gitkeep
Normal file
0
persistance/.gitkeep
Normal file
1260
poetry.lock
generated
1260
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -28,13 +28,19 @@ pytest-emoji = "^0.2.0"
|
||||
coverage = "^7.3.2"
|
||||
pytest-cov = "^4.1.0"
|
||||
pytest = "^7.4.0"
|
||||
sqlalchemy = "^2.0.21"
|
||||
sanic-beskar = "^2.2.12"
|
||||
bson = "^0.5.10"
|
||||
fastpbkdf2 = "^0.2"
|
||||
cryptography = "^41.0.4"
|
||||
sanic-limiter = { git = "https://github.com/Omegastick/sanic-limiter" }
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
asyncio_mode = "auto"
|
||||
filterwarnings = [
|
||||
"ignore::DeprecationWarning",
|
||||
"ignore::pytest.PytestCollectionWarning"
|
||||
]
|
||||
"ignore::pytest.PytestCollectionWarning",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
|
@ -1,50 +1,75 @@
|
||||
aiodns==3.0.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
aiodns==3.1.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
aiofiles==23.2.1 ; python_version >= "3.11" and python_version < "4.0"
|
||||
aiohttp[speedups]==3.8.5 ; python_version >= "3.11" and python_version < "4.0"
|
||||
aiohttp[speedups]==3.8.6 ; python_version >= "3.11" and python_version < "4.0"
|
||||
aiosignal==1.3.1 ; python_version >= "3.11" and python_version < "4.0"
|
||||
anyio==4.0.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
argon2-cffi==23.1.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
argon2-cffi-bindings==21.2.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
async-timeout==4.0.3 ; python_version >= "3.11" and python_version < "4.0"
|
||||
asyncstdlib==3.10.8 ; python_version >= "3.11" and python_version < "4.0"
|
||||
asyncstdlib==3.10.9 ; python_version >= "3.11" and python_version < "4.0"
|
||||
attrs==23.1.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
beautifulsoup4==4.12.2 ; python_version >= "3.11" and python_version < "4.0"
|
||||
brotli==1.1.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
bson==0.5.10 ; python_version >= "3.11" and python_version < "4.0"
|
||||
certifi==2023.7.22 ; python_version >= "3.11" and python_version < "4.0"
|
||||
cffi==1.15.1 ; python_version >= "3.11" and python_version < "4.0"
|
||||
charset-normalizer==3.2.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
cffi==1.16.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
charset-normalizer==3.3.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
colorama==0.4.6 ; python_version >= "3.11" and python_version < "4.0" and sys_platform == "win32"
|
||||
coverage==7.3.1 ; python_version >= "3.11" and python_version < "4.0"
|
||||
coverage[toml]==7.3.1 ; python_version >= "3.11" and python_version < "4.0"
|
||||
coverage==7.3.2 ; python_version >= "3.11" and python_version < "4.0"
|
||||
coverage[toml]==7.3.2 ; python_version >= "3.11" and python_version < "4.0"
|
||||
cryptography==41.0.4 ; python_version >= "3.11" and python_version < "4.0"
|
||||
cytoolz==0.12.2 ; python_version >= "3.11" and python_version < "4.0"
|
||||
deprecated==1.2.14 ; python_version >= "3.11" and python_version < "4.0"
|
||||
fastpbkdf2==0.2 ; python_version >= "3.11" and python_version < "4.0"
|
||||
frozenlist==1.4.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
greenlet==3.0.0 ; python_version >= "3.11" and python_version < "4.0" and platform_machine == "aarch64" or python_version >= "3.11" and python_version < "4.0" and platform_machine == "ppc64le" or python_version >= "3.11" and python_version < "4.0" and platform_machine == "x86_64" or python_version >= "3.11" and python_version < "4.0" and platform_machine == "amd64" or python_version >= "3.11" and python_version < "4.0" and platform_machine == "AMD64" or python_version >= "3.11" and python_version < "4.0" and platform_machine == "win32" or python_version >= "3.11" and python_version < "4.0" and platform_machine == "WIN32"
|
||||
h11==0.14.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
html5tagger==1.3.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
httpcore==0.18.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
httptools==0.6.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
httpx==0.25.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
idna==3.4 ; python_version >= "3.11" and python_version < "4.0"
|
||||
importlib-resources==6.1.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
iniconfig==2.0.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
iso8601==2.1.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
jinja2==3.1.2 ; python_version >= "3.11" and python_version < "4.0"
|
||||
limits==3.6.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
lxml==4.9.3 ; python_version >= "3.11" and python_version < "4.0"
|
||||
markupsafe==2.1.3 ; python_version >= "3.11" and python_version < "4.0"
|
||||
multidict==6.0.4 ; python_version >= "3.11" and python_version < "4.0"
|
||||
mypy==1.5.1 ; python_version >= "3.11" and python_version < "4.0"
|
||||
mypy==1.6.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
mypy-extensions==1.0.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
packaging==23.1 ; python_version >= "3.11" and python_version < "4.0"
|
||||
packaging==23.2 ; python_version >= "3.11" and python_version < "4.0"
|
||||
passlib==1.7.4 ; python_version >= "3.11" and python_version < "4.0"
|
||||
pendulum==2.1.2 ; python_version >= "3.11" and python_version < "4.0"
|
||||
pluggy==1.3.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
pycares==4.3.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
py-buzz==4.1.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
pycares==4.4.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
pycparser==2.21 ; python_version >= "3.11" and python_version < "4.0"
|
||||
pydantic==1.10.12 ; python_version >= "3.11" and python_version < "4.0"
|
||||
pycryptodomex==3.19.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
pydantic==1.10.13 ; python_version >= "3.11" and python_version < "4.0"
|
||||
pyjwt==2.8.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
pyseto==1.7.5 ; python_version >= "3.11" and python_version < "4.0"
|
||||
pytest==7.4.2 ; python_version >= "3.11" and python_version < "4.0"
|
||||
pytest-asyncio==0.21.1 ; python_version >= "3.11" and python_version < "4.0"
|
||||
pytest-cov==4.1.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
pytest-emoji==0.2.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
pytest-md==0.2.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
python-dateutil==2.8.2 ; python_version >= "3.11" and python_version < "4.0"
|
||||
pytzdata==2020.1 ; python_version >= "3.11" and python_version < "4.0"
|
||||
pyyaml==6.0.1 ; python_version >= "3.11" and python_version < "4.0"
|
||||
sanic==23.6.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
sanic-beskar==2.2.12 ; python_version >= "3.11" and python_version < "4.0"
|
||||
sanic-ext==23.6.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
sanic-limiter @ git+https://github.com/Omegastick/sanic-limiter ; python_version >= "3.11" and python_version < "4.0"
|
||||
sanic-routing==23.6.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
sanic-testing==23.6.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
sanic[ext]==23.6.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
setuptools==68.2.2 ; python_version >= "3.11" and python_version < "4.0"
|
||||
six==1.16.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
sniffio==1.3.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
soupsieve==2.5 ; python_version >= "3.11" and python_version < "4.0"
|
||||
sqlalchemy==2.0.21 ; python_version >= "3.11" and python_version < "4.0"
|
||||
toolz==0.12.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
tracerite==1.1.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
types-aiofiles==23.2.0.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
@ -55,4 +80,5 @@ typing-extensions==4.8.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
ujson==5.8.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
uvloop==0.17.0 ; sys_platform != "win32" and implementation_name == "cpython" and python_version >= "3.11" and python_version < "4.0"
|
||||
websockets==11.0.3 ; python_version >= "3.11" and python_version < "4.0"
|
||||
wrapt==1.15.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
yarl==1.9.2 ; python_version >= "3.11" and python_version < "4.0"
|
||||
|
Loading…
x
Reference in New Issue
Block a user