@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
385
app/web.py
385
app/web.py
@@ -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
341
poetry.lock
generated
@@ -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"},
|
||||
|
||||
@@ -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
5
run.py
@@ -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
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user