mirror of
https://github.com/ReVanced/revanced-api.git
synced 2026-01-30 06:31:03 +00:00
Merge pull request #102 from ReVanced/dev
This commit is contained in:
6
.dockerignore
Normal file
6
.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
.idea
|
||||||
|
.devcontainer
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.github
|
||||||
|
.vscode
|
||||||
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@v4
|
||||||
|
|
||||||
|
- 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}}"
|
||||||
12
.github/workflows/dev.yml
vendored
12
.github/workflows/dev.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: Security check
|
name: Security check
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Security Checks (PyCharm Security)
|
- name: Security Checks (PyCharm Security)
|
||||||
uses: tonybaloney/pycharm-security@master
|
uses: tonybaloney/pycharm-security@master
|
||||||
with:
|
with:
|
||||||
@@ -33,24 +33,24 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout Dockerfile
|
- name: Checkout Dockerfile
|
||||||
id: checkout
|
id: checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: ${{ inputs.branch }}
|
ref: ${{ inputs.branch }}
|
||||||
|
|
||||||
- name: Setup QEMU
|
- name: Setup QEMU
|
||||||
id: qemu
|
id: qemu
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v3
|
||||||
with:
|
with:
|
||||||
image: tonistiigi/binfmt:latest
|
image: tonistiigi/binfmt:latest
|
||||||
platforms: all
|
platforms: all
|
||||||
|
|
||||||
- name: Setup Docker Buildx
|
- name: Setup Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Extract metadata (tags, labels) for Docker
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
flavor: |
|
flavor: |
|
||||||
@@ -59,7 +59,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build Docker image
|
- name: Build Docker image
|
||||||
id: build
|
id: build
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
|
|||||||
12
.github/workflows/main.yml
vendored
12
.github/workflows/main.yml
vendored
@@ -22,22 +22,22 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout Dockerfile
|
- name: Checkout Dockerfile
|
||||||
id: checkout
|
id: checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup QEMU
|
- name: Setup QEMU
|
||||||
id: qemu
|
id: qemu
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v3
|
||||||
with:
|
with:
|
||||||
image: tonistiigi/binfmt:latest
|
image: tonistiigi/binfmt:latest
|
||||||
platforms: all
|
platforms: all
|
||||||
|
|
||||||
- name: Setup Docker Buildx
|
- name: Setup Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
id: ghcr
|
id: ghcr
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
@@ -45,7 +45,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Extract metadata (tags, labels) for Docker
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
flavor: |
|
flavor: |
|
||||||
@@ -54,7 +54,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build and push main Docker image
|
- name: Build and push main Docker image
|
||||||
id: build
|
id: build
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
build-args: GH_TOKEN=${{ secrets.GH_TOKEN }}
|
build-args: GH_TOKEN=${{ secrets.GH_TOKEN }}
|
||||||
context: .
|
context: .
|
||||||
|
|||||||
4
.github/workflows/pytest.yml
vendored
4
.github/workflows/pytest.yml
vendored
@@ -21,12 +21,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: "3.11.4"
|
python-version: "3.11.6"
|
||||||
|
|
||||||
- name: Install project dependencies
|
- name: Install project dependencies
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
45
.github/workflows/quodana.yml
vendored
45
.github/workflows/quodana.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@v3
|
|
||||||
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.1
|
|
||||||
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
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -157,7 +157,8 @@ cython_debug/
|
|||||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
#.idea/
|
.idea/
|
||||||
|
|
||||||
# custom
|
# custom
|
||||||
env.sh
|
env.sh
|
||||||
|
persistance/database.db
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.4.0
|
rev: v4.5.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
@@ -12,15 +12,9 @@ repos:
|
|||||||
- id: check-toml
|
- id: check-toml
|
||||||
- id: check-merge-conflict
|
- id: check-merge-conflict
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 23.7.0
|
rev: 23.9.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
language_version: python3.11
|
language_version: python3.11
|
||||||
- repo: https://github.com/pryorda/dockerfilelint-precommit-hooks
|
|
||||||
rev: v0.1.0
|
|
||||||
hooks:
|
|
||||||
- id: dockerfilelint
|
|
||||||
stages: [commit]
|
|
||||||
|
|
||||||
ci:
|
ci:
|
||||||
autoupdate_branch: "dev"
|
autoupdate_branch: "dev"
|
||||||
|
|||||||
38
Dockerfile
38
Dockerfile
@@ -1,14 +1,36 @@
|
|||||||
FROM python:3.11-slim
|
## Build dependencies
|
||||||
|
FROM python:3.11-slim as dependencies
|
||||||
ARG GITHUB_TOKEN
|
|
||||||
ENV GITHUB_TOKEN $GITHUB_TOKEN
|
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends gcc git \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN python -m venv /opt/venv
|
||||||
|
ENV PATH="/opt/venv/bin:$PATH"
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
RUN pip install -r requirements.txt
|
||||||
|
|
||||||
|
## Image
|
||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
ENV PATH="/opt/venv/bin:$PATH"
|
||||||
|
|
||||||
|
COPY --from=dependencies /opt/venv /opt/venv
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN apt update && \
|
VOLUME persistance
|
||||||
apt-get install build-essential libffi-dev libssl-dev openssl --no-install-recommends -y \
|
|
||||||
&& pip install --no-cache-dir -r requirements.txt
|
|
||||||
|
|
||||||
CMD [ "python3", "-m" , "sanic", "app:app", "--fast", "--access-logs", "--motd", "--noisy-exceptions", "-H", "0.0.0.0"]
|
CMD docker/run-backend.sh
|
||||||
|
HEALTHCHECK CMD docker/run-healthcheck.sh
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||

