Merge pull request #21 from msys2/fastapi

Port to FastAPI
This commit is contained in:
Christoph Reiter
2020-05-26 20:59:38 +02:00
committed by GitHub
18 changed files with 561 additions and 320 deletions

View File

@@ -12,5 +12,5 @@ COPY . /app
WORKDIR /app
RUN poetry install --no-dev
ENTRYPOINT ["poetry","run", "gunicorn", "-w1", "-b0.0.0.0:80", "app:app"]
ENTRYPOINT ["poetry","run", "uvicorn", "--host", "0.0.0.0", "--port", "80", "app:app"]
EXPOSE 80

View File

@@ -3,16 +3,18 @@
import os
from flask import Flask
from jinja2 import StrictUndefined
from fastapi import FastAPI
from .web import packages
from .web import webapp
from .fetch import start_update_thread
app = Flask(__name__)
app.register_blueprint(packages)
app.jinja_env.undefined = StrictUndefined
app = FastAPI(openapi_url=None)
app.mount("/", webapp)
if not os.environ.get("NO_UPDATE_THREAD"):
start_update_thread()
# https://github.com/tiangolo/fastapi/issues/1480
@app.on_event("startup")
async def startup_event() -> None:
if not os.environ.get("NO_UPDATE_THREAD"):
start_update_thread()

View File

@@ -33,7 +33,7 @@
<dt class="col-sm-3 text-sm-right">Group(s):</dt>
<dd class="col-sm-9">
{% for g in s.groups %}
<a href="{{ url_for('.group', name=g) }}">{{ g }}</a>{{ ", " if not loop.last else "" }}
<a href="{{ url_for('group', group_name=g) }}">{{ g }}</a>{{ ", " if not loop.last else "" }}
{% else %}
-
{% endfor %}

View File

@@ -19,7 +19,7 @@
<tbody>
{% for s in sources %}
<tr>
<td><a href="{{ url_for('.base', name=s.name) }}">{{ s.name }}</a></td>
<td><a href="{{ url_for('base', base_name=s.name) }}">{{ s.name }}</a></td>
<td>{{ s.version }}</td>
<td>{{ s.desc }}</td>
</tr>

View File

@@ -4,7 +4,7 @@
<div class="card mb-3">
<div class="card-header">
<h4 class="card-title">Group: <a href="{{ url_for('.group', name=name) }}">{{ name }}</a></h4>
<h4 class="card-title">Group: <a href="{{ url_for('group', group_name=name) }}">{{ name }}</a></h4>
<h6 class="card-subtitle mb-2 text-muted">Packages that are part of the group</h6>
</div>
<div class="card-body">

View File

@@ -18,7 +18,7 @@
<tbody>
{% for g, count in groups|dictsort %}
<tr>
<td><a href="{{ url_for('.group', name=g) }}">{{ g }}</a></td>
<td><a href="{{ url_for('group', group_name=g) }}">{{ g }}</a></td>
<td>{{ count }}</td>
</tr>
{% endfor %}

View File

@@ -4,16 +4,16 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<script src="{{ url_for('static', filename='index.js') }}" defer></script>
<link rel="shortcut icon" href="{{ url_for('static', filename='images/favicon.ico') }}"/>
<link rel="stylesheet" href="{{ url_for('static', filename='index.css') }}">
<script src="{{ url_for('static', path='/index.js') }}" defer></script>
<link rel="shortcut icon" href="{{ url_for('static', path='images/favicon.ico') }}"/>
<link rel="stylesheet" href="{{ url_for('static', path='index.css') }}">
<title>{% block title %}{% endblock %} - MSYS2 Packages</title>
</head>
<body>
<nav class="navbar navbar-expand-md navbar-light fixed-top bg-light">
<a class="navbar-brand" href="{{ url_for('.index') }}" style="line-height: 2em">
<img src="{{ url_for('static', filename='images/logo.svg') }}" style="width:2em;height:2em;margin-right:0.5em;" class="d-inline-block align-top" alt="MSYS2"> MSYS2 Packages
<a class="navbar-brand" href="{{ url_for('index') }}" style="line-height: 2em">
<img src="{{ url_for('static', path='images/logo.svg') }}" style="width:2em;height:2em;margin-right:0.5em;" class="d-inline-block align-top" alt="MSYS2"> MSYS2 Packages
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>

View File

@@ -1,30 +1,30 @@
<li class="nav-item">
<a class="nav-link {{ 'active' if is_endpoint('.updates') else '' }}" href="{{ url_for('.updates') }}">Repo Updates</a>
<a class="nav-link {{ 'active' if is_endpoint('updates') else '' }}" href="{{ url_for('updates') }}">Repo Updates</a>
</li>
<li class="nav-item">
<a class="nav-link {{ 'active' if is_endpoint('.repos') else '' }}" href="{{ url_for('.repos') }}">Repos</a>
<a class="nav-link {{ 'active' if is_endpoint('repos') else '' }}" href="{{ url_for('repos') }}">Repos</a>
</li>
<li class="nav-item">
<a class="nav-link {{ 'active' if is_endpoint('.base') else '' }}" href="{{ url_for('.base') }}">Base Packages</a>
<a class="nav-link {{ 'active' if is_endpoint('base') else '' }}" href="{{ url_for('base') }}">Base Packages</a>
</li>
<li class="nav-item">
<a class="nav-link {{ 'active' if is_endpoint('.group') else '' }}" href="{{ url_for('.group') }}">Groups</a>
<a class="nav-link {{ 'active' if is_endpoint('group') else '' }}" href="{{ url_for('group') }}">Groups</a>
</li>
<li class="nav-item">
<a class="nav-link {{ 'active' if is_endpoint('.search') else '' }}" href="{{ url_for('.search') }}">Search</a>
<a class="nav-link {{ 'active' if is_endpoint('search') else '' }}" href="{{ url_for('search') }}">Search</a>
</li>
<li class="nav-item">
<a class="nav-link {{ 'active' if is_endpoint('.outofdate') else '' }}" href="{{ url_for('.outofdate') }}">Outdated</a>
<a class="nav-link {{ 'active' if is_endpoint('outofdate') else '' }}" href="{{ url_for('outofdate') }}">Outdated</a>
</li>
<li class="nav-item">
<a class="nav-link {{ 'active' if is_endpoint('.queue') else '' }}" href="{{ url_for('.queue') }}">Update Queue</a>
<a class="nav-link {{ 'active' if is_endpoint('queue') else '' }}" href="{{ url_for('queue') }}">Update Queue</a>
</li>
<li class="nav-item">
<a class="nav-link {{ 'active' if is_endpoint('.new') else '' }}" href="{{ url_for('.new') }}">New Queue</a>
<a class="nav-link {{ 'active' if is_endpoint('new') else '' }}" href="{{ url_for('new') }}">New Queue</a>
</li>
<li class="nav-item">
<a class="nav-link {{ 'active' if is_endpoint('.removals') else '' }}" href="{{ url_for('.removals') }}">Removal Queue</a>
<a class="nav-link {{ 'active' if is_endpoint('removals') else '' }}" href="{{ url_for('removals') }}">Removal Queue</a>
</li>
<li class="nav-item">
<a class="nav-link {{ 'active' if is_endpoint('.python2') else '' }}" href="{{ url_for('.python2') }}">Python 2 Removal</a>
<a class="nav-link {{ 'active' if is_endpoint('python2') else '' }}" href="{{ url_for('python2') }}">Python 2 Removal</a>
</li>

View File

@@ -31,7 +31,7 @@
<tbody>
{% for (s, myver, gitver, ver, url, date) in to_update %}
<tr>
<td><a href="{{ url_for('.base', name=s.name) }}">{{ s.name }}</a></td>
<td><a href="{{ url_for('base', base_name=s.name) }}">{{ s.name }}</a></td>
<td>{{ myver }}{% if gitver %} <span class="text-muted small px-2">({{ gitver }} in git)</span>{% endif %}</td>
<td></td>
<td><a href="{{ url }}">{{ ver }}</a></td>
@@ -43,7 +43,7 @@
<h6>{{ missing|length }} packages not found in other distros:</h6>
{% for (s, short_name) in missing %}
<a href="{{ url_for('.base', name=s.name) }}">{{ s.name }}</a>
<a href="{{ url_for('base', base_name=s.name) }}">{{ s.name }}</a>
<span class="text-muted">({{ short_name }})</span>{{ ", " if not loop.last else '' }}
{% endfor %}
@@ -51,7 +51,7 @@
<br>
<h6>{{ skipped|length }} packages skipped (Windows only etc.):</h6>
{% for s in skipped %}
<a href="{{ url_for('.base', name=s.name) }}">{{ s.name }}</a>{{ ", " if not loop.last else '' }}
<a href="{{ url_for('base', base_name=s.name) }}">{{ s.name }}</a>{{ ", " if not loop.last else '' }}
{% endfor %}
</div>
</div>

View File

@@ -20,7 +20,7 @@
<div class="card-body overflow-auto">
<dl class="row">
<dt class="col-sm-3 text-sm-right">Base Package:</dt>
<dd class="col-sm-9"><a href="{{ url_for('.base', name=p.base) }}">{{ p.base }}</a></dd>
<dd class="col-sm-9"><a href="{{ url_for('base', base_name=p.base) }}">{{ p.base }}</a></dd>
<dt class="col-sm-3 text-sm-right">Repo:</dt>
<dd class="col-sm-9">{{ package_name(p, p.repo) }}</dd>

View File

@@ -19,7 +19,7 @@
<tbody>
{% for s, info in results %}
<tr>
<td><a href="{{ url_for('.base', name=s) }}">{{ s }}</a></td>
<td><a href="{{ url_for('base', base_name=s) }}">{{ s }}</a></td>
<td>{{ info[0] }}</td>
<td>{{ '✓' if info[1] else ''}}</td>
</tr>

View File

@@ -25,7 +25,7 @@
{% for (srcpkg, s, p) in updates %}
<tr>
<td><a href="{{ srcpkg.history_url }}">{{ srcpkg.date }}</a></td>
<td><a href="{{ url_for('.base', name=s.name) }}">{{ s.name }}</a></td>
<td><a href="{{ url_for('base', base_name=s.name) }}">{{ s.name }}</a></td>
<td>{{ p.version }}</td>
<td></td>
<td>{{ srcpkg.build_version }}</td>

View File

@@ -6,7 +6,7 @@
<div class="card">
<div class="card-body overflow-auto">
<form class="form-inline" action="{{ url_for('.search') }}" method="get">
<form class="form-inline" action="{{ url_for('search') }}" method="get">
Search in
<select style="margin: 0 12px" name="t" class="custom-select custom-select-sm">
@@ -45,7 +45,7 @@
<tbody>
{% for s in results %}
<tr>
<td><a href="{{ url_for('.base', name=s.name) }}">{{ s.name }}</a></td>
<td><a href="{{ url_for('base', base_name=s.name) }}">{{ s.name }}</a></td>
<td>{{ s.version }}</td>
<td>{{ s.desc }}</td>
</tr>

View File

@@ -8,23 +8,90 @@ import re
import datetime
import hmac
import hashlib
from functools import wraps
from typing import Callable, Any, List, Union, Dict, Optional, Tuple
import httpx
from flask import render_template, request, url_for, redirect, \
make_response, Blueprint, abort, jsonify, Request
import jinja2
from fastapi import APIRouter, Request, HTTPException, Depends, Response, FastAPI
from fastapi.templating import Jinja2Templates
from fastapi.responses import RedirectResponse, HTMLResponse, JSONResponse
from fastapi_etag import Etag
from fastapi.staticfiles import StaticFiles
from fastapi_etag import add_exception_handler as add_etag_exception_handler
from .appstate import state, get_repositories, Package, is_skipped, Source
from .utils import package_name_is_vcs, extract_upstream_version, version_is_newer_than
from .appconfig import REQUEST_TIMEOUT
packages = Blueprint('packages', __name__, template_folder='templates')
router = APIRouter(default_response_class=HTMLResponse)
DIR = os.path.dirname(os.path.realpath(__file__))
templates = Jinja2Templates(directory=os.path.join(DIR, "templates"))
@packages.app_template_filter('timestamp')
def _jinja2_filter_timestamp(d: int) -> str:
async def get_etag(request: Request) -> str:
return state.etag
def template_filter(name: str) -> Callable:
def wrap(f: Callable) -> Callable:
templates.env.filters[name] = f
return f
return wrap
def context_function(name: str) -> Callable:
def wrap(f: Callable) -> Callable:
@jinja2.contextfunction
def ctxfunc(context: Dict, *args: Any, **kwargs: Any) -> Any:
return f(context["request"], *args, **kwargs)
templates.env.globals[name] = ctxfunc
return f
return wrap
@context_function("is_endpoint")
def is_endpoint(request: Request, endpoint: str) -> bool:
path: str = request.scope["path"]
return path == "/" + endpoint or path.startswith("/" + endpoint + "/")
@context_function("update_timestamp")
def update_timestamp(request: Request) -> float:
return state.last_update
@context_function("package_url")
def package_url(request: Request, package: Package, name: str = None) -> str:
res: str = ""
if name is None:
res = request.url_for("package", package_name=name or package.name)
res += "?repo=" + package.repo
if package.repo_variant:
res += "&variant=" + package.repo_variant
else:
res = request.url_for("package", package_name=re.split("[<>=]+", name)[0])
if package.repo_variant:
res += "?repo=" + package.repo
res += "&variant=" + package.repo_variant
return res
@context_function("package_name")
def package_name(request: Request, package: Package, name: str = None) -> str:
name = name or package.name
name = re.split("[<>=]+", name, 1)[0]
return (name or package.name)
@context_function("package_restriction")
def package_restriction(request: Request, package: Package, name: str = None) -> str:
name = name or package.name
return name[len(re.split("[<>=]+", name)[0]):].strip()
@template_filter('timestamp')
def filter_timestamp(d: int) -> str:
try:
return datetime.datetime.fromtimestamp(
int(d)).strftime('%Y-%m-%d %H:%M:%S')
@@ -32,8 +99,8 @@ def _jinja2_filter_timestamp(d: int) -> str:
return "-"
@packages.app_template_filter('filesize')
def _jinja2_filter_filesize(d: int) -> str:
@template_filter('filesize')
def filter_filesize(d: int) -> str:
d = int(d)
if d > 1024 ** 3:
return "%.2f GB" % (d / (1024 ** 3))
@@ -41,154 +108,96 @@ def _jinja2_filter_filesize(d: int) -> str:
return "%.2f MB" % (d / (1024 ** 2))
@packages.context_processor
def funcs() -> Dict[str, Callable]:
def is_endpoint(value: str) -> bool:
if value.startswith(".") and request.blueprint is not None:
value = request.blueprint + value
return value == request.endpoint
def package_url(package: Package, name: str = None) -> str:
res: str = ""
if name is None:
res = url_for(".package", name=name or package.name)
res += "?repo=" + package.repo
if package.repo_variant:
res += "&variant=" + package.repo_variant
else:
res = url_for(".package", name=re.split("[<>=]+", name)[0])
if package.repo_variant:
res += "?repo=" + package.repo
res += "&variant=" + package.repo_variant
return res
def package_name(package: Package, name: str = None) -> str:
name = name or package.name
name = re.split("[<>=]+", name, 1)[0]
return (name or package.name)
def package_restriction(package: Package, name: str = None) -> str:
name = name or package.name
return name[len(re.split("[<>=]+", name)[0]):].strip()
def update_timestamp() -> float:
global state
return state.last_update
return dict(package_url=package_url, package_name=package_name,
package_restriction=package_restriction,
update_timestamp=update_timestamp, is_endpoint=is_endpoint)
@router.get('/repos', dependencies=[Depends(Etag(get_etag))])
async def repos(request: Request, response: Response) -> Response:
return templates.TemplateResponse("repos.html", {"request": request, "repos": get_repositories()}, headers=dict(response.headers))
def cache_route(f: Callable) -> Callable:
@wraps(f)
def wrapper(*args: Any, **kwargs: Any) -> Any:
global state
response = make_response()
response.set_etag(state.etag)
response.make_conditional(request)
if response.status_code == 304:
return response
result = f(*args, **kwargs)
if isinstance(result, str):
response.set_data(result)
return response
else:
return result
return wrapper
@router.get('/', dependencies=[Depends(Etag(get_etag))])
async def index(request: Request, response: Response) -> Response:
return RedirectResponse(request.url_for('updates'), headers=dict(response.headers))
RouteResponse = Any
@packages.route('/repos')
@cache_route
def repos() -> RouteResponse:
return render_template('repos.html', repos=get_repositories())
@packages.route('/')
def index() -> RouteResponse:
return redirect(url_for('.updates'))
@packages.route('/base')
@packages.route('/base/<name>')
@cache_route
def base(name: str = None) -> RouteResponse:
@router.get('/base', dependencies=[Depends(Etag(get_etag))])
@router.get('/base/{base_name}', dependencies=[Depends(Etag(get_etag))])
async def base(request: Request, response: Response, base_name: str = None) -> Response:
global state
if name is not None:
res = [s for s in state.sources if s.name == name]
return render_template('base.html', sources=res)
if base_name is not None:
res = [s for s in state.sources if s.name == base_name]
return templates.TemplateResponse("base.html", {
"request": request,
"sources": res,
}, headers=dict(response.headers))
else:
return render_template('baseindex.html', sources=state.sources)
return templates.TemplateResponse("baseindex.html", {
"request": request,
"sources": state.sources,
}, headers=dict(response.headers))
@packages.route('/group/')
@packages.route('/group/<name>')
@cache_route
def group(name: Optional[str] = None) -> RouteResponse:
@router.get('/group/', dependencies=[Depends(Etag(get_etag))])
@router.get('/group/{group_name}', dependencies=[Depends(Etag(get_etag))])
async def group(request: Request, response: Response, group_name: Optional[str] = None) -> Response:
global state
if name is not None:
if group_name is not None:
res = []
for s in state.sources:
for k, p in sorted(s.packages.items()):
if name in p.groups:
if group_name in p.groups:
res.append(p)
return render_template('group.html', name=name, packages=res)
return templates.TemplateResponse("group.html", {
"request": request,
"name": group_name,
"packages": res,
}, headers=dict(response.headers))
else:
groups: Dict[str, int] = {}
for s in state.sources:
for k, p in sorted(s.packages.items()):
for name in p.groups:
groups[name] = groups.get(name, 0) + 1
return render_template('groups.html', groups=groups)
return templates.TemplateResponse('groups.html', {
"request": request,
"groups": groups,
}, headers=dict(response.headers))
@packages.route('/package/<name>')
@cache_route
def package(name: str) -> RouteResponse:
@router.get('/package/{package_name}', dependencies=[Depends(Etag(get_etag))])
async def package(request: Request, response: Response, package_name: str, repo: Optional[str] = None, variant: Optional[str] = None) -> Response:
global state
repo = request.args.get('repo')
variant = request.args.get('variant')
packages = []
for s in state.sources:
for k, p in sorted(s.packages.items()):
if p.name == name or name in p.provides:
if p.name == package_name or package_name in p.provides:
if not repo or p.repo == repo:
if not variant or p.repo_variant == variant:
packages.append((s, p))
return render_template('package.html', packages=packages)
return templates.TemplateResponse("package.html", {
"request": request,
"packages": packages,
}, headers=dict(response.headers))
@packages.route('/updates')
@cache_route
def updates() -> RouteResponse:
global state
@router.get('/updates', dependencies=[Depends(Etag(get_etag))])
async def updates(request: Request, response: Response) -> Response:
packages: List[Package] = []
for s in state.sources:
packages.extend(s.packages.values())
packages.sort(key=lambda p: p.builddate, reverse=True)
return render_template('updates.html', packages=packages[:150])
return templates.TemplateResponse("updates.html", {
"request": request,
"packages": packages[:150],
}, headers=dict(response.headers))
@packages.route('/outofdate')
@cache_route
def outofdate() -> RouteResponse:
global state
@router.get('/outofdate', dependencies=[Depends(Etag(get_etag))])
async def outofdate(request: Request, response: Response) -> Response:
missing = []
skipped = []
to_update = []
@@ -224,17 +233,17 @@ def outofdate() -> RouteResponse:
missing.sort(key=lambda i: i[0].date, reverse=True)
skipped.sort(key=lambda i: i.name)
return render_template(
'outofdate.html',
all_sources=all_sources, to_update=to_update, missing=missing,
skipped=skipped)
return templates.TemplateResponse("outofdate.html", {
"request": request,
"all_sources": all_sources,
"to_update": to_update,
"missing": missing,
"skipped": skipped,
}, headers=dict(response.headers))
@packages.route('/queue')
@cache_route
def queue() -> RouteResponse:
global state
@router.get('/queue', dependencies=[Depends(Etag(get_etag))])
async def queue(request: Request, response: Response) -> Response:
# Create entries for all packages where the version doesn't match
updates = []
for s in state.sources:
@@ -251,14 +260,14 @@ def queue() -> RouteResponse:
key=lambda i: (i[0].date, i[0].pkgbase, i[0].pkgname),
reverse=True)
return render_template('queue.html', updates=updates)
return templates.TemplateResponse("queue.html", {
"request": request,
"updates": updates,
}, headers=dict(response.headers))
@packages.route('/new')
@cache_route
def new() -> RouteResponse:
global state
@router.get('/new', dependencies=[Depends(Etag(get_etag))])
async def new(request: Request, response: Response) -> Response:
# Create dummy entries for all GIT only packages
available = {}
for srcinfo in state.sourceinfos.values():
@@ -273,14 +282,14 @@ def new() -> RouteResponse:
key=lambda i: (i.date, i.pkgbase, i.pkgname),
reverse=True)
return render_template('new.html', new=new)
return templates.TemplateResponse("new.html", {
"request": request,
"new": new,
}, headers=dict(response.headers))
@packages.route('/removals')
@cache_route
def removals() -> RouteResponse:
global state
@router.get('/removals', dependencies=[Depends(Etag(get_etag))])
async def removals(request: Request, response: Response) -> Response:
# get all packages in the pacman repo which are no in GIT
missing = []
for s in state.sources:
@@ -289,12 +298,14 @@ def removals() -> RouteResponse:
missing.append((s, p))
missing.sort(key=lambda i: (i[1].builddate, i[1].name), reverse=True)
return render_template('removals.html', missing=missing)
return templates.TemplateResponse("removals.html", {
"request": request,
"missing": missing,
}, headers=dict(response.headers))
@packages.route('/python2')
@cache_route
def python2() -> RouteResponse:
@router.get('/python2', dependencies=[Depends(Etag(get_etag))])
async def python2(request: Request, response: Response) -> Response:
def is_split_package(p: Package) -> bool:
py2 = False
@@ -350,17 +361,16 @@ def python2() -> RouteResponse:
results = sorted(grouped.items(), key=lambda i: (i[1][0], i[0]))
return render_template(
'python2.html', results=results)
return templates.TemplateResponse("python2.html", {
"request": request,
"results": results,
}, headers=dict(response.headers))
@packages.route('/search')
@cache_route
def search() -> str:
global state
query = request.args.get('q', '')
qtype = request.args.get('t', '')
@router.get('/search', dependencies=[Depends(Etag(get_etag))])
async def search(request: Request, response: Response, q: str = "", t: str = "") -> Response:
query = q
qtype = t
if qtype not in ["pkg", "binpkg"]:
qtype = "pkg"
@@ -382,60 +392,71 @@ def search() -> str:
res_pkg.append(sub)
res_pkg.sort(key=lambda p: p.name)
return render_template(
'search.html', results=res_pkg, query=query, qtype=qtype)
return templates.TemplateResponse("search.html", {
"request": request,
"results": res_pkg,
"query": query,
"qtype": qtype,
}, headers=dict(response.headers))
def trigger_appveyor_build(account: str, project: str, token: str) -> str:
async def trigger_appveyor_build(account: str, project: str, token: str) -> str:
"""Returns an URL for the build or raises RequestException"""
r = httpx.post(
"https://ci.appveyor.com/api/builds",
json={
"accountName": account,
"projectSlug": project,
"branch": "master",
},
headers={
"Authorization": "Bearer " + token,
},
timeout=REQUEST_TIMEOUT)
r.raise_for_status()
async with httpx.AsyncClient() as client:
r = await client.post(
"https://ci.appveyor.com/api/builds",
json={
"accountName": account,
"projectSlug": project,
"branch": "master",
},
headers={
"Authorization": "Bearer " + token,
},
timeout=REQUEST_TIMEOUT)
r.raise_for_status()
try:
build_id = r.json()['buildId']
except (ValueError, KeyError):
build_id = 0
try:
build_id = r.json()['buildId']
except (ValueError, KeyError):
build_id = 0
return "https://ci.appveyor.com/project/%s/%s/builds/%d" % (
account, project, build_id)
def check_github_signature(request: Request, secret: str) -> bool:
async def check_github_signature(request: Request, secret: str) -> bool:
signature = request.headers.get('X-Hub-Signature', '')
mac = hmac.new(secret.encode("utf-8"), request.get_data(), hashlib.sha1)
mac = hmac.new(secret.encode("utf-8"), await request.body(), hashlib.sha1)
return hmac.compare_digest("sha1=" + mac.hexdigest(), signature)
@packages.route("/webhook", methods=['POST'])
def github_payload() -> RouteResponse:
@router.post("/webhook", response_class=JSONResponse)
async def github_payload(request: Request) -> Response:
secret = os.environ.get("GITHUB_WEBHOOK_SECRET")
if not secret:
abort(500, 'webhook secret config incomplete')
raise HTTPException(500, 'webhook secret config incomplete')
if not check_github_signature(request, secret):
abort(400, 'Invalid signature')
if not await check_github_signature(request, secret):
raise HTTPException(400, 'Invalid signature')
event = request.headers.get('X-GitHub-Event', '')
if event == 'ping':
return jsonify({'msg': 'pong'})
return JSONResponse({'msg': 'pong'})
if event == 'push':
account = os.environ.get("APPVEYOR_ACCOUNT")
project = os.environ.get("APPVEYOR_PROJECT")
token = os.environ.get("APPVEYOR_TOKEN")
if not account or not project or not token:
abort(500, 'appveyor config incomplete')
build_url = trigger_appveyor_build(account, project, token)
return jsonify({'msg': 'triggered a build: %s' % build_url})
raise HTTPException(500, 'appveyor config incomplete')
build_url = await trigger_appveyor_build(account, project, token)
return JSONResponse({'msg': 'triggered a build: %s' % build_url})
else:
abort(400, 'Unsupported event type: ' + event)
raise HTTPException(400, 'Unsupported event type: ' + event)
webapp = FastAPI(openapi_url=None)
webapp.mount("/static", StaticFiles(directory=os.path.join(DIR, "static")), name="static")
webapp.include_router(router)
add_etag_exception_handler(webapp)

341
poetry.lock generated
View File

@@ -1,3 +1,19 @@
[[package]]
category = "main"
description = "File support for asyncio."
name = "aiofiles"
optional = false
python-versions = "*"
version = "0.5.0"
[[package]]
category = "dev"
description = "Enhance the standard unittest package with features for testing asyncio libraries"
name = "asynctest"
optional = false
python-versions = ">=3.5"
version = "0.13.0"
[[package]]
category = "dev"
description = "Atomic file writes."
@@ -54,6 +70,32 @@ optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "0.4.3"
[[package]]
category = "main"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
name = "fastapi"
optional = false
python-versions = ">=3.6"
version = "0.55.1"
[package.dependencies]
pydantic = ">=0.32.2,<2.0.0"
starlette = "0.13.2"
[package.extras]
all = ["requests", "aiofiles", "jinja2", "python-multipart", "itsdangerous", "pyyaml", "graphene", "ujson", "orjson", "email-validator", "uvicorn", "async-exit-stack", "async-generator"]
dev = ["pyjwt", "passlib", "autoflake", "flake8", "uvicorn", "graphene"]
doc = ["mkdocs", "mkdocs-material", "markdown-include", "typer", "typer-cli", "pyyaml"]
test = ["pytest (>=4.0.0)", "pytest-cov", "mypy", "black", "isort", "requests", "email-validator", "sqlalchemy", "peewee", "databases", "orjson", "async-exit-stack", "async-generator", "python-multipart", "aiofiles", "flask"]
[[package]]
category = "main"
description = "Convenience library for working with etags in fastapi"
name = "fastapi-etag"
optional = false
python-versions = ">=3.7,<4.0"
version = "0.2.1"
[[package]]
category = "dev"
description = "the modular source code checker: pep8 pyflakes and co"
@@ -71,42 +113,6 @@ pyflakes = ">=2.2.0,<2.3.0"
python = "<3.8"
version = "*"
[[package]]
category = "main"
description = "A simple framework for building complex web applications."
name = "flask"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "1.1.2"
[package.dependencies]
Jinja2 = ">=2.10.1"
Werkzeug = ">=0.15"
click = ">=5.1"
itsdangerous = ">=0.24"
[package.extras]
dev = ["pytest", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"]
docs = ["sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"]
dotenv = ["python-dotenv"]
[[package]]
category = "main"
description = "WSGI HTTP Server for UNIX"
name = "gunicorn"
optional = false
python-versions = ">=3.4"
version = "20.0.4"
[package.dependencies]
setuptools = ">=3.0"
[package.extras]
eventlet = ["eventlet (>=0.9.7)"]
gevent = ["gevent (>=0.13)"]
setproctitle = ["setproctitle"]
tornado = ["tornado (>=0.2)"]
[[package]]
category = "main"
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
@@ -145,16 +151,15 @@ version = "2020.5.19"
[[package]]
category = "main"
description = "A minimal low-level HTTP client."
name = "httpcore"
description = "A collection of framework independent HTTP protocol utils."
marker = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\""
name = "httptools"
optional = false
python-versions = ">=3.6"
version = "0.9.0"
python-versions = "*"
version = "0.1.1"
[package.dependencies]
h11 = ">=0.8,<0.10"
h2 = ">=3.0.0,<4.0.0"
sniffio = ">=1.0.0,<2.0.0"
[package.extras]
test = ["Cython (0.29.14)"]
[[package]]
category = "main"
@@ -162,16 +167,18 @@ description = "The next generation HTTP client."
name = "httpx"
optional = false
python-versions = ">=3.6"
version = "0.13.1"
version = "0.12.1"
[package.dependencies]
certifi = "*"
chardet = ">=3.0.0,<4.0.0"
h11 = ">=0.8,<0.10"
h2 = ">=3.0.0,<4.0.0"
hstspreload = "*"
httpcore = ">=0.9.0,<0.10.0"
idna = ">=2.0.0,<3.0.0"
rfc3986 = ">=1.3,<2"
sniffio = "*"
sniffio = ">=1.0.0,<2.0.0"
urllib3 = ">=1.0.0,<2.0.0"
[[package]]
category = "main"
@@ -205,14 +212,6 @@ zipp = ">=0.5"
docs = ["sphinx", "rst.linker"]
testing = ["packaging", "importlib-resources"]
[[package]]
category = "main"
description = "Various helpers to pass data to untrusted environments and back."
name = "itsdangerous"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.1.0"
[[package]]
category = "main"
description = "A very fast and expressive template engine."
@@ -319,6 +318,19 @@ optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "2.6.0"
[[package]]
category = "main"
description = "Data validation and settings management using python 3.6 type hinting"
name = "pydantic"
optional = false
python-versions = ">=3.6"
version = "1.5.1"
[package.extras]
dotenv = ["python-dotenv (>=0.10.4)"]
email = ["email-validator (>=1.0.3)"]
typing_extensions = ["typing-extensions (>=3.7.2)"]
[[package]]
category = "dev"
description = "passive checker of Python programs"
@@ -361,6 +373,36 @@ version = ">=0.12"
checkqa-mypy = ["mypy (v0.761)"]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
[[package]]
category = "dev"
description = "Python HTTP for Humans."
name = "requests"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "2.23.0"
[package.dependencies]
certifi = ">=2017.4.17"
chardet = ">=3.0.2,<4"
idna = ">=2.5,<3"
urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26"
[package.extras]
security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"]
[[package]]
category = "dev"
description = "A utility for mocking out the Python HTTPX library."
name = "respx"
optional = false
python-versions = ">=3.6"
version = "0.10.1"
[package.dependencies]
asynctest = "*"
httpx = ">=0.12,<0.13"
[[package]]
category = "main"
description = "Validating URI References per RFC 3986"
@@ -388,6 +430,17 @@ optional = false
python-versions = ">=3.5"
version = "1.1.0"
[[package]]
category = "main"
description = "The little ASGI library that shines."
name = "starlette"
optional = false
python-versions = ">=3.6"
version = "0.13.2"
[package.extras]
full = ["aiofiles", "graphene", "itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests", "ujson"]
[[package]]
category = "dev"
description = "a fork of Python 2 and 3 ast modules with type comment support"
@@ -404,6 +457,46 @@ optional = false
python-versions = "*"
version = "3.7.4.2"
[[package]]
category = "main"
description = "HTTP library with thread-safe connection pooling, file post, and more."
name = "urllib3"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
version = "1.25.9"
[package.extras]
brotli = ["brotlipy (>=0.6.0)"]
secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)", "ipaddress"]
socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"]
[[package]]
category = "main"
description = "The lightning-fast ASGI server."
name = "uvicorn"
optional = false
python-versions = "*"
version = "0.11.5"
[package.dependencies]
click = ">=7.0.0,<8.0.0"
h11 = ">=0.8,<0.10"
httptools = ">=0.1.0,<0.2.0"
uvloop = ">=0.14.0"
websockets = ">=8.0.0,<9.0.0"
[package.extras]
watchgodreload = ["watchgod (>=0.6,<0.7)"]
[[package]]
category = "main"
description = "Fast implementation of asyncio event loop on top of libuv"
marker = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\""
name = "uvloop"
optional = false
python-versions = "*"
version = "0.14.0"
[[package]]
category = "dev"
description = "Measures number of Terminal column cells of wide-character codes"
@@ -414,15 +507,11 @@ version = "0.1.9"
[[package]]
category = "main"
description = "The comprehensive WSGI web application library."
name = "werkzeug"
description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
name = "websockets"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "1.0.1"
[package.extras]
dev = ["pytest", "pytest-timeout", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"]
watchdog = ["watchdog"]
python-versions = ">=3.6.1"
version = "8.1"
[[package]]
category = "dev"
@@ -438,10 +527,18 @@ docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
testing = ["jaraco.itertools", "func-timeout"]
[metadata]
content-hash = "545a4c900891c3a80359d7a346f1bc18bbe3f070422a26045df8901ca080e2e2"
content-hash = "3c4bada6c61e8bbdacf58ab39026fc2c40f253cc3b92334b76948f3b318e0a12"
python-versions = "^3.7"
[metadata.files]
aiofiles = [
{file = "aiofiles-0.5.0-py3-none-any.whl", hash = "sha256:377fdf7815cc611870c59cbd07b68b180841d2a2b79812d8c218be02448c2acb"},
{file = "aiofiles-0.5.0.tar.gz", hash = "sha256:98e6bcfd1b50f97db4980e182ddd509b7cc35909e903a8fe50d8849e02d815af"},
]
asynctest = [
{file = "asynctest-0.13.0-py3-none-any.whl", hash = "sha256:5da6118a7e6d6b54d83a8f7197769d046922a44d2a99c21382f0a6e4fadae676"},
{file = "asynctest-0.13.0.tar.gz", hash = "sha256:c27862842d15d83e6a34eb0b2866c323880eb3a75e4485b079ea11748fd77fac"},
]
atomicwrites = [
{file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
{file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
@@ -466,18 +563,18 @@ colorama = [
{file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"},
{file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"},
]
fastapi = [
{file = "fastapi-0.55.1-py3-none-any.whl", hash = "sha256:b1a96ea772f10cd0235eb09d6e282b1f5e6135dad5121ed80d6beb8fa932e075"},
{file = "fastapi-0.55.1.tar.gz", hash = "sha256:912bc1a1b187146fd74dd45e17ea10aede3d962c921142c412458e911c50dc4c"},
]
fastapi-etag = [
{file = "fastapi-etag-0.2.1.tar.gz", hash = "sha256:acf94931f15698dff30bb4257a8d5b3bc31330530cf16db586034d42ece238dd"},
{file = "fastapi_etag-0.2.1-py3-none-any.whl", hash = "sha256:db3d7e9b6883cc5515e05af5bf4eb18dd9147eb9c743ab451aa5a333c73249c5"},
]
flake8 = [
{file = "flake8-3.8.2-py2.py3-none-any.whl", hash = "sha256:ccaa799ef9893cebe69fdfefed76865aeaefbb94cb8545617b2298786a4de9a5"},
{file = "flake8-3.8.2.tar.gz", hash = "sha256:c69ac1668e434d37a2d2880b3ca9aafd54b3a10a3ac1ab101d22f29e29cf8634"},
]
flask = [
{file = "Flask-1.1.2-py2.py3-none-any.whl", hash = "sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557"},
{file = "Flask-1.1.2.tar.gz", hash = "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060"},
]
gunicorn = [
{file = "gunicorn-20.0.4-py2.py3-none-any.whl", hash = "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c"},
{file = "gunicorn-20.0.4.tar.gz", hash = "sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626"},
]
h11 = [
{file = "h11-0.9.0-py2.py3-none-any.whl", hash = "sha256:4bc6d6a1238b7615b266ada57e0618568066f57dd6fa967d1290ec9309b2f2f1"},
{file = "h11-0.9.0.tar.gz", hash = "sha256:33d4bca7be0fa039f4e84d50ab00531047e53d6ee8ffbc83501ea602c169cae1"},
@@ -494,13 +591,23 @@ hstspreload = [
{file = "hstspreload-2020.5.19-py3-none-any.whl", hash = "sha256:6f8f617d8e0dc2f8e2851131b1d43de24adea875a1a9238b1baa1c6373f162cc"},
{file = "hstspreload-2020.5.19.tar.gz", hash = "sha256:d703d771d47035ad9298c14b84b8293a33fcbbfe6deedeeb35fa7e46dc3c619e"},
]
httpcore = [
{file = "httpcore-0.9.0-py3-none-any.whl", hash = "sha256:d6187f7642b1745037253eb5798dee7f17f85c5534a65e2de4daa11627486a0a"},
{file = "httpcore-0.9.0.tar.gz", hash = "sha256:f77ea2e5fecd58d2b049a2efd706788b1808b73c2367479a0f4a49f6e391cba7"},
httptools = [
{file = "httptools-0.1.1-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:a2719e1d7a84bb131c4f1e0cb79705034b48de6ae486eb5297a139d6a3296dce"},
{file = "httptools-0.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:fa3cd71e31436911a44620473e873a256851e1f53dee56669dae403ba41756a4"},
{file = "httptools-0.1.1-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:86c6acd66765a934e8730bf0e9dfaac6fdcf2a4334212bd4a0a1c78f16475ca6"},
{file = "httptools-0.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:bc3114b9edbca5a1eb7ae7db698c669eb53eb8afbbebdde116c174925260849c"},
{file = "httptools-0.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:ac0aa11e99454b6a66989aa2d44bca41d4e0f968e395a0a8f164b401fefe359a"},
{file = "httptools-0.1.1-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:96da81e1992be8ac2fd5597bf0283d832287e20cb3cfde8996d2b00356d4e17f"},
{file = "httptools-0.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:56b6393c6ac7abe632f2294da53f30d279130a92e8ae39d8d14ee2e1b05ad1f2"},
{file = "httptools-0.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:96eb359252aeed57ea5c7b3d79839aaa0382c9d3149f7d24dd7172b1bcecb009"},
{file = "httptools-0.1.1-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:fea04e126014169384dee76a153d4573d90d0cbd1d12185da089f73c78390437"},
{file = "httptools-0.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:3592e854424ec94bd17dc3e0c96a64e459ec4147e6d53c0a42d0ebcef9cb9c5d"},
{file = "httptools-0.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:0a4b1b2012b28e68306575ad14ad5e9120b34fccd02a81eb08838d7e3bbb48be"},
{file = "httptools-0.1.1.tar.gz", hash = "sha256:41b573cf33f64a8f8f3400d0a7faf48e1888582b6f6e02b82b9bd4f0bf7497ce"},
]
httpx = [
{file = "httpx-0.13.1-py3-none-any.whl", hash = "sha256:b132a566677c66d436502484bc16376d1de5b32743d2a319f90bbbeda2acbd95"},
{file = "httpx-0.13.1.tar.gz", hash = "sha256:d90cc7930bb3be2522c398038a07010b7830a8d9a5d9b91cdcaedea79290e9b9"},
{file = "httpx-0.12.1-py3-none-any.whl", hash = "sha256:ce51c8e8ed2834447fde5a94650299fd74017b7da69cc786b6421fefda09a393"},
{file = "httpx-0.12.1.tar.gz", hash = "sha256:405b4749f597b1f45cae5bffc17b23dc251cce30a0c4c8126f1007b9e728a615"},
]
hyperframe = [
{file = "hyperframe-5.2.0-py2.py3-none-any.whl", hash = "sha256:5187962cb16dcc078f23cb5a4b110098d546c3f41ff2d4038a9896893bbd0b40"},
@@ -514,10 +621,6 @@ importlib-metadata = [
{file = "importlib_metadata-1.6.0-py2.py3-none-any.whl", hash = "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f"},
{file = "importlib_metadata-1.6.0.tar.gz", hash = "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e"},
]
itsdangerous = [
{file = "itsdangerous-1.1.0-py2.py3-none-any.whl", hash = "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"},
{file = "itsdangerous-1.1.0.tar.gz", hash = "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19"},
]
jinja2 = [
{file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"},
{file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"},
@@ -601,6 +704,25 @@ pycodestyle = [
{file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"},
{file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"},
]
pydantic = [
{file = "pydantic-1.5.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2a6904e9f18dea58f76f16b95cba6a2f20b72d787abd84ecd67ebc526e61dce6"},
{file = "pydantic-1.5.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:da8099fca5ee339d5572cfa8af12cf0856ae993406f0b1eb9bb38c8a660e7416"},
{file = "pydantic-1.5.1-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:68dece67bff2b3a5cc188258e46b49f676a722304f1c6148ae08e9291e284d98"},
{file = "pydantic-1.5.1-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:ab863853cb502480b118187d670f753be65ec144e1654924bec33d63bc8b3ce2"},
{file = "pydantic-1.5.1-cp36-cp36m-win_amd64.whl", hash = "sha256:2007eb062ed0e57875ce8ead12760a6e44bf5836e6a1a7ea81d71eeecf3ede0f"},
{file = "pydantic-1.5.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:20a15a303ce1e4d831b4e79c17a4a29cb6740b12524f5bba3ea363bff65732bc"},
{file = "pydantic-1.5.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:473101121b1bd454c8effc9fe66d54812fdc128184d9015c5aaa0d4e58a6d338"},
{file = "pydantic-1.5.1-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:9be755919258d5d168aeffbe913ed6e8bd562e018df7724b68cabdee3371e331"},
{file = "pydantic-1.5.1-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:b96ce81c4b5ca62ab81181212edfd057beaa41411cd9700fbcb48a6ba6564b4e"},
{file = "pydantic-1.5.1-cp37-cp37m-win_amd64.whl", hash = "sha256:93b9f265329d9827f39f0fca68f5d72cc8321881cdc519a1304fa73b9f8a75bd"},
{file = "pydantic-1.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e2c753d355126ddd1eefeb167fa61c7037ecd30b98e7ebecdc0d1da463b4ea09"},
{file = "pydantic-1.5.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:8433dbb87246c0f562af75d00fa80155b74e4f6924b0db6a2078a3cd2f11c6c4"},
{file = "pydantic-1.5.1-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:0a1cdf24e567d42dc762d3fed399bd211a13db2e8462af9dfa93b34c41648efb"},
{file = "pydantic-1.5.1-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:8be325fc9da897029ee48d1b5e40df817d97fe969f3ac3fd2434ba7e198c55d5"},
{file = "pydantic-1.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:3714a4056f5bdbecf3a41e0706ec9b228c9513eee2ad884dc2c568c4dfa540e9"},
{file = "pydantic-1.5.1-py36.py37.py38-none-any.whl", hash = "sha256:70f27d2f0268f490fe3de0a9b6fca7b7492b8fd6623f9fecd25b221ebee385e3"},
{file = "pydantic-1.5.1.tar.gz", hash = "sha256:f0018613c7a0d19df3240c2a913849786f21b6539b9f23d85ce4067489dfacfa"},
]
pyflakes = [
{file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"},
{file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"},
@@ -613,6 +735,14 @@ pytest = [
{file = "pytest-5.4.2-py3-none-any.whl", hash = "sha256:95c710d0a72d91c13fae35dce195633c929c3792f54125919847fdcdf7caa0d3"},
{file = "pytest-5.4.2.tar.gz", hash = "sha256:eb2b5e935f6a019317e455b6da83dd8650ac9ffd2ee73a7b657a30873d67a698"},
]
requests = [
{file = "requests-2.23.0-py2.py3-none-any.whl", hash = "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee"},
{file = "requests-2.23.0.tar.gz", hash = "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"},
]
respx = [
{file = "respx-0.10.1-py2.py3-none-any.whl", hash = "sha256:43aca802e0fd0c964865b07f101943e7b5902ea070ec94cf8e84a39db8729b06"},
{file = "respx-0.10.1.tar.gz", hash = "sha256:190d1fb5bddaf6fcc1319a3cdfbd682c77d7167017b3283cbe79b8fb74927135"},
]
rfc3986 = [
{file = "rfc3986-1.4.0-py2.py3-none-any.whl", hash = "sha256:af9147e9aceda37c91a05f4deb128d4b4b49d6b199775fd2d2927768abdc8f50"},
{file = "rfc3986-1.4.0.tar.gz", hash = "sha256:112398da31a3344dc25dbf477d8df6cb34f9278a94fee2625d89e4514be8bb9d"},
@@ -625,6 +755,10 @@ sniffio = [
{file = "sniffio-1.1.0-py3-none-any.whl", hash = "sha256:20ed6d5b46f8ae136d00b9dcb807615d83ed82ceea6b2058cecb696765246da5"},
{file = "sniffio-1.1.0.tar.gz", hash = "sha256:8e3810100f69fe0edd463d02ad407112542a11ffdc29f67db2bf3771afb87a21"},
]
starlette = [
{file = "starlette-0.13.2-py3-none-any.whl", hash = "sha256:6169ee78ded501095d1dda7b141a1dc9f9934d37ad23196e180150ace2c6449b"},
{file = "starlette-0.13.2.tar.gz", hash = "sha256:a9bb130fa7aa736eda8a814b6ceb85ccf7a209ed53843d0d61e246b380afa10f"},
]
typed-ast = [
{file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"},
{file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"},
@@ -653,13 +787,52 @@ typing-extensions = [
{file = "typing_extensions-3.7.4.2-py3-none-any.whl", hash = "sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5"},
{file = "typing_extensions-3.7.4.2.tar.gz", hash = "sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae"},
]
urllib3 = [
{file = "urllib3-1.25.9-py2.py3-none-any.whl", hash = "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115"},
{file = "urllib3-1.25.9.tar.gz", hash = "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527"},
]
uvicorn = [
{file = "uvicorn-0.11.5-py3-none-any.whl", hash = "sha256:50577d599775dac2301bac8bd5b540d19a9560144143c5bdab13cba92783b6e7"},
{file = "uvicorn-0.11.5.tar.gz", hash = "sha256:596eaa8645b6dbc24d6610e335f8ddf5f925b4c4b86fdc7146abb0bf0da65d17"},
]
uvloop = [
{file = "uvloop-0.14.0-cp35-cp35m-macosx_10_11_x86_64.whl", hash = "sha256:08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd"},
{file = "uvloop-0.14.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:4544dcf77d74f3a84f03dd6278174575c44c67d7165d4c42c71db3fdc3860726"},
{file = "uvloop-0.14.0-cp36-cp36m-macosx_10_11_x86_64.whl", hash = "sha256:b4f591aa4b3fa7f32fb51e2ee9fea1b495eb75b0b3c8d0ca52514ad675ae63f7"},
{file = "uvloop-0.14.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362"},
{file = "uvloop-0.14.0-cp37-cp37m-macosx_10_11_x86_64.whl", hash = "sha256:afd5513c0ae414ec71d24f6f123614a80f3d27ca655a4fcf6cabe50994cc1891"},
{file = "uvloop-0.14.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95"},
{file = "uvloop-0.14.0-cp38-cp38-macosx_10_11_x86_64.whl", hash = "sha256:bcac356d62edd330080aed082e78d4b580ff260a677508718f88016333e2c9c5"},
{file = "uvloop-0.14.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:4315d2ec3ca393dd5bc0b0089d23101276778c304d42faff5dc4579cb6caef09"},
{file = "uvloop-0.14.0.tar.gz", hash = "sha256:123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e"},
]
wcwidth = [
{file = "wcwidth-0.1.9-py2.py3-none-any.whl", hash = "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1"},
{file = "wcwidth-0.1.9.tar.gz", hash = "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1"},
]
werkzeug = [
{file = "Werkzeug-1.0.1-py2.py3-none-any.whl", hash = "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43"},
{file = "Werkzeug-1.0.1.tar.gz", hash = "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"},
websockets = [
{file = "websockets-8.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c"},
{file = "websockets-8.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170"},
{file = "websockets-8.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8"},
{file = "websockets-8.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb"},
{file = "websockets-8.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5"},
{file = "websockets-8.1-cp36-cp36m-win32.whl", hash = "sha256:2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a"},
{file = "websockets-8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5"},
{file = "websockets-8.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989"},
{file = "websockets-8.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d"},
{file = "websockets-8.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779"},
{file = "websockets-8.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8"},
{file = "websockets-8.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422"},
{file = "websockets-8.1-cp37-cp37m-win32.whl", hash = "sha256:7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc"},
{file = "websockets-8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308"},
{file = "websockets-8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092"},
{file = "websockets-8.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485"},
{file = "websockets-8.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1"},
{file = "websockets-8.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55"},
{file = "websockets-8.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824"},
{file = "websockets-8.1-cp38-cp38-win32.whl", hash = "sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36"},
{file = "websockets-8.1-cp38-cp38-win_amd64.whl", hash = "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b"},
{file = "websockets-8.1.tar.gz", hash = "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f"},
]
zipp = [
{file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"},

View File

@@ -7,14 +7,19 @@ license = "MIT"
[tool.poetry.dependencies]
python = "^3.7"
flask = "^1.1.2"
httpx = "^0.13.1"
gunicorn = "^20.0.4"
httpx = "^0.12"
fastapi = "^0.55.1"
uvicorn = "^0.11.5"
aiofiles = "^0.5.0"
jinja2 = "^2.11.2"
fastapi-etag = "^0.2.1"
[tool.poetry.dev-dependencies]
mypy = "^0.770"
flake8 = "^3.8.2"
pytest = "^5.4.2"
requests = "^2.23.0"
respx = "^0.10.1"
[build-system]
requires = ["poetry>=0.12"]

5
run.py
View File

@@ -5,6 +5,7 @@ import sys
import argparse
from typing import List, Optional, Union
import uvicorn
from app import app
from app import appconfig
@@ -15,12 +16,10 @@ def main(argv: List[str]) -> Optional[Union[int, str]]:
help="use local repo cache")
parser.add_argument("-p", "--port", type=int, default=8160,
help="port number")
parser.add_argument("-d", "--debug", action="store_true")
args = parser.parse_args()
appconfig.CACHE_LOCAL = args.cache
print("http://localhost:%d" % args.port)
app.run(port=args.port, debug=args.debug)
uvicorn.run(app, host="127.0.0.1", port=args.port)
return None

View File

@@ -2,27 +2,33 @@
import os
os.environ["NO_UPDATE_THREAD"] = "1"
import respx
import pytest
from app import app
from app.fetch import parse_cygwin_versions
from fastapi.testclient import TestClient
@pytest.fixture
def client():
client = app.test_client()
yield client
os.environ["NO_UPDATE_THREAD"] = "1"
with TestClient(app) as client:
yield client
def test_main_endpoints(client):
endpoints = [
'', 'repos', 'base', 'group', 'updates', 'outofdate', 'queue', 'new',
'removals', 'search', 'base/foo', 'group/foo', 'package/foo']
for name in endpoints:
r = client.get('/' + name, follow_redirects=True)
assert r.status_code == 200
@pytest.mark.parametrize("endpoint", [
'', 'repos', 'base', 'group', 'updates', 'outofdate', 'queue', 'new',
'removals', 'search', 'base/foo', 'group/foo', 'package/foo', 'python2',
])
def test_main_endpoints(client, endpoint):
r = client.get('/' + endpoint)
assert r.status_code == 200
assert "etag" in r.headers
etag = r.headers["etag"]
r = client.get('/' + endpoint, headers={"if-none-match": etag})
assert r.status_code == 304
r = client.get('/' + endpoint, headers={"if-none-match": "nope"})
assert r.status_code == 200
def test_parse_cygwin_versions():
@@ -40,3 +46,38 @@ source: x86_64/release/python36/python36-3.6.9-1-src.tar.xz 17223444 ef39d9419""
assert versions["python36"][0] == "3.6.9"
assert versions["python36"][1] == "https://cygwin.com/packages/summary/python36-src.html"
assert versions["python36"][2] == "https://mirrors.kernel.org/sourceware/cygwin/x86_64/release/python36/python36-3.6.9-1-src.tar.xz"
def test_webhook_ping(client, monkeypatch):
monkeypatch.setenv("GITHUB_WEBHOOK_SECRET", "foobar")
r = client.post("/webhook", headers={
"X-Hub-Signature": "sha1=241ebb961521e58a8b2d5d1436863df772ffd531",
"X-GitHub-Event": "ping",
})
r.raise_for_status()
assert r.json() == {"msg": "pong"}
def test_webhook_push(client, monkeypatch):
monkeypatch.setenv("GITHUB_WEBHOOK_SECRET", "foobar")
monkeypatch.setenv("APPVEYOR_ACCOUNT", "account")
monkeypatch.setenv("APPVEYOR_PROJECT", "project")
monkeypatch.setenv("APPVEYOR_TOKEN", "token")
with respx.mock:
request = respx.post(
"https://ci.appveyor.com/api/builds",
status_code=201,
content={
"buildId": 1234
})
r = client.post("/webhook", headers={
"X-Hub-Signature": "sha1=241ebb961521e58a8b2d5d1436863df772ffd531",
"X-GitHub-Event": "push",
})
assert request.called
r.raise_for_status()
assert "msg" in r.json()
assert "1234" in r.json()["msg"]