Minimal PyPI server for uploading & downloading packages with pip/easy_install
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
pypiserver/pypiserver/__main__.py

221 lines
6.8 KiB

#! /usr/bin/env python3
"""Entrypoint for pypiserver."""
import enum
import importlib
import logging
import sys
import typing as t
from pathlib import Path
from wsgiref.simple_server import WSGIRequestHandler
import functools as ft
from pypiserver.config import Config, UpdateConfig
log = logging.getLogger("pypiserver.main")
def init_logging(
level: int = logging.NOTSET,
frmt: str = None,
filename: t.Union[str, Path] = None,
stream: t.Optional[t.IO] = sys.stderr,
logger: logging.Logger = None,
) -> None:
"""Configure the specified logger, or the root logger otherwise."""
logger = logger or logging.getLogger()
logger.setLevel(level)
formatter = logging.Formatter(frmt)
if len(logger.handlers) == 0 and stream is not None:
handler = logging.StreamHandler(stream)
handler.setFormatter(formatter)
logger.addHandler(logging.StreamHandler(stream))
if filename:
handler = logging.FileHandler(filename)
handler.setFormatter(formatter)
logger.addHandler(handler)
class WsgiHandler(WSGIRequestHandler):
"""A simple request handler to configure logging."""
# The default `FixedHandler` that bottle's `WSGIRefServer` uses does not
# log in a particularly predictable or configurable way. We'll pass this
# in to use instead.
def address_string(self) -> str: # Prevent reverse DNS lookups please.
# This method copied directly from bottle's `FixedHandler` and
# maintained on the Chesterton's fence principle (i.e. I don't know
# why it's important, so I'm not going to get rid of it)
return self.client_address[0]
def log_message(
self, format: str, *args: t.Any # pylint: disable=redefined-builtin
) -> None:
"""Log a message."""
# The log_message method on the `HttpRequestHandler` base class just
# writes directly to stderr. We'll use its same formatting, but pass
# it through the logger instead.
log.info(
"%s - - [%s] %s\n",
self.address_string(),
self.log_date_time_string(),
format % args,
)
class AutoServer(enum.Enum):
"""Expected servers that can be automaticlaly selected by bottle."""
Waitress = enum.auto()
Paste = enum.auto()
Twisted = enum.auto()
CherryPy = enum.auto()
WsgiRef = enum.auto()
# Possible automatically selected servers. This MUST match the available
# auto servers in bottle.py
AUTO_SERVER_IMPORTS = (
(AutoServer.Waitress, "waitress"),
(AutoServer.Paste, "paste"),
(AutoServer.Twisted, "twisted.web"),
(AutoServer.CherryPy, "cheroot.wsgi"),
(AutoServer.CherryPy, "cherrypy.wsgiserver"),
# this should always be available because it's part of the stdlib
(AutoServer.WsgiRef, "wsgiref"),
)
def _can_import(name: str) -> bool:
"""Attempt to import a module. Return a bool indicating success."""
try:
importlib.import_module(name)
return True
except ImportError:
return False
def guess_auto_server() -> AutoServer:
"""Guess which server bottle will use for the auto setting."""
# Return the first server that can be imported.
server = next(
(s for s, i in AUTO_SERVER_IMPORTS if _can_import(i)),
None,
)
if server is None:
raise RuntimeError(
"Unexpected error determining bottle auto server. There may be an "
"issue with this python environment. Please report this bug at "
"https://github.com/pypiserver/pypiserver/issues"
)
return server
def main(argv: t.Sequence[str] = None) -> None:
"""Application entrypoint for pypiserver.
This function drives the application (as opposed to the library)
implementation of pypiserver. Usage from the commandline will result in
this function being called.
"""
# pylint: disable=import-outside-toplevel
import pypiserver # pylint: disable=redefined-outer-name
if argv is None:
# The first item in sys.argv is the name of the python file being
# executed, which we don't need
argv = sys.argv[1:]
config = Config.from_args(argv)
init_logging(
level=config.log_level,
filename=config.log_file,
frmt=config.log_frmt,
stream=config.log_stream,
)
# Check to see if we were asked to run an update command instead of running
# the server
if isinstance(config, UpdateConfig):
from pypiserver.manage import update_all_packages
update_all_packages(
config.roots,
config.download_directory,
dry_run=not config.execute,
stable_only=config.allow_unstable,
ignorelist=config.ignorelist,
)
return
# Fixes #49:
# The gevent server adapter needs to patch some
# modules BEFORE importing bottle!
if config.server_method.startswith("gevent"):
import gevent.monkey # @UnresolvedImport
gevent.monkey.patch_all()
from pypiserver import bottle
bottle.debug(config.verbosity > 1)
bottle._stderr = ft.partial( # pylint: disable=protected-access
_logwrite, logging.getLogger(bottle.__name__), logging.INFO
)
# Here `app` is a Bottle instance, which we pass to bottle.run() to run
# the server
app = pypiserver.app_from_config(config)
if config.server_method == "gunicorn":
# When bottle runs gunicorn, gunicorn tries to pull its arguments from
# sys.argv. Because pypiserver's arguments don't match gunicorn's,
# this leads to errors.
# Gunicorn can be configured by using a `gunicorn.conf.py` config file
# or by specifying the `GUNICORN_CMD_ARGS` env var. See gunicorn
# docs for more info.
sys.argv = ["gunicorn"]
wsgi_kwargs = {"handler_class": WsgiHandler}
if config.server_method == "auto":
expected_server = guess_auto_server()
extra_kwargs = (
wsgi_kwargs if expected_server is AutoServer.WsgiRef else {}
)
log.debug(
"Server 'auto' selected. Expecting bottle to run '%s'. "
"Passing extra keyword args: %s",
expected_server.name,
extra_kwargs,
)
else:
extra_kwargs = wsgi_kwargs if config.server_method == "wsgiref" else {}
log.debug(
"Running bottle with selected server '%s'", config.server_method
)
bottle.run(
app=app,
host=config.host,
port=config.port,
server=config.server_method,
**extra_kwargs,
)
def _logwrite(logger, level, msg):
if msg:
line_endings = ["\r\n", "\n\r", "\n"]
for le in line_endings: # pylint: disable=invalid-name
if msg.endswith(le):
msg = msg[: -len(le)]
if msg:
logger.log(level, msg)
if __name__ == "__main__":
main()