|

|
||||||
[](https://codecov.io/gh/ReVanced/revanced-api)
|
[](https://codecov.io/gh/ReVanced/revanced-api)
|
||||||
[](https://github.com/revanced/revanced-api/actions/workflows/main.yml)
|
[](https://github.com/revanced/revanced-api/actions/workflows/main.yml)
|
||||||
[](https://github.com/revanced/revanced-api/actions/workflows/quodana.yml)
|
[](https://github.com/revanced/revanced-api/actions/workflows/qodana.yml)
|
||||||
[](https://github.com/revanced/revanced-api/actions/workflows/pytest.yml)
|
[](https://github.com/revanced/revanced-api/actions/workflows/pytest.yml)
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -20,7 +20,11 @@ To run this API, you need Python 3.11.x. You can install the dependencies with p
|
|||||||
poetry install
|
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:
|
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.info import info
|
||||||
from api.compat import github as compat
|
from api.compat import github as compat
|
||||||
from api.donations import donations
|
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
|
||||||
@@ -11,6 +11,14 @@ class ContactFields(BaseModel):
|
|||||||
email: str
|
email: str
|
||||||
|
|
||||||
|
|
||||||
|
class BrandingFields(BaseModel):
|
||||||
|
"""
|
||||||
|
Implements the fields for the API owner branding info.
|
||||||
|
"""
|
||||||
|
|
||||||
|
logo: str
|
||||||
|
|
||||||
|
|
||||||
class InfoFields(BaseModel):
|
class InfoFields(BaseModel):
|
||||||
"""
|
"""
|
||||||
Implements the fields for the API owner info.
|
Implements the fields for the API owner info.
|
||||||
@@ -18,6 +26,7 @@ class InfoFields(BaseModel):
|
|||||||
|
|
||||||
name: str
|
name: str
|
||||||
about: str
|
about: str
|
||||||
|
branding: BrandingFields
|
||||||
contact: ContactFields
|
contact: ContactFields
|
||||||
socials: list[SocialFields]
|
socials: list[SocialFields]
|
||||||
donations: DonationFields
|
donations: DonationFields
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ class SocialFields(BaseModel):
|
|||||||
|
|
||||||
name: str
|
name: str
|
||||||
url: str
|
url: str
|
||||||
|
preferred: bool
|
||||||
|
|
||||||
|
|
||||||
class SocialsResponseModel(BaseModel):
|
class SocialsResponseModel(BaseModel):
|
||||||
|
|||||||
10
app.py
10
app.py
@@ -6,6 +6,9 @@ from sanic_ext import Config
|
|||||||
from api import api
|
from api import api
|
||||||
from config import *
|
from config import *
|
||||||
|
|
||||||
|
from limiter import configure_limiter
|
||||||
|
from auth import configure_auth
|
||||||
|
|
||||||
REDIRECTS = {
|
REDIRECTS = {
|
||||||
"/": "/docs/swagger",
|
"/": "/docs/swagger",
|
||||||
}
|
}
|
||||||
@@ -25,8 +28,13 @@ app.config.CORS_SUPPORTS_CREDENTIALS = True
|
|||||||
app.config.CORS_SEND_WILDCARD = True
|
app.config.CORS_SEND_WILDCARD = True
|
||||||
app.config.CORS_ORIGINS = "*"
|
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
|
# 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()
|
||||||
23
config.py
23
config.py
@@ -55,14 +55,18 @@ compat_repositories: list = [
|
|||||||
|
|
||||||
# Social Links
|
# Social Links
|
||||||
|
|
||||||
social_links: list[dict[str, str]] = [
|
social_links: list[dict[str, str | bool]] = [
|
||||||
{"name": "Website", "url": "https://revanced.app"},
|
{"name": "Website", "url": "https://revanced.app", "preferred": True},
|
||||||
{"name": "GitHub", "url": "https://github.com/revanced"},
|
{"name": "GitHub", "url": "https://github.com/revanced", "preferred": False},
|
||||||
{"name": "Twitter", "url": "https://twitter.com/revancedapp"},
|
{"name": "Twitter", "url": "https://twitter.com/revancedapp", "preferred": False},
|
||||||
{"name": "Discord", "url": "https://revanced.app/discord"},
|
{"name": "Discord", "url": "https://revanced.app/discord", "preferred": True},
|
||||||
{"name": "Reddit", "url": "https://www.reddit.com/r/revancedapp"},
|
{
|
||||||
{"name": "Telegram", "url": "https://t.me/app_revanced"},
|
"name": "Reddit",
|
||||||
{"name": "YouTube", "url": "https://www.youtube.com/@ReVanced"},
|
"url": "https://www.reddit.com/r/revancedapp",
|
||||||
|
"preferred": False,
|
||||||
|
},
|
||||||
|
{"name": "Telegram", "url": "https://t.me/app_revanced", "preferred": False},
|
||||||
|
{"name": "YouTube", "url": "https://www.youtube.com/@ReVanced", "preferred": False},
|
||||||
]
|
]
|
||||||
|
|
||||||
# Donation info
|
# Donation info
|
||||||
@@ -116,6 +120,9 @@ links: list[dict[str, str | bool]] = [
|
|||||||
default_info: dict[str, str | list[str | bool] | bool] = {
|
default_info: dict[str, str | list[str | bool] | bool] = {
|
||||||
"name": "ReVanced",
|
"name": "ReVanced",
|
||||||
"about": "ReVanced was born out of Vanced's discontinuation and it is our goal to continue the legacy of what Vanced left behind. Thanks to ReVanced Patcher, it's possible to create long-lasting patches for nearly any Android app. ReVanced's patching system is designed to allow patches to work on new versions of the apps automatically with bare minimum maintenance.",
|
"about": "ReVanced was born out of Vanced's discontinuation and it is our goal to continue the legacy of what Vanced left behind. Thanks to ReVanced Patcher, it's possible to create long-lasting patches for nearly any Android app. ReVanced's patching system is designed to allow patches to work on new versions of the apps automatically with bare minimum maintenance.",
|
||||||
|
"branding": {
|
||||||
|
"logo": "https://raw.githubusercontent.com/ReVanced/revanced-branding/main/assets/revanced-logo/revanced-logo.svg"
|
||||||
|
},
|
||||||
"contact": {"email": "contact@revanced.app"},
|
"contact": {"email": "contact@revanced.app"},
|
||||||
"socials": social_links,
|
"socials": social_links,
|
||||||
"donations": {"wallets": wallets, "links": links},
|
"donations": {"wallets": wallets, "links": links},
|
||||||
|
|||||||
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:
|
revanced-api:
|
||||||
container_name: revanced-api
|
container_name: revanced-api
|
||||||
image: ghcr.io/revanced/revanced-api:latest
|
image: ghcr.io/revanced/revanced-api:latest
|
||||||
|
volumes:
|
||||||
|
- /data/revanced-api:/usr/src/app/persistence
|
||||||
environment:
|
environment:
|
||||||
- GITHUB_TOKEN=YOUR_GITHUB_TOKEN
|
- GITHUB_TOKEN=YOUR_GITHUB_TOKEN
|
||||||
|
- SECRET_KEY=YOUR_SECRET_KEY
|
||||||
|
- USERNAME=YOUR_USERNAME
|
||||||
|
- PASSWORD=YOUR_PASSWORD
|
||||||
ports:
|
ports:
|
||||||
- 127.0.0.1:7934:8000
|
- 127.0.0.1:7934:8000
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
1
docker/run-backend.sh
Executable file
1
docker/run-backend.sh
Executable file
@@ -0,0 +1 @@
|
|||||||
|
python3 -m sanic /usr/src/app --fast --access-logs --motd --noisy-exceptions -H 0.0.0.0
|
||||||
1
docker/run-healthcheck.sh
Executable file
1
docker/run-healthcheck.sh
Executable file
@@ -0,0 +1 @@
|
|||||||
|
curl --fail http://0.0.0.0:8000/docs
|
||||||
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
1910
poetry.lock
generated
1910
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -8,16 +8,16 @@ readme = "README.md"
|
|||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.11"
|
python = "^3.11"
|
||||||
aiohttp = {version = "^3.8.5", extras = ["speedups"]}
|
aiohttp = {version = "^3.8.6", extras = ["speedups"]}
|
||||||
sanic = {version = "^23.6.0", extras = ["ext"]}
|
sanic = {version = "^23.6.0", extras = ["ext"]}
|
||||||
ujson = "^5.8.0"
|
ujson = "^5.8.0"
|
||||||
asyncstdlib = "^3.10.8"
|
asyncstdlib = "^3.10.9"
|
||||||
pydantic = "^1.10.12"
|
pydantic = "^1.10.13"
|
||||||
cytoolz = "^0.12.2"
|
cytoolz = "^0.12.2"
|
||||||
beautifulsoup4 = "^4.12.2"
|
beautifulsoup4 = "^4.12.2"
|
||||||
setuptools = "^68.1.2"
|
setuptools = "^68.1.2"
|
||||||
lxml = "^4.9.3"
|
lxml = "^4.9.3"
|
||||||
mypy = "^1.5.1"
|
mypy = "^1.6.1"
|
||||||
types-ujson = "^5.8.0.1"
|
types-ujson = "^5.8.0.1"
|
||||||
types-aiofiles = "^23.2.0.0"
|
types-aiofiles = "^23.2.0.0"
|
||||||
sanic-testing = "^23.6.0"
|
sanic-testing = "^23.6.0"
|
||||||
@@ -25,16 +25,22 @@ pytest-asyncio = "^0.21.1"
|
|||||||
types-beautifulsoup4 = "^4.12.0.6"
|
types-beautifulsoup4 = "^4.12.0.6"
|
||||||
pytest-md = "^0.2.0"
|
pytest-md = "^0.2.0"
|
||||||
pytest-emoji = "^0.2.0"
|
pytest-emoji = "^0.2.0"
|
||||||
coverage = "^7.3.0"
|
coverage = "^7.3.2"
|
||||||
pytest-cov = "^4.1.0"
|
pytest-cov = "^4.1.0"
|
||||||
pytest = "^7.4.0"
|
pytest = "^7.4.0"
|
||||||
|
sqlalchemy = "^2.0.22"
|
||||||
|
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]
|
[tool.pytest.ini_options]
|
||||||
asyncio_mode = "auto"
|
asyncio_mode = "auto"
|
||||||
filterwarnings = [
|
filterwarnings = [
|
||||||
"ignore::DeprecationWarning",
|
"ignore::DeprecationWarning",
|
||||||
"ignore::pytest.PytestCollectionWarning"
|
"ignore::pytest.PytestCollectionWarning",
|
||||||
]
|
]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core"]
|
requires = ["poetry-core"]
|
||||||
|
|||||||
@@ -1,58 +1,84 @@
|
|||||||
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"
|
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"
|
aiosignal==1.3.1 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
anyio==3.7.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"
|
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"
|
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"
|
beautifulsoup4==4.12.2 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
brotli==1.0.9 ; 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"
|
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"
|
cffi==1.16.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
charset-normalizer==3.2.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"
|
colorama==0.4.6 ; python_version >= "3.11" and python_version < "4.0" and sys_platform == "win32"
|
||||||
coverage==7.3.0 ; 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.0 ; 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"
|
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"
|
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"
|
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"
|
html5tagger==1.3.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
httpcore==0.17.3 ; 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"
|
httptools==0.6.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
httpx==0.24.1 ; 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"
|
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"
|
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"
|
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"
|
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"
|
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"
|
||||||
pluggy==1.2.0 ; python_version >= "3.11" and python_version < "4.0"
|
passlib==1.7.4 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
pycares==4.3.0 ; 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"
|
||||||
|
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"
|
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"
|
||||||
pytest==7.4.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-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-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-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"
|
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"
|
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-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-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-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"
|
sanic[ext]==23.6.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
setuptools==68.1.2 ; 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"
|
sniffio==1.3.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
soupsieve==2.4.1 ; 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"
|
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"
|
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"
|
types-aiofiles==23.2.0.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
types-beautifulsoup4==4.12.0.6 ; python_version >= "3.11" and python_version < "4.0"
|
types-beautifulsoup4==4.12.0.6 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
types-html5lib==1.1.11.15 ; python_version >= "3.11" and python_version < "4.0"
|
types-html5lib==1.1.11.15 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
types-ujson==5.8.0.1 ; python_version >= "3.11" and python_version < "4.0"
|
types-ujson==5.8.0.1 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
typing-extensions==4.7.1 ; python_version >= "3.11" and python_version < "4.0"
|
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"
|
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"
|
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"
|
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"
|
yarl==1.9.2 ; python_version >= "3.11" and python_version < "4.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user