pypiserver 2.4.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pypiserver/__init__.py +222 -0
- pypiserver/__main__.py +231 -0
- pypiserver/_app.py +435 -0
- pypiserver/backend.py +325 -0
- pypiserver/bottle_wrapper/__init__.py +5 -0
- pypiserver/bottle_wrapper/bottle.py +3812 -0
- pypiserver/cache.py +147 -0
- pypiserver/config.py +1035 -0
- pypiserver/core.py +70 -0
- pypiserver/manage.py +220 -0
- pypiserver/pkg_helpers.py +112 -0
- pypiserver/plugin.py +42 -0
- pypiserver/welcome.html +33 -0
- pypiserver-2.4.2.dist-info/METADATA +1373 -0
- pypiserver-2.4.2.dist-info/RECORD +19 -0
- pypiserver-2.4.2.dist-info/WHEEL +4 -0
- pypiserver-2.4.2.dist-info/entry_points.txt +5 -0
- pypiserver-2.4.2.dist-info/licenses/AUTHORS.rst +15 -0
- pypiserver-2.4.2.dist-info/licenses/LICENSE.txt +50 -0
pypiserver/__init__.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import pathlib
|
|
3
|
+
import re as _re
|
|
4
|
+
import sys
|
|
5
|
+
import typing as t
|
|
6
|
+
|
|
7
|
+
from pypiserver.bottle_wrapper import Bottle
|
|
8
|
+
from pypiserver.config import Config, RunConfig, strtobool
|
|
9
|
+
|
|
10
|
+
__version__ = "2.4.2"
|
|
11
|
+
version = __version__
|
|
12
|
+
__version_info__ = tuple(_re.split("[.-]", __version__))
|
|
13
|
+
__updated__ = "2026-09-20 13:04:37"
|
|
14
|
+
|
|
15
|
+
__title__ = "pypiserver"
|
|
16
|
+
__summary__ = "A minimal PyPI server for use with pip/easy_install."
|
|
17
|
+
__uri__ = "https://github.com/pypiserver/pypiserver"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def identity(x):
|
|
21
|
+
return x
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def backwards_compat_kwargs(kwargs: dict, warn: bool = True) -> dict:
|
|
25
|
+
"""Return a dict with deprecated kwargs converted to new kwargs.
|
|
26
|
+
|
|
27
|
+
:param kwargs: the incoming kwargs to convert
|
|
28
|
+
:param warn: whether to output a warning to stderr if there are deprecated
|
|
29
|
+
kwargs found in the incoming kwargs
|
|
30
|
+
"""
|
|
31
|
+
# A mapping of deprecated kwargs to a 2-tuple of their corresponding updated
|
|
32
|
+
# kwarg and a function to convert the value of the deprecated kwarg to a
|
|
33
|
+
# value for the new kwarg. `identity` is just a function that returns
|
|
34
|
+
# whatever it is passed and is used in cases where the only change from
|
|
35
|
+
# a legacy kwarg is its name.
|
|
36
|
+
backwards_compat = {
|
|
37
|
+
"authenticated": ("authenticate", identity),
|
|
38
|
+
"passwords": ("password_file", identity),
|
|
39
|
+
# `root` could be a string or an array of strings. Handle both cases,
|
|
40
|
+
# converting strings to Path instances.
|
|
41
|
+
"root": (
|
|
42
|
+
"roots",
|
|
43
|
+
lambda root: [
|
|
44
|
+
# Convert strings to absolute Path instances
|
|
45
|
+
pathlib.Path(r).expanduser().resolve()
|
|
46
|
+
for r in ([root] if isinstance(root, str) else root)
|
|
47
|
+
],
|
|
48
|
+
),
|
|
49
|
+
# `redirect_to_fallback` was changed to `disable_fallback` for clearer
|
|
50
|
+
# use as a flag to disable the default behavior. Since its behavior
|
|
51
|
+
# is the opposite, we negate it.
|
|
52
|
+
"redirect_to_fallback": (
|
|
53
|
+
"disable_fallback",
|
|
54
|
+
lambda redirect: not redirect,
|
|
55
|
+
),
|
|
56
|
+
"server": ("server_method", identity),
|
|
57
|
+
# `welcome_msg` now is just provided as text, so that anyone using
|
|
58
|
+
# pypiserver as a library doesn't need to worry about distributing
|
|
59
|
+
# files if they don't need to. If we're still passed an old-style
|
|
60
|
+
# `welcome_file` argument, we go ahead and resolve it to an absolute
|
|
61
|
+
# path and read the text.
|
|
62
|
+
"welcome_file": (
|
|
63
|
+
"welcome_msg",
|
|
64
|
+
lambda p: pathlib.Path(p).expanduser().resolve().read_text(),
|
|
65
|
+
),
|
|
66
|
+
}
|
|
67
|
+
# Warn the user if they're using any deprecated arguments
|
|
68
|
+
if warn and any(k in backwards_compat for k in kwargs):
|
|
69
|
+
# Make nice instructions like `Please replace the following:
|
|
70
|
+
# 'authenticated' with 'authenticate'` and print to stderr.
|
|
71
|
+
replacement_strs = (
|
|
72
|
+
f"'{k}' with '{backwards_compat[k][0]}'"
|
|
73
|
+
for k in filter(lambda k: k in kwargs, backwards_compat)
|
|
74
|
+
)
|
|
75
|
+
warn_str = (
|
|
76
|
+
"You are using deprecated arguments. Please replace the following: \n"
|
|
77
|
+
f" {', '.join(replacement_strs)}"
|
|
78
|
+
)
|
|
79
|
+
print(warn_str, file=sys.stderr)
|
|
80
|
+
|
|
81
|
+
# Create an iterable of 2-tuple to collect into the updated dictionary. Each
|
|
82
|
+
# item will either be the existing key-value pair from kwargs, or, if the
|
|
83
|
+
# keyword is a legacy keyword, the new key and potentially adjusted value
|
|
84
|
+
# for that keyword. Note that depending on the order the argument are
|
|
85
|
+
# specified, this _could_ mean an updated legacy keyword could override
|
|
86
|
+
# a new argument if that argument is also specified. However, in that
|
|
87
|
+
# case, our updated kwargs dictionary would have a different number of
|
|
88
|
+
# keys compared to our incoming dictionary, so we check for that case
|
|
89
|
+
# below.
|
|
90
|
+
rv_iter = (
|
|
91
|
+
(
|
|
92
|
+
(k, v)
|
|
93
|
+
if k not in backwards_compat
|
|
94
|
+
else (backwards_compat[k][0], backwards_compat[k][1](v))
|
|
95
|
+
)
|
|
96
|
+
for k, v in kwargs.items()
|
|
97
|
+
)
|
|
98
|
+
updated_kwargs = dict(rv_iter)
|
|
99
|
+
|
|
100
|
+
# If our dictionaries have different lengths, we must have gotten duplicate
|
|
101
|
+
# legacy/modern keys. Figure out which keys were dupes and throw an error.
|
|
102
|
+
if len(updated_kwargs) != len(kwargs):
|
|
103
|
+
legacy_to_modern = {k: v[0] for k, v in backwards_compat.items()}
|
|
104
|
+
dupes = [
|
|
105
|
+
(k, v)
|
|
106
|
+
for k, v in legacy_to_modern.items()
|
|
107
|
+
if k in kwargs and v in kwargs
|
|
108
|
+
]
|
|
109
|
+
raise ValueError(
|
|
110
|
+
"Keyword arguments for pypiserver app() constructor contained "
|
|
111
|
+
"duplicate legacy and modern keys. Duplicates are shown below, in "
|
|
112
|
+
"the form (legacy_key, modern_key):\n"
|
|
113
|
+
f"{dupes}"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
return updated_kwargs
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def app(**kwargs: t.Any) -> Bottle:
|
|
120
|
+
"""Construct a bottle app running pypiserver.
|
|
121
|
+
|
|
122
|
+
:param kwds: Any overrides for defaults. Any property of RunConfig
|
|
123
|
+
(or its base), defined in `pypiserver.config`, may be overridden.
|
|
124
|
+
"""
|
|
125
|
+
config = Config.default_with_overrides(**backwards_compat_kwargs(kwargs))
|
|
126
|
+
return setup_routes_from_config(app_from_config(config), config)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def app_from_config(config: RunConfig) -> Bottle:
|
|
130
|
+
"""Construct a bottle app from the provided RunConfig."""
|
|
131
|
+
# The _app module instantiates a Bottle instance directly when it is
|
|
132
|
+
# imported. That is `_app.app`. We directly mutate some global variables
|
|
133
|
+
# on the imported `_app` module so that its endpoints will behave as
|
|
134
|
+
# we expect.
|
|
135
|
+
_app = __import__("_app", globals(), locals(), ["."], 1)
|
|
136
|
+
# Because we're about to mutate our import, we pop it out of the imported
|
|
137
|
+
# modules map, so that any future imports do not receive our mutated version
|
|
138
|
+
sys.modules.pop("pypiserver._app", None)
|
|
139
|
+
_app.config = config
|
|
140
|
+
# Add a reference to our config on the Bottle app for easy access in testing
|
|
141
|
+
# and other contexts.
|
|
142
|
+
_app.app._pypiserver_config = config
|
|
143
|
+
return _app.app
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def setup_routes_from_config(app: Bottle, config: RunConfig) -> Bottle:
|
|
147
|
+
"""Set up additional routes supplied from the config."""
|
|
148
|
+
|
|
149
|
+
def _setup_health_endpoint(app, config):
|
|
150
|
+
if config.health_endpoint in [route.rule for route in app.routes]:
|
|
151
|
+
raise RuntimeError(
|
|
152
|
+
"Provided health endpoint overlaps with existing routes"
|
|
153
|
+
)
|
|
154
|
+
app.route(config.health_endpoint, "GET", lambda: "Ok")
|
|
155
|
+
|
|
156
|
+
_setup_health_endpoint(app, config)
|
|
157
|
+
return app
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
T = t.TypeVar("T")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def paste_app_factory(_global_config, **local_conf):
|
|
164
|
+
"""Parse a paste config and return an app.
|
|
165
|
+
|
|
166
|
+
The paste config is entirely strings, so we need to parse those
|
|
167
|
+
strings into values usable for the config, if they're present.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
def to_bool(val: t.Optional[str]) -> t.Optional[bool]:
|
|
171
|
+
"""Convert a string value, if provided, to a bool."""
|
|
172
|
+
return val if val is None else strtobool(val)
|
|
173
|
+
|
|
174
|
+
def to_int(val: t.Optional[str]) -> t.Optional[int]:
|
|
175
|
+
"""Convert a string value, if provided, to an int."""
|
|
176
|
+
return val if val is None else int(val)
|
|
177
|
+
|
|
178
|
+
def to_list(
|
|
179
|
+
val: t.Optional[str],
|
|
180
|
+
sep: str = " ",
|
|
181
|
+
transform: t.Callable[[str], T] = str.strip,
|
|
182
|
+
) -> t.Optional[t.List[T]]:
|
|
183
|
+
"""Convert a string value, if provided, to a list.
|
|
184
|
+
|
|
185
|
+
:param sep: the separator between items in the string representation
|
|
186
|
+
of the list
|
|
187
|
+
:param transform: an optional function to call on each string item of
|
|
188
|
+
the list
|
|
189
|
+
"""
|
|
190
|
+
if val is None:
|
|
191
|
+
return val
|
|
192
|
+
return list(filter(None, map(transform, val.split(sep))))
|
|
193
|
+
|
|
194
|
+
def _make_root(root: str) -> pathlib.Path:
|
|
195
|
+
"""Convert a specified string root into an absolute Path instance."""
|
|
196
|
+
return pathlib.Path(root.strip()).expanduser().resolve()
|
|
197
|
+
|
|
198
|
+
# A map of config keys we expect in the paste config to the appropriate
|
|
199
|
+
# function to parse the string config value. This map includes both
|
|
200
|
+
# current and legacy keys.
|
|
201
|
+
maps = {
|
|
202
|
+
"cache_control": to_int,
|
|
203
|
+
"roots": functools.partial(to_list, sep="\n", transform=_make_root),
|
|
204
|
+
# root is a deprecated argument for roots
|
|
205
|
+
"root": functools.partial(to_list, sep="\n", transform=_make_root),
|
|
206
|
+
"disable_fallback": to_bool,
|
|
207
|
+
# redirect_to_fallback is a deprecated argument for disable_fallback
|
|
208
|
+
"redirect_to_fallback": to_bool,
|
|
209
|
+
"overwrite": to_bool,
|
|
210
|
+
"authenticate": functools.partial(to_list, sep=" "),
|
|
211
|
+
# authenticated is a deprecated argument for authenticate
|
|
212
|
+
"authenticated": functools.partial(to_list, sep=" "),
|
|
213
|
+
"verbosity": to_int,
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
# First, convert values from strings to whatever types we need, or leave
|
|
217
|
+
# them as strings if there's no mapping function available for them.
|
|
218
|
+
mapped_conf = {k: maps.get(k, identity)(v) for k, v in local_conf.items()}
|
|
219
|
+
# Convert any legacy key/value pairs into their modern form.
|
|
220
|
+
updated_conf = backwards_compat_kwargs(mapped_conf)
|
|
221
|
+
|
|
222
|
+
return app(**updated_conf)
|
pypiserver/__main__.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
#! /usr/bin/env python3
|
|
2
|
+
"""Entrypoint for pypiserver."""
|
|
3
|
+
|
|
4
|
+
import enum
|
|
5
|
+
import functools as ft
|
|
6
|
+
import importlib
|
|
7
|
+
import logging
|
|
8
|
+
import sys
|
|
9
|
+
from collections.abc import Sequence
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import IO, Any
|
|
12
|
+
from wsgiref.simple_server import WSGIRequestHandler
|
|
13
|
+
|
|
14
|
+
from pypiserver.config import Config, UpdateConfig
|
|
15
|
+
|
|
16
|
+
log = logging.getLogger("pypiserver.main")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def init_logging(
|
|
20
|
+
level: int = logging.NOTSET,
|
|
21
|
+
frmt: str | None = None,
|
|
22
|
+
filename: str | Path | None = None,
|
|
23
|
+
stream: IO | None = sys.stderr,
|
|
24
|
+
logger: logging.Logger | None = None,
|
|
25
|
+
) -> None:
|
|
26
|
+
"""Configure the specified logger, or the root logger otherwise."""
|
|
27
|
+
logger = logger or logging.getLogger()
|
|
28
|
+
logger.setLevel(level)
|
|
29
|
+
|
|
30
|
+
formatter = logging.Formatter(frmt)
|
|
31
|
+
if len(logger.handlers) == 0 and stream is not None:
|
|
32
|
+
handler: logging.Handler = logging.StreamHandler(stream)
|
|
33
|
+
handler.setFormatter(formatter)
|
|
34
|
+
logger.addHandler(logging.StreamHandler(stream))
|
|
35
|
+
|
|
36
|
+
if filename:
|
|
37
|
+
handler = logging.FileHandler(filename)
|
|
38
|
+
handler.setFormatter(formatter)
|
|
39
|
+
logger.addHandler(handler)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class WsgiHandler(WSGIRequestHandler):
|
|
43
|
+
"""A simple request handler to configure logging."""
|
|
44
|
+
|
|
45
|
+
# The default `FixedHandler` that bottle's `WSGIRefServer` uses does not
|
|
46
|
+
# log in a particularly predictable or configurable way. We'll pass this
|
|
47
|
+
# in to use instead.
|
|
48
|
+
def address_string(self) -> str: # Prevent reverse DNS lookups please.
|
|
49
|
+
# This method copied directly from bottle's `FixedHandler` and
|
|
50
|
+
# maintained on the Chesterton's fence principle (i.e. I don't know
|
|
51
|
+
# why it's important, so I'm not going to get rid of it)
|
|
52
|
+
return self.client_address[0]
|
|
53
|
+
|
|
54
|
+
def log_message( # pylint: disable=redefined-builtin
|
|
55
|
+
self, format: str, *args: Any
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Log a message."""
|
|
58
|
+
# The log_message method on the `HttpRequestHandler` base class just
|
|
59
|
+
# writes directly to stderr. We'll use its same formatting, but pass
|
|
60
|
+
# it through the logger instead.
|
|
61
|
+
log.info(
|
|
62
|
+
"%s - - [%s] %s\n",
|
|
63
|
+
self.address_string(),
|
|
64
|
+
self.log_date_time_string(),
|
|
65
|
+
format % args,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class AutoServer(enum.Enum):
|
|
70
|
+
"""Expected servers that can be automaticlaly selected by bottle."""
|
|
71
|
+
|
|
72
|
+
Waitress = enum.auto()
|
|
73
|
+
PasteServer = enum.auto()
|
|
74
|
+
Twisted = enum.auto()
|
|
75
|
+
CherryPy = enum.auto()
|
|
76
|
+
Cheroot = enum.auto()
|
|
77
|
+
WsgiRef = enum.auto()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# Possible automatically selected servers. This MUST match the available
|
|
81
|
+
# auto servers in bottle.py
|
|
82
|
+
AUTO_SERVER_IMPORTS = (
|
|
83
|
+
(AutoServer.Waitress, "waitress"),
|
|
84
|
+
(AutoServer.PasteServer, "paste"),
|
|
85
|
+
(AutoServer.Twisted, "twisted.web"),
|
|
86
|
+
(AutoServer.CherryPy, "cherrypy"),
|
|
87
|
+
(AutoServer.Cheroot, "cheroot"),
|
|
88
|
+
# this should always be available because it's part of the stdlib
|
|
89
|
+
(AutoServer.WsgiRef, "wsgiref"),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _can_import(name: str) -> bool:
|
|
94
|
+
"""Attempt to import a module. Return a bool indicating success."""
|
|
95
|
+
try:
|
|
96
|
+
importlib.import_module(name)
|
|
97
|
+
return True
|
|
98
|
+
except ImportError:
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def guess_auto_server() -> AutoServer:
|
|
103
|
+
"""Guess which server bottle will use for the auto setting."""
|
|
104
|
+
# Return the first server that can be imported.
|
|
105
|
+
server = next(
|
|
106
|
+
(s for s, i in AUTO_SERVER_IMPORTS if _can_import(i)),
|
|
107
|
+
None,
|
|
108
|
+
)
|
|
109
|
+
if server is None:
|
|
110
|
+
raise RuntimeError(
|
|
111
|
+
"Unexpected error determining bottle auto server. There may be an "
|
|
112
|
+
"issue with this python environment. Please report this bug at "
|
|
113
|
+
"https://github.com/pypiserver/pypiserver/issues"
|
|
114
|
+
)
|
|
115
|
+
return server
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def main(argv: Sequence[str] | None = None) -> None:
|
|
119
|
+
"""Application entrypoint for pypiserver.
|
|
120
|
+
|
|
121
|
+
This function drives the application (as opposed to the library)
|
|
122
|
+
implementation of pypiserver. Usage from the commandline will result in
|
|
123
|
+
this function being called.
|
|
124
|
+
"""
|
|
125
|
+
# pylint: disable=import-outside-toplevel
|
|
126
|
+
import pypiserver # pylint: disable=redefined-outer-name
|
|
127
|
+
|
|
128
|
+
if argv is None:
|
|
129
|
+
# The first item in sys.argv is the name of the python file being
|
|
130
|
+
# executed, which we don't need
|
|
131
|
+
argv = sys.argv[1:]
|
|
132
|
+
|
|
133
|
+
config = Config.from_args(argv)
|
|
134
|
+
|
|
135
|
+
init_logging(
|
|
136
|
+
level=config.log_level,
|
|
137
|
+
filename=config.log_file,
|
|
138
|
+
frmt=config.log_frmt,
|
|
139
|
+
stream=config.log_stream,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Check to see if we were asked to run an update command instead of running
|
|
143
|
+
# the server
|
|
144
|
+
if isinstance(config, UpdateConfig):
|
|
145
|
+
from pypiserver.manage import update_all_packages
|
|
146
|
+
|
|
147
|
+
update_all_packages(
|
|
148
|
+
config.roots,
|
|
149
|
+
config.download_directory,
|
|
150
|
+
dry_run=not config.execute,
|
|
151
|
+
stable_only=config.allow_unstable,
|
|
152
|
+
ignorelist=config.ignorelist,
|
|
153
|
+
)
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
# Fixes #49:
|
|
157
|
+
# The gevent server adapter needs to patch some
|
|
158
|
+
# modules BEFORE importing bottle!
|
|
159
|
+
if config.server_method.startswith("gevent"):
|
|
160
|
+
import gevent.monkey # @UnresolvedImport
|
|
161
|
+
|
|
162
|
+
gevent.monkey.patch_all()
|
|
163
|
+
|
|
164
|
+
from pypiserver import bottle_wrapper as bottle
|
|
165
|
+
|
|
166
|
+
bottle.debug(config.verbosity > 1)
|
|
167
|
+
bottle._stderr = ft.partial( # pylint: disable=protected-access
|
|
168
|
+
_logwrite, logging.getLogger(bottle.__name__), logging.INFO
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Here `app` is a Bottle instance, which we pass to bottle.run() to run
|
|
172
|
+
# the server
|
|
173
|
+
app = pypiserver.app_from_config(config)
|
|
174
|
+
app = pypiserver.setup_routes_from_config(app, config)
|
|
175
|
+
|
|
176
|
+
if config.server_method == "gunicorn":
|
|
177
|
+
# When bottle runs gunicorn, gunicorn tries to pull its arguments from
|
|
178
|
+
# sys.argv. Because pypiserver's arguments don't match gunicorn's,
|
|
179
|
+
# this leads to errors.
|
|
180
|
+
# Gunicorn can be configured by using a `gunicorn.conf.py` config file
|
|
181
|
+
# or by specifying the `GUNICORN_CMD_ARGS` env var. See gunicorn
|
|
182
|
+
# docs for more info.
|
|
183
|
+
sys.argv = ["gunicorn"]
|
|
184
|
+
|
|
185
|
+
wsgi_kwargs = {"handler_class": WsgiHandler}
|
|
186
|
+
|
|
187
|
+
if config.server_method == "auto":
|
|
188
|
+
expected_server = guess_auto_server()
|
|
189
|
+
extra_kwargs = (
|
|
190
|
+
wsgi_kwargs if expected_server is AutoServer.WsgiRef else {}
|
|
191
|
+
)
|
|
192
|
+
log.debug(
|
|
193
|
+
"Server 'auto' selected. Expecting bottle to run '%s'. "
|
|
194
|
+
"Passing extra keyword args: %s",
|
|
195
|
+
expected_server.name,
|
|
196
|
+
extra_kwargs,
|
|
197
|
+
)
|
|
198
|
+
else:
|
|
199
|
+
extra_kwargs = wsgi_kwargs if config.server_method == "wsgiref" else {}
|
|
200
|
+
log.debug(
|
|
201
|
+
"Running bottle with selected server '%s'", config.server_method
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# We strip&check because bottle mount() will raise error if path has no segment.
|
|
205
|
+
if config.server_base_url.strip("/"):
|
|
206
|
+
main_app = bottle.Bottle()
|
|
207
|
+
main_app.mount(config.server_base_url, app)
|
|
208
|
+
else:
|
|
209
|
+
main_app = app
|
|
210
|
+
|
|
211
|
+
bottle.run(
|
|
212
|
+
app=main_app,
|
|
213
|
+
host=config.host,
|
|
214
|
+
port=config.port,
|
|
215
|
+
server=config.server_method,
|
|
216
|
+
**extra_kwargs,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _logwrite(logger: logging.Logger, level: int, msg: str | None) -> None:
|
|
221
|
+
if msg:
|
|
222
|
+
line_endings = ["\r\n", "\n\r", "\n"]
|
|
223
|
+
for le in line_endings: # pylint: disable=invalid-name
|
|
224
|
+
if msg.endswith(le):
|
|
225
|
+
msg = msg[: -len(le)]
|
|
226
|
+
if msg:
|
|
227
|
+
logger.log(level, msg)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
if __name__ == "__main__":
|
|
231
|
+
main()
|