Skip to content

Commit

Permalink
uv migration (#186)
Browse files Browse the repository at this point in the history
* 🚀 switch to uv

* 🚨 fix linter errors
  • Loading branch information
chassing authored Dec 2, 2024
1 parent 6d7032d commit eefd0cb
Show file tree
Hide file tree
Showing 29 changed files with 664 additions and 261 deletions.
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

0 comments on commit eefd0cb

Please sign in to comment.