Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

uv migration #186

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.docker
.git/
/.venv/
/venv/
/.idea/
**/__pycache__/
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
.venv/*
venv/*
.idea/*
__pycache__
*.egg-info/*
.coverage
.pytest_cache/
build/
2 changes: 2 additions & 0 deletions .tekton/github-mirror-master-pull-request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ spec:
value: Dockerfile
- name: path-context
value: .
- name: target-stage
value: test
pipelineRef:
resolver: git
params:
Expand Down
2 changes: 2 additions & 0 deletions .tekton/github-mirror-master-push.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ spec:
value: Dockerfile
- name: path-context
value: .
- name: target-stage
value: prod
pipelineRef:
resolver: git
params:
Expand Down
41 changes: 23 additions & 18 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,28 +1,33 @@
FROM registry.access.redhat.com/ubi9/python-311:1-77@sha256:3231676407c7e727cfbd853137cfaab2f891410762268fbebe4ea9f27d8f568b as builder
FROM registry.access.redhat.com/ubi9/python-311:1-77@sha256:3231676407c7e727cfbd853137cfaab2f891410762268fbebe4ea9f27d8f568b AS builder
COPY --from=ghcr.io/astral-sh/uv:0.5.5@sha256:dc60491f42c9c7228fe2463f551af49a619ebcc9cbd10a470ced7ada63aa25d4 /uv /bin/uv
WORKDIR /ghmirror
RUN python3 -m venv venv
ENV VIRTUAL_ENV=/ghmirror/venv
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
COPY --chown=1001:0 setup.py VERSION ./
RUN pip install .

FROM builder as test
COPY --chown=1001:0 requirements-check.txt ./
RUN pip install -r requirements-check.txt
COPY --chown=1001:0 . ./
ENTRYPOINT ["make"]
CMD ["check"]
COPY --chown=1001:0 pyproject.toml uv.lock ./
RUN uv lock --locked
COPY --chown=1001:0 ghmirror ./ghmirror
RUN uv sync --frozen --no-cache --compile-bytecode --no-group dev --python /usr/bin/python3.11

FROM registry.access.redhat.com/ubi9/ubi-minimal:9.4-1227@sha256:f182b500ff167918ca1010595311cf162464f3aa1cab755383d38be61b4d30aa
FROM registry.access.redhat.com/ubi9/ubi-minimal:9.4-1227@sha256:f182b500ff167918ca1010595311cf162464f3aa1cab755383d38be61b4d30aa AS prod
RUN microdnf upgrade -y && \
microdnf install -y python3.11 && \
microdnf clean all
COPY LICENSE /licenses/LICENSE
USER 1001
WORKDIR /ghmirror
ENV VIRTUAL_ENV=/ghmirror/venv
RUN chown -R 1001:0 /ghmirror
USER 1001
ENV VIRTUAL_ENV=/ghmirror/.venv
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
COPY --from=builder $VIRTUAL_ENV $VIRTUAL_ENV
COPY --chown=1001:0 . ./
COPY --from=builder /ghmirror /ghmirror
ENTRYPOINT ["gunicorn", "ghmirror.app:APP"]
CMD ["--workers", "1", "--threads", "8", "--bind", "0.0.0.0:8080"]

FROM prod AS test
COPY --from=ghcr.io/astral-sh/uv:0.5.5@sha256:dc60491f42c9c7228fe2463f551af49a619ebcc9cbd10a470ced7ada63aa25d4 /uv /bin/uv
USER root
RUN microdnf install -y make
USER 1001
COPY --chown=1001:0 Makefile ./
COPY --chown=1001:0 tests ./tests
ENV UV_NO_CACHE=true
RUN uv sync --frozen
RUN make check

15 changes: 5 additions & 10 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
develop:
pip install --editable .
pip install -r requirements-check.txt


check:
ruff check --no-fix
ruff format --check
python3 -m pytest -v --forked --cov=ghmirror --cov-report=term-missing tests/
uv run ruff check --no-fix
uv run ruff format --check
uv run pytest -v --forked --cov=ghmirror --cov-report=term-missing tests/

accept:
python3 acceptance/test_basic.py

format:
ruff check
ruff format
uv run ruff check
uv run ruff format
1 change: 0 additions & 1 deletion VERSION

This file was deleted.

Empty file added acceptance/__init__.py
Empty file.
2 changes: 1 addition & 1 deletion acceptance/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def test_get_repo(path, code, cache):
url = f"{GITHUB_MIRROR_URL}{path}"
headers = {"Authorization": f"token {CLIENT_TOKEN}"}

response = requests.get(url, headers=headers)
response = requests.get(url, headers=headers, timeout=60)

assert response.status_code == code
if cache is not None:
Expand Down
28 changes: 11 additions & 17 deletions ghmirror/app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@
# Copyright: Red Hat Inc. 2020
# Author: Amador Pahim <[email protected]>

"""
The GitHub Mirror endpoints
"""
"""The GitHub Mirror endpoints"""

import logging
import os
Expand All @@ -37,12 +35,10 @@


def error_handler(exception):
"""
Used when an exception happens in the flask app.
"""
"""Used when an exception happens in the flask app."""
return (
flask.jsonify(
message=f"Error reaching {GH_API}: {str(exception.__class__.__name__)}"
message=f"Error reaching {GH_API}: {exception.__class__.__name__!s}"
),
502,
)
Expand All @@ -54,17 +50,13 @@ def error_handler(exception):

@APP.route("/healthz", methods=["GET"])
def healthz():
"""
Health check endpoint for Kubernetes.
"""
"""Health check endpoint for Kubernetes."""
return flask.Response("OK")


@APP.route("/metrics", methods=["GET"])
def metrics():
"""
Prometheus metrics endpoint.
"""
"""Prometheus metrics endpoint."""
headers = {"Content-type": "text/plain"}

stats_cache = StatsCache()
Expand All @@ -80,9 +72,7 @@ def metrics():
@APP.route("/<path:path>", methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
@check_user
def ghmirror(path):
"""
Default endpoint, matching any url without a specific endpoint.
"""
"""Default endpoint, matching any url without a specific endpoint."""
url = f"{GH_API}/{path}"

if flask.request.args:
Expand Down Expand Up @@ -111,4 +101,8 @@ def ghmirror(path):


if __name__ == "__main__": # pragma: no cover
APP.run(host="127.0.0.1", debug=True, port="8080")
APP.run(
host="127.0.0.1",
debug=bool(os.environ.get("GITHUB_MIRROR_DEBUG", "1")),
port=8080,
)
4 changes: 1 addition & 3 deletions ghmirror/core/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@
# Copyright: Red Hat Inc. 2020
# Author: Amador Pahim <[email protected]>

"""
System constants.
"""
"""System constants."""

GH_API = "https://api.github.com"
GH_STATUS_API = "https://www.githubstatus.com/api/v2/components.json"
Expand Down
47 changes: 15 additions & 32 deletions ghmirror/core/mirror_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@
# Copyright: Red Hat Inc. 2020
# Author: Amador Pahim <[email protected]>

"""
Implements conditional requests
"""
"""Implements conditional requests"""

# ruff: noqa: PLR2004
import hashlib
Expand All @@ -35,10 +33,7 @@


def _get_elements_per_page(url_params):
"""
Get 'per_page' parameter if present in URL or
return None if not present
"""
"""Get 'per_page' parameter if present in URL or return None if not present"""
if url_params is not None:
per_page = url_params.get("per_page")
if per_page is not None:
Expand All @@ -48,10 +43,10 @@ def _get_elements_per_page(url_params):


def _cache_response(resp, cache, cache_key):
"""
Implements the logic to decide whether or not
whe should cache a request acording to the headers
and content
"""Cache response if it makes sense

Implements the logic to decide whether or not whe should cache a request acording
to the headers and content
"""
# Caching only makes sense when at least one
# of those headers is present
Expand All @@ -65,10 +60,7 @@ def _cache_response(resp, cache, cache_key):
def _online_request(
session, method, url, cached_response, headers=None, parameters=None
):
"""
Handle API errors on conditional requests and try
to serve contents from cache
"""
"""Handle API errors on conditional requests and try to serve contents from cache"""
try:
resp = session.request(
method=method,
Expand Down Expand Up @@ -146,21 +138,18 @@ def _handle_not_changed(

@requests_metrics
def conditional_request(session, method, url, auth, data=None, url_params=None):
"""
Implements conditional requests, checking first whether
the upstream API is online of offline to decide which
"""Implements conditional requests.

Checking first whether the upstream API is online of offline to decide which
request routine to call.
"""
if GithubStatus().online:
return online_request(session, method, url, auth, data, url_params)
return offline_request(method, url, auth)


# pylint: disable-msg=too-many-locals
def online_request(session, method, url, auth, data=None, url_params=None):
"""
Implements conditional requests.
"""
"""Implements conditional requests."""
cache = RequestsCache()
headers = {}
parameters = url_params.to_dict() if url_params is not None else {}
Expand Down Expand Up @@ -241,8 +230,7 @@ def online_request(session, method, url, auth, data=None, url_params=None):


def _should_error_response_be_served_from_cache(response):
"""Parse a response to check if we should serve contents
from cache
"""Parse a response to check if we should serve contents from cache

:param response: requests module response
:type response: requests.Response
Expand All @@ -251,7 +239,6 @@ def _should_error_response_be_served_from_cache(response):
from cache
:rtype: str, optional
"""

if _is_rate_limit_error(response):
return "RATE_LIMITED"

Expand Down Expand Up @@ -280,9 +267,7 @@ def _is_rate_limit_error(response):
def offline_request(
method, url, auth, error_code=504, error_message=b'{"message": "gateway timeout"}\n'
):
"""
Implements offline requests (serves content from cache, when possible).
"""
"""Implements offline requests (serves content from cache, when possible)."""
headers = {}
if auth is None:
auth_sha = None
Expand All @@ -299,8 +284,7 @@ def offline_request(
response = requests.models.Response()
response.status_code = error_code
response.headers["X-Cache"] = "OFFLINE_MISS"
# pylint: disable=protected-access
response._content = error_message
response._content = error_message # noqa: SLF001
return response

cache = RequestsCache()
Expand All @@ -320,6 +304,5 @@ def offline_request(
response = requests.models.Response()
response.status_code = error_code
response.headers["X-Cache"] = "OFFLINE_MISS"
# pylint: disable=protected-access
response._content = error_message
response._content = error_message # noqa: SLF001
return response
31 changes: 13 additions & 18 deletions ghmirror/core/mirror_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,14 @@
# Copyright: Red Hat Inc. 2020
# Author: Amador Pahim <[email protected]>

"""
Module containing all the abstractions around an HTTP response.
"""
"""Module containing all the abstractions around an HTTP response."""


class MirrorResponse:
"""
Wrapper around the requests.Response, implementing properties
that replace the strings containing the GutHub API url by the
mirror url where needed.
"""Wrapper around the requests.Response.

Implementing properties that replace the strings containing the
GutHub API url by the mirror url where needed.

:param original_response: the return from the original request
to the GitHub API
Expand All @@ -40,10 +38,10 @@ def __init__(self, original_response, gh_api_url, gh_mirror_url):

@property
def headers(self):
"""
Retrieves the headers we are interested in from the original
response and sanitizes them so we can impersonate the GitHub
API.
"""Sanitize headers.

Retrieves the headers we are interested in from the original response and
sanitizes them so we can impersonate the GitHub API.

:return: the sanitized headers
:rtype: dict
Expand Down Expand Up @@ -76,7 +74,8 @@ def headers(self):

@property
def content(self):
"""
"""Sanitize content.

Retrieves the content from the original response and sanitizes
them so we can impersonate the GitHub API.

Expand All @@ -86,17 +85,13 @@ def content(self):
if self._original_response.content is None:
return None

sanitized_content = self._original_response.content.replace(
return self._original_response.content.replace(
self._gh_api_url.encode(), self._gh_mirror_url.encode()
)

return sanitized_content

@property
def status_code(self):
"""
Convenience method to expose the original response HTTP
status code.
"""Convenience method to expose the original response HTTP status code.

:return: the response status code
"""
Expand Down
Loading