Compare commits

...

28 commits

Author SHA1 Message Date
Alexandre Aubin
709585be83 Update changelog for 12.0.2 2024-08-31 20:13:48 +02:00
Alexandre Aubin
6f09185a70
Merge pull request #339 from selfhoster1312/redirect-referer
portal-api: login/logout redirect to referer when param referer_redirect is set
2024-08-20 18:52:03 +02:00
Alexandre Aubin
0d7a143c01
Merge pull request #340 from selfhoster1312/bypass-csrf-login
portal-api: Bypass CSRF protection for login route
2024-08-20 16:27:23 +02:00
Alexandre Aubin
9cc786e83c Update changelog for 12.0.1 testing 2024-07-26 21:57:48 +02:00
Alexandre Aubin
bb0a9bd767 Merge remote-tracking branch 'origin/dev' into bookworm 2024-07-17 18:57:04 +02:00
Alexandre Aubin
cfb840c5cc perf: in call_async_output: only wait for 0.1 sec, should speed up things significantly for stuff that calls a lot of hooks... 2023-11-27 15:56:52 +01:00
Alexandre Aubin
976aac0d05 Do not log about loading auth module, it creates tricky issue when manually launching yunohost APIs to debug them 2023-11-27 15:55:45 +01:00
Alexandre Aubin
d53dfc2997 debug: print stacktrace to stderr upon 500 errors, because otherwise APIs are hell to debug ~_~ 2023-11-13 15:36:29 +01:00
axolotle
924fd7825e cors: fix some http response error not being catched by cors decorator 2023-11-08 19:11:18 +01:00
Alexandre Aubin
20d3b82340 fix test ... apparently the API now returns 405 when no action is specified, I guess that's okay 2023-09-27 20:23:11 +02:00
Alexandre Aubin
f562a9b333 fix old logger mechanism remants 2023-09-27 20:15:57 +02:00
Alexandre Aubin
bd9736efc1 quality: we're in python 3.11 bruh³ 2023-09-27 20:11:16 +02:00
Alexandre Aubin
8c28a573e2 quality: we're in python 3.11 bruh² 2023-09-27 20:09:48 +02:00
Alexandre Aubin
fc1eef2d92 quality: we're in python 3.11 bruh 2023-09-27 20:08:38 +02:00
Alexandre Aubin
37331cb1d6 quality: fix test/conftest.py, there's no ActionFilter anymore 2023-09-27 20:06:34 +02:00
Alexandre Aubin
7210b66fba quality: update tox.ini, bookworm has python 3.11 2023-09-27 20:04:15 +02:00
Alexandre Aubin
2696e811ce quality: make linter gods happy 2023-09-27 20:02:41 +02:00
Alexandre Aubin
75f522be56
Merge pull request #341 from YunoHost/portal-api
Tweaks and fixes for new portal API / ssowat refactoring
2023-09-27 18:55:29 +02:00
Alexandre Aubin
8016725604
Merge pull request #337 from YunoHost/logging-is-a-mess
Moulinette logging is an unecessarily complex mess, episode 57682
2023-09-27 17:45:10 +02:00
selfhoster1312
7daa50459a Bypass CSRF protection for the /yunohost/portalapi/login route
Allowing login from simple HTML form
Also allow to pass username/password as two params instead of a combined "credentials"
2023-08-14 22:07:49 +02:00
selfhoster1312
e24d56d5f1 /yunohost/sso/log{in,out} 303 to referer when GET/POST param referer_redirect is set 2023-08-14 22:00:28 +02:00
Alexandre Aubin
a6c7e55d1d api: fix authentication failure not deleting expired cookies 2023-07-29 19:10:47 +02:00
Alexandre Aubin
328107c946 api: Add a proper mechanism to allow specific, configurable CORS origins 2023-07-29 19:09:52 +02:00
axolotle
4104704a87 allow json requests 2023-07-28 17:17:51 +02:00
Tagada
3ad73355da Merge branch 'dev' into bookworm 2023-07-20 16:10:46 +02:00
Alexandre Aubin
0f056d66d7 Moulinette logging is an unecessarily complex mess, episode 57682 2023-07-18 00:20:24 +02:00
Alexandre Aubin
34a2a66027 Fix boring login API expecting a weird form/multiparam thing instead of classic JSON for credentials ... 2023-07-14 19:09:00 +02:00
Alexandre Aubin
e8f10ce54e Update changelog for 12.0.0 2023-05-04 22:38:06 +02:00
13 changed files with 116 additions and 151 deletions

View file

@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
python-version: [3.9] python-version: [3.11]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
@ -26,13 +26,13 @@ jobs:
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install tox tox-gh-actions pip install tox tox-gh-actions
- name: Test with tox - name: Test with tox
run: tox -e py39-pytest run: tox -e py311-pytest
invalidcode: invalidcode:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
python-version: [3.9] python-version: [3.11]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
@ -44,6 +44,6 @@ jobs:
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install tox tox-gh-actions pip install tox tox-gh-actions
- name: Linter - name: Linter
run: tox -e py39-invalidcode run: tox -e py311-invalidcode
- name: Mypy - name: Mypy
run: tox -e py39-mypy run: tox -e py311-mypy

17
debian/changelog vendored
View file

@ -1,3 +1,20 @@
moulinette (12.0.2) testing; urgency=low
- portal-api: Bypass CSRF protection for login route ([#340](http://github.com/YunoHost/moulinette/pull/340))
- portal-api: login/logout redirect to referer when param referer_redirect is set ([#339](http://github.com/YunoHost/moulinette/pull/339))
Thanks to all contributors <3 ! (selfhoster1312)
-- Alexandre Aubin <alex.aubin@mailoo.org> Sat, 31 Aug 2024 20:12:43 +0200
moulinette (12.0.1) testing; urgency=low
- Tweaks and fixes for new portal API / ssowat refactoring ([#341](https://github.com/YunoHost/moulinette/pull/341))
- Simplify logging : unecessary messages + obscure concept of "action id" ([#337](https://github.com/YunoHost/moulinette/pull/337))
- Misc tweaks to adapt code and tests to Python 3.11
-- Alexandre Aubin <alex.aubin@mailoo.org> Thu, 04 May 2023 20:30:19 +0200
moulinette (11.2.1) stable; urgency=low moulinette (11.2.1) stable; urgency=low
- repo chores: various black enhancements - repo chores: various black enhancements

View file

@ -53,7 +53,7 @@ class Moulinette:
# Easy access to interfaces # Easy access to interfaces
def api(host="localhost", port=80, routes={}, actionsmap=None, locales_dir=None): def api(host="localhost", port=80, routes={}, actionsmap=None, locales_dir=None, allowed_cors_origins=[]):
"""Web server (API) interface """Web server (API) interface
Run a HTTP server with the moulinette for an API usage. Run a HTTP server with the moulinette for an API usage.
@ -73,6 +73,7 @@ def api(host="localhost", port=80, routes={}, actionsmap=None, locales_dir=None)
Api( Api(
routes=routes, routes=routes,
actionsmap=actionsmap, actionsmap=actionsmap,
allowed_cors_origins=allowed_cors_origins,
).run(host, port) ).run(host, port)
except MoulinetteError as e: except MoulinetteError as e:
import logging import logging

View file

@ -19,7 +19,6 @@ from moulinette.core import (
MoulinetteValidationError, MoulinetteValidationError,
) )
from moulinette.interfaces import BaseActionsMapParser from moulinette.interfaces import BaseActionsMapParser
from moulinette.utils.log import start_action_logging
from moulinette.utils.filesystem import read_yaml from moulinette.utils.filesystem import read_yaml
logger = logging.getLogger("moulinette.actionsmap") logger = logging.getLogger("moulinette.actionsmap")
@ -392,8 +391,6 @@ class ActionsMap:
self.from_cache = False self.from_cache = False
logger.debug("loading actions map")
actionsmap_yml_dir = os.path.dirname(actionsmap_yml) actionsmap_yml_dir = os.path.dirname(actionsmap_yml)
actionsmap_yml_file = os.path.basename(actionsmap_yml) actionsmap_yml_file = os.path.basename(actionsmap_yml)
actionsmap_yml_stat = os.stat(actionsmap_yml) actionsmap_yml_stat = os.stat(actionsmap_yml)
@ -461,7 +458,7 @@ class ActionsMap:
# Load and initialize the authenticator module # Load and initialize the authenticator module
auth_module = f"{self.namespace}.authenticators.{auth_method}" auth_module = f"{self.namespace}.authenticators.{auth_method}"
logger.debug(f"Loading auth module {auth_module}") #logger.debug(f"Loading auth module {auth_module}")
try: try:
mod = import_module(auth_module) mod = import_module(auth_module)
except ImportError as e: except ImportError as e:
@ -556,17 +553,7 @@ class ActionsMap:
logger.exception(error_message) logger.exception(error_message)
raise MoulinetteError(error_message, raw_msg=True) raise MoulinetteError(error_message, raw_msg=True)
else: else:
log_id = start_action_logging() logger.debug("processing action '%s'", full_action_name)
if logger.isEnabledFor(logging.DEBUG):
# Log arguments in debug mode only for safety reasons
logger.debug(
"processing action [%s]: %s with args=%s",
log_id,
full_action_name,
arguments,
)
else:
logger.debug("processing action [%s]: %s", log_id, full_action_name)
# Load translation and process the action # Load translation and process the action
start = time() start = time()
@ -574,7 +561,7 @@ class ActionsMap:
return func(**arguments) return func(**arguments)
finally: finally:
stop = time() stop = time()
logger.debug("action [%s] executed in %.3fs", log_id, stop - start) logger.debug("action executed in %.3fs", stop - start)
# Private methods # Private methods
@ -592,9 +579,6 @@ class ActionsMap:
""" """
logger.debug("building parser...")
start = time()
interface_type = top_parser.interface interface_type = top_parser.interface
# If loading from cache, extra were already checked when cache was # If loading from cache, extra were already checked when cache was
@ -711,5 +695,4 @@ class ActionsMap:
else: else:
action_parser.want_to_take_lock = True action_parser.want_to_take_lock = True
logger.debug("building parser took %.3fs", time() - start)
return top_parser return top_parser

View file

@ -32,8 +32,7 @@ class BaseActionsMapParser:
""" """
def __init__(self, parent=None, **kwargs): def __init__(self, parent=None, **kwargs):
if not parent: pass
logger.debug("initializing base actions map parser for %s", self.interface)
# Virtual properties # Virtual properties
# Each parser classes must implement these properties. # Each parser classes must implement these properties.

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import sys
import re import re
import errno import errno
import logging import logging
@ -13,7 +13,7 @@ from gevent import sleep
from gevent.queue import Queue from gevent.queue import Queue
from geventwebsocket import WebSocketError from geventwebsocket import WebSocketError
from bottle import request, response, Bottle, HTTPResponse, FileUpload from bottle import redirect, request, response, Bottle, HTTPResponse, FileUpload
from bottle import abort from bottle import abort
from moulinette import m18n, Moulinette from moulinette import m18n, Moulinette
@ -30,7 +30,7 @@ from moulinette.interfaces import (
) )
from moulinette.utils import log from moulinette.utils import log
logger = log.getLogger("moulinette.interface.api") logger = logging.getLogger("moulinette.interface.api")
# API helpers ---------------------------------------------------------- # API helpers ----------------------------------------------------------
@ -269,13 +269,14 @@ class _ActionsMapPlugin:
name="login", name="login",
method="POST", method="POST",
callback=self.login, callback=self.login,
skip=["actionsmap"], skip=[filter_csrf, "actionsmap"],
) )
app.route( app.route(
"/logout", "/logout",
name="logout", name="logout",
method="GET", method="GET",
callback=self.logout, callback=self.logout,
# No need to bypass CSRF here because filter allows GET requests
skip=["actionsmap"], skip=["actionsmap"],
) )
@ -309,6 +310,9 @@ class _ActionsMapPlugin:
return value return value
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
if request.get_header("Content-Type") == "application/json":
return callback((request.method, context.rule), request.json)
params = kwargs params = kwargs
# Format boolean params # Format boolean params
for a in args: for a in args:
@ -350,11 +354,21 @@ class _ActionsMapPlugin:
""" """
if "credentials" not in request.params: if request.get_header("Content-Type") == "application/json":
if "credentials" not in request.json:
raise HTTPResponse("Missing credentials parameter", 400) raise HTTPResponse("Missing credentials parameter", 400)
credentials = request.json["credentials"]
profile = request.json.get("profile", self.actionsmap.default_authentication)
else:
if "credentials" in request.params:
credentials = request.params["credentials"] credentials = request.params["credentials"]
elif "username" in request.params and "password" in request.params:
credentials = request.params["username"] + ":" + request.params["password"]
else:
raise HTTPResponse("Missing credentials parameter", 400)
profile = request.params.get("profile", self.actionsmap.default_authentication) profile = request.params.get("profile", self.actionsmap.default_authentication)
authenticator = self.actionsmap.get_authenticator(profile) authenticator = self.actionsmap.get_authenticator(profile)
try: try:
@ -367,6 +381,10 @@ class _ActionsMapPlugin:
raise HTTPResponse(e.strerror, 401) raise HTTPResponse(e.strerror, 401)
else: else:
authenticator.set_session_cookie(auth_infos) authenticator.set_session_cookie(auth_infos)
referer = request.get_header("Referer")
if "referer_redirect" in request.params and referer:
redirect(referer)
else:
return m18n.g("logged_in") return m18n.g("logged_in")
# This is called before each time a route is going to be processed # This is called before each time a route is going to be processed
@ -374,6 +392,7 @@ class _ActionsMapPlugin:
try: try:
session_infos = authenticator.get_session_cookie() session_infos = authenticator.get_session_cookie()
except Exception: except Exception:
authenticator.delete_session_cookie()
msg = m18n.g("authentication_required") msg = m18n.g("authentication_required")
raise HTTPResponse(msg, 401) raise HTTPResponse(msg, 401)
@ -390,7 +409,11 @@ class _ActionsMapPlugin:
else: else:
# Delete cookie and clean the session # Delete cookie and clean the session
authenticator.delete_session_cookie() authenticator.delete_session_cookie()
return m18n.g("logged_out") referer = request.get_header("Referer")
if "referer_redirect" in request.params and referer:
redirect(referer)
else:
return m18n.g("logged_in")
def messages(self): def messages(self):
"""Listen to the messages WebSocket stream """Listen to the messages WebSocket stream
@ -457,6 +480,7 @@ class _ActionsMapPlugin:
tb = traceback.format_exc() tb = traceback.format_exc()
logs = {"route": _route, "arguments": arguments, "traceback": tb} logs = {"route": _route, "arguments": arguments, "traceback": tb}
print(tb, file=sys.stderr)
return HTTPResponse(json_encode(logs), 500) return HTTPResponse(json_encode(logs), 500)
else: else:
return format_for_response(ret) return format_for_response(ret)
@ -702,9 +726,11 @@ class Interface:
type = "api" type = "api"
def __init__(self, routes={}, actionsmap=None): def __init__(self, routes={}, actionsmap=None, allowed_cors_origins=[]):
actionsmap = ActionsMap(actionsmap, ActionsMapParser()) actionsmap = ActionsMap(actionsmap, ActionsMapParser())
self.allowed_cors_origins = allowed_cors_origins
# Attempt to retrieve log queues from an APIQueueHandler # Attempt to retrieve log queues from an APIQueueHandler
handler = log.getHandlersByClass(APIQueueHandler, limit=1) handler = log.getHandlersByClass(APIQueueHandler, limit=1)
if handler: if handler:
@ -714,11 +740,22 @@ class Interface:
# TODO: Return OK to 'OPTIONS' xhr requests (l173) # TODO: Return OK to 'OPTIONS' xhr requests (l173)
app = Bottle(autojson=True) app = Bottle(autojson=True)
# Wrapper which sets proper header def cors(callback):
def apiheader(callback):
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
response.set_header("Access-Control-Allow-Origin", "*") try:
return callback(*args, **kwargs) r = callback(*args, **kwargs)
except HTTPResponse as e:
r = e
origin = request.headers.environ.get("HTTP_ORIGIN", "")
if origin and origin in self.allowed_cors_origins:
resp = r if isinstance(r, HTTPResponse) else response
resp.headers['Access-Control-Allow-Origin'] = origin
resp.headers['Access-Control-Allow-Methods'] = 'GET, HEAD, POST, PUT, OPTIONS, DELETE'
resp.headers['Access-Control-Allow-Headers'] = 'Origin, Accept, Content-Type, X-Requested-With, X-CSRF-Token'
resp.headers['Access-Control-Allow-Credentials'] = 'true'
return r
return wrapper return wrapper
@ -727,7 +764,7 @@ class Interface:
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
try: try:
locale = request.params.pop("locale") locale = request.params.pop("locale")
except KeyError: except (KeyError, ValueError):
locale = m18n.default_locale locale = m18n.default_locale
m18n.set_locale(locale) m18n.set_locale(locale)
return callback(*args, **kwargs) return callback(*args, **kwargs)
@ -736,7 +773,7 @@ class Interface:
# Install plugins # Install plugins
app.install(filter_csrf) app.install(filter_csrf)
app.install(apiheader) app.install(cors)
app.install(api18n) app.install(api18n)
actionsmapplugin = _ActionsMapPlugin(actionsmap, log_queues) actionsmapplugin = _ActionsMapPlugin(actionsmap, log_queues)
app.install(actionsmapplugin) app.install(actionsmapplugin)
@ -745,6 +782,12 @@ class Interface:
self.display = actionsmapplugin.display self.display = actionsmapplugin.display
self.prompt = actionsmapplugin.prompt self.prompt = actionsmapplugin.prompt
def handle_options():
return HTTPResponse("", 204)
app.route('/<:re:.*>', method="OPTIONS",
callback=handle_options, skip=["actionsmap"])
# Append additional routes # Append additional routes
# TODO: Add optional authentication to those routes? # TODO: Add optional authentication to those routes?
for (m, p), c in routes.items(): for (m, p), c in routes.items():

View file

@ -50,7 +50,7 @@ def monkey_get_action_name(argument):
argparse._get_action_name = monkey_get_action_name argparse._get_action_name = monkey_get_action_name
logger = log.getLogger("moulinette.cli") logger = logging.getLogger("moulinette.cli")
# CLI helpers ---------------------------------------------------------- # CLI helpers ----------------------------------------------------------
@ -234,28 +234,26 @@ class TTYHandler(logging.StreamHandler):
log.CRITICAL: "red", log.CRITICAL: "red",
} }
def __init__(self, message_key="fmessage"): def __init__(self, message_key="message_with_color"):
logging.StreamHandler.__init__(self) logging.StreamHandler.__init__(self)
self.message_key = message_key self.message_key = message_key
def format(self, record): def format(self, record):
"""Enhance message with level and colors if supported.""" """Enhance message with level and colors if supported."""
msg = record.getMessage() msg = record.getMessage()
level = record.levelname
level_with_color = level
if self.supports_color(): if self.supports_color():
level = "" if self.level > log.DEBUG and record.levelname in ["SUCCESS", "WARNING", "ERROR", "INFO"]:
if self.level <= log.DEBUG: level = m18n.g(record.levelname.lower())
# add level name before message
level = "%s " % record.levelname
elif record.levelname in ["SUCCESS", "WARNING", "ERROR", "INFO"]:
# add translated level name before message
level = "%s " % m18n.g(record.levelname.lower())
color = self.LEVELS_COLOR.get(record.levelno, "white") color = self.LEVELS_COLOR.get(record.levelno, "white")
msg = "{}{}{}{}".format(colors_codes[color], level, END_CLI_COLOR, msg) level_with_color = f"{colors_codes[color]}{level}{END_CLI_COLOR}"
if self.level == log.DEBUG:
level_with_color = level_with_color + " " * max(0, 7 - len(level))
if self.formatter: if self.formatter:
# use user-defined formatter record.__dict__["level_with_color"] = level_with_color
record.__dict__[self.message_key] = msg
return self.formatter.format(record) return self.formatter.format(record)
return msg return level_with_color + " " + msg
def emit(self, record): def emit(self, record):
# set proper stream first # set proper stream first

View file

@ -6,7 +6,6 @@ from logging import (
addLevelName, addLevelName,
setLoggerClass, setLoggerClass,
Logger, Logger,
getLogger,
NOTSET, # noqa NOTSET, # noqa
DEBUG, DEBUG,
INFO, INFO,
@ -109,88 +108,14 @@ class MoulinetteLogger(Logger):
f = currentframe() f = currentframe()
if f is not None: if f is not None:
f = f.f_back f = f.f_back
rv = "(unknown file)", 0, "(unknown function)" rv = "(unknown file)", 0, "(unknown function)", None
while hasattr(f, "f_code"): while hasattr(f, "f_code"):
co = f.f_code co = f.f_code
filename = os.path.normcase(co.co_filename) filename = os.path.normcase(co.co_filename)
if filename == _srcfile or filename == __file__: if filename == _srcfile or filename == __file__:
f = f.f_back f = f.f_back
continue continue
rv = (co.co_filename, f.f_lineno, co.co_name) rv = (co.co_filename, f.f_lineno, co.co_name, None)
break break
return rv return rv
def _log(self, *args, **kwargs):
"""Append action_id if available to the extra."""
if self.action_id is not None:
extra = kwargs.get("extra", {})
if "action_id" not in extra:
# FIXME: Get real action_id instead of logger/current one
extra["action_id"] = _get_action_id()
kwargs["extra"] = extra
return super()._log(*args, **kwargs)
# Action logging -------------------------------------------------------
pid = os.getpid()
action_id = 0
def _get_action_id():
return "%d.%d" % (pid, action_id)
def start_action_logging():
"""Configure logging for a new action
Returns:
The new action id
"""
global action_id
action_id += 1
return _get_action_id()
def getActionLogger(name=None, logger=None, action_id=None):
"""Get the logger adapter for an action
Return a logger for the specified name - or use given logger - and
optionally for a given action id, retrieving it if necessary.
Either a name or a logger must be specified.
"""
if not name and not logger:
raise ValueError("Either a name or a logger must be specified")
logger = logger or getLogger(name)
logger.action_id = action_id if action_id else _get_action_id()
return logger
class ActionFilter:
"""Extend log record for an optionnal action
Filter a given record and look for an `action_id` key. If it is not found
and `strict` is True, the record will not be logged. Otherwise, the key
specified by `message_key` will be added to the record, containing the
message formatted for the action or just the original one.
"""
def __init__(self, message_key="fmessage", strict=False):
self.message_key = message_key
self.strict = strict
def filter(self, record):
msg = record.getMessage()
action_id = record.__dict__.get("action_id", None)
if action_id is not None:
msg = "[{:s}] {:s}".format(action_id, msg)
elif self.strict:
return False
record.__dict__[self.message_key] = msg
return True

View file

@ -82,7 +82,7 @@ def call_async_output(args, callback, **kwargs):
while p.poll() is None: while p.poll() is None:
while True: while True:
try: try:
callback, message = log_queue.get(True, 1) callback, message = log_queue.get(True, 0.1)
except queue.Empty: except queue.Empty:
break break

View file

@ -60,7 +60,7 @@ setup(
license="AGPL", license="AGPL",
packages=find_packages(exclude=["test"]), packages=find_packages(exclude=["test"]),
data_files=[("/usr/share/moulinette/locales", locale_files)], data_files=[("/usr/share/moulinette/locales", locale_files)],
python_requires=">=3.7.0,<3.10", python_requires=">=3.11.0,<3.12",
install_requires=install_deps, install_requires=install_deps,
tests_require=test_deps, tests_require=test_deps,
extras_require=extras, extras_require=extras,

View file

@ -46,7 +46,7 @@ def logging_configuration(moulinette):
"format": "%(asctime)-15s %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s" # noqa "format": "%(asctime)-15s %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s" # noqa
}, },
}, },
"filters": {"action": {"()": "moulinette.utils.log.ActionFilter"}}, "filters": {},
"handlers": { "handlers": {
"api": { "api": {
"level": level, "level": level,

View file

@ -180,7 +180,7 @@ class TestAuthAPI:
def test_request_arg_without_action(self, moulinette_webapi, caplog, mocker): def test_request_arg_without_action(self, moulinette_webapi, caplog, mocker):
self.login(moulinette_webapi) self.login(moulinette_webapi)
moulinette_webapi.get("/test-auth", status=404) moulinette_webapi.get("/test-auth", status=405)
class TestAuthCLI: class TestAuthCLI:

21
tox.ini
View file

@ -1,6 +1,6 @@
[tox] [tox]
envlist = envlist =
py{37,39}-{pytest,lint,invalidcode,mypy} py311-{pytest,lint,invalidcode,mypy}
format format
format-check format-check
docs docs
@ -11,20 +11,19 @@ usedevelop = True
passenv = * passenv = *
extras = tests extras = tests
deps = deps =
py{37,39}-pytest: .[tests] py311-pytest: .[tests]
py{37,39}-lint: flake8 py311-lint: flake8
py{37,39}-invalidcode: flake8 py311-invalidcode: flake8
py{37,39}-mypy: mypy >= 0.761 py311-mypy: mypy >= 0.761
commands = commands =
py{37,39}-pytest: pytest {posargs} -c pytest.ini py311-pytest: pytest {posargs} -c pytest.ini
py{37,39}-lint: flake8 moulinette test py311-lint: flake8 moulinette test
py{37,39}-invalidcode: flake8 moulinette test --select F py311-invalidcode: flake8 moulinette test --select F
py{37,39}-mypy: mypy --ignore-missing-imports --install-types --non-interactive moulinette/ py311-mypy: mypy --ignore-missing-imports --install-types --non-interactive moulinette/
[gh-actions] [gh-actions]
python = python =
3.7: py37 3.11: py311
3.9: py39
[testenv:format] [testenv:format]
basepython = python3 basepython = python3