Migrate to rust based python tooling, use python 3.14 by default

master
alufers 5 days ago
parent abea8a1139
commit 594897aa7e

@ -1,3 +0,0 @@
[flake8]
max-line-length = 120
extend-ignore = E203,E501

@ -1,36 +1,10 @@
# This workflow will install Python dependencies, run tests and lint with a single version of Python
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
name: Prek checks
name: Python
on:
push:
branches: [master]
pull_request:
branches: [master]
permissions:
contents: read
on: [push, pull_request]
jobs:
build:
prek:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
version: "latest"
- name: Install dependencies
run: |
uv sync
- name: Run Python tests
run: |
uv run pytest --cov
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
- uses: actions/checkout@v6
- uses: j178/prek-action@v2

@ -18,12 +18,12 @@ jobs:
steps:
# Checkout project repository
- uses: actions/checkout@v4
- name: Set up Python 3.12
- name: Set up Python 3.14
uses: actions/setup-python@v5
with:
python-version: "3.12"
python-version: "3.14"
- name: Install uv
uses: astral-sh/setup-uv@v4
uses: astral-sh/setup-uv@v6
with:
version: "latest"
- name: Install dependencies
@ -31,7 +31,7 @@ jobs:
uv sync
- name: Run Python lint checks
run: |
uv run pre-commit run --all-files
uv run prek run --all-files
- name: Run Python tests
run: |
uv run pytest --cov

@ -1,4 +0,0 @@
[mypy]
[mypy-json_stream.*]
ignore_missing_imports = True

@ -3,7 +3,7 @@
exclude: ^(api_protobuf/.*|docs/protobuf_docs\.md|dhaul_openapi/messages/.*)$
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
rev: v6.0.0
hooks:
- id: check-yaml
- id: end-of-file-fixer
@ -12,53 +12,20 @@ repos:
exclude: ^testdata/.*
- id: check-json
- id: detect-private-key
- id: fix-encoding-pragma
- id: check-merge-conflict
- id: check-added-large-files
- repo: https://github.com/psf/black
rev: 24.10.0
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.14
hooks:
- id: black
args: [--config=pyproject.toml]
- repo: https://github.com/PyCQA/isort
rev: 5.13.2
hooks:
- id: isort
- repo: https://github.com/pre-commit/mirrors-mypy
rev: "v1.13.0"
hooks:
- id: mypy
- repo: https://github.com/PyCQA/docformatter
rev: eb1df347edd128b30cd3368dddc3aa65edcfac38
# TODO: Switch back to upstream docformatter
# after https://github.com/PyCQA/docformatter/issues/289 is fixed
hooks:
- id: docformatter
args:
- --in-place
- --config=pyproject.toml
- repo: https://github.com/PyCQA/autoflake
rev: v2.3.1
hooks:
- id: autoflake
- repo: https://github.com/pycqa/flake8
rev: 7.1.1
hooks:
- id: flake8
entry: flake8
- repo: https://github.com/netromdk/vermin
rev: v1.6.0
hooks:
- id: vermin
# specify your target version here, OR in a Vermin config file as usual:
args: ["-t=3.9-", "--violations"]
# (if your target is specified in a Vermin config, you may omit the 'args' entry entirely)
- id: ruff
args: [--fix]
- id: ruff-format
- repo: https://github.com/adrienverge/yamllint.git
rev: v1.35.1
rev: v1.38.0
hooks:
- id: yamllint
- repo: https://github.com/igorshubovych/markdownlint-cli
rev: v0.42.0
rev: v0.48.0
hooks:
- id: markdownlint-fix
- id: markdownlint

@ -1,4 +1,4 @@
FROM python:3.12-slim-bookworm AS builder
FROM python:3.14-slim-bookworm AS builder
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
ENV UV_HTTP_TIMEOUT=100 \
UV_NO_CACHE=1 \
@ -7,9 +7,9 @@ WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN uv sync --no-dev --frozen --no-install-project
COPY . .
RUN uv sync --no-dev --frozen
RUN uv sync --no-dev --frozen --no-editable
FROM python:3.12-slim-bookworm AS final
FROM python:3.14-slim-bookworm AS final
ENV PYTHONFAULTHANDLER=1 \
PYTHONHASHSEED=random \
PYTHONUNBUFFERED=1

@ -123,14 +123,14 @@ To create a specification by inspecting HTTP traffic you will need to:
See the [examples](./example_outputs/). You will find a generated schema there and an html file with the generated documentation (via [redoc-cli](https://www.npmjs.com/package/redoc-cli)).
See the generated html file [here](https://raw.githack.com/alufers/mitmproxy2swagger/master/example_outputs/lisek-static.html).
See the [generated html file](https://raw.githack.com/alufers/mitmproxy2swagger/master/example_outputs/lisek-static.html).
## Development and contributing
This project uses:
- [uv](https://docs.astral.sh/uv/) for dependency management
- [pre-commit](https://pre-commit.com/) for code formatting and linting
- [prek](https://github.com/j178/prek) for code formatting and linting
- [pytest](https://docs.pytest.org/en/stable/) for unit testing
To install the dependencies:
@ -142,13 +142,13 @@ uv sync
Run linters:
```bash
pre-commit run --all-files
uv run prek run --all-files
```
Install pre-commit hooks:
Install prek hooks:
```bash
pre-commit install
uv run prek install
```
Run tests:

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
import sys
ANSI_RGB = "\033[38;2;{};{};{}m"
@ -64,5 +63,5 @@ def print_progress_bar(progress=0.0):
progress_bar_contents += " "
progress_bar_contents += ANSI_RESET
sys.stdout.write("[{}] {:.1f}%".format(progress_bar_contents, progress * 100))
sys.stdout.write(f"[{progress_bar_contents}] {progress * 100:.1f}%")
sys.stdout.flush()

@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
import os
from base64 import b64decode
from typing import Iterator, Union
from collections.abc import Iterator
import json_stream
@ -44,7 +43,7 @@ class HarFlowWrapper:
def get_url(self):
return self.flow["request"]["url"]
def get_matching_url(self, prefix) -> Union[str, None]:
def get_matching_url(self, prefix) -> str | None:
"""Get the requests URL if the prefix matches the URL, None otherwise."""
if self.flow["request"]["url"].startswith(prefix):
return self.flow["request"]["url"]
@ -112,7 +111,7 @@ class HarCaptureReader:
def captured_requests(self) -> Iterator[HarFlowWrapper]:
har_file_size = os.path.getsize(self.file_path)
with open(self.file_path, "r", encoding="utf-8") as f:
with open(self.file_path, encoding="utf-8") as f:
data = json_stream.load(f)
for entry in data["log"]["entries"].persistent():
if self.progress_callback:

@ -1,6 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Converts a mitmproxy dump file to a swagger schema."""
import argparse
import json
import os
@ -8,7 +8,9 @@ import re
import sys
import traceback
import urllib
from typing import Any, Optional, Sequence, Union
import urllib.parse
from collections.abc import Sequence
from typing import Any
import msgpack
import ruamel.yaml
@ -56,7 +58,7 @@ def detect_input_format(file_path):
return MitmproxyCaptureReader(file_path, progress_callback)
def main(override_args: Optional[Sequence[str]] = None):
def main(override_args: Sequence[str] | None = None):
parser = argparse.ArgumentParser(
description="Converts a mitmproxy dump file or HAR to a swagger schema."
)
@ -115,7 +117,7 @@ def main(override_args: Optional[Sequence[str]] = None):
yaml = ruamel.yaml.YAML()
capture_reader: Union[MitmproxyCaptureReader, HarCaptureReader]
capture_reader: MitmproxyCaptureReader | HarCaptureReader
if args.format == "flow" or args.format == "mitmproxy":
capture_reader = MitmproxyCaptureReader(args.input, progress_callback)
elif args.format == "har":
@ -130,7 +132,7 @@ def main(override_args: Optional[Sequence[str]] = None):
base_dir = os.getcwd()
relative_path = args.output
abs_path = os.path.join(base_dir, relative_path)
with open(abs_path, "r") as f:
with open(abs_path) as f:
swagger = yaml.load(f)
except FileNotFoundError:
print("No existing swagger file found. Creating new one.")

@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
import os
import typing
from typing import Iterator
from collections.abc import Iterator
from urllib.parse import urlparse
from mitmproxy import http
@ -53,7 +51,7 @@ class MitmproxyFlowWrapper:
def get_url(self) -> str:
return self.flow.request.url
def get_matching_url(self, prefix) -> typing.Union[str, None]:
def get_matching_url(self, prefix) -> str | None:
"""Get the requests URL if the prefix matches the URL, None otherwise.
This takes into account a quirk of mitmproxy where it sometimes
@ -82,8 +80,8 @@ class MitmproxyFlowWrapper:
def get_method(self) -> str:
return self.flow.request.method
def get_request_headers(self) -> dict[str, typing.List[str]]:
headers: dict[str, typing.List[str]] = {}
def get_request_headers(self) -> dict[str, list[str]]:
headers: dict[str, list[str]] = {}
for k, v in self.flow.request.headers.items(multi=True):
# create list on key if it does not exist
headers[k] = headers.get(k, [])
@ -93,22 +91,27 @@ class MitmproxyFlowWrapper:
def get_request_body(self):
return self.flow.request.content
@property
def _response(self) -> http.Response:
assert self.flow.response is not None
return self.flow.response
def get_response_status_code(self):
return self.flow.response.status_code
return self._response.status_code
def get_response_reason(self):
return self.flow.response.reason
return self._response.reason
def get_response_headers(self):
headers = {}
for k, v in self.flow.response.headers.items(multi=True):
for k, v in self._response.headers.items(multi=True):
# create list on key if it does not exist
headers[k] = headers.get(k, [])
headers[k].append(v)
return headers
def get_response_body(self):
return self.flow.response.content
return self._response.content
class MitmproxyCaptureReader:
@ -126,9 +129,7 @@ class MitmproxyCaptureReader:
self.progress_callback(logfile.tell() / logfile_size)
if isinstance(f, http.HTTPFlow):
if f.response is None:
print(
"[warn] flow without response: {}".format(f.request.url)
)
print(f"[warn] flow without response: {f.request.url}")
continue
yield MitmproxyFlowWrapper(f)
except FlowReadException as e:

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
import urllib
import urllib.parse
import uuid
from typing import Any, List
from typing import Any
VERBS = [
"add",
@ -81,7 +81,7 @@ def url_to_params(url, path_template):
return params
def request_to_headers(headers: dict[str, List[Any]], add_example: bool = False):
def request_to_headers(headers: dict[str, list[Any]], add_example: bool = False):
"""When given an url and its path template, generates the parameters section of the
request."""
params = []

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from .testing_util import get_nested_key, mitmproxy2swagger_e2e_test

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
from openapi_spec_validator import validate_spec
from mitmproxy2swagger.testing_util import mitmproxy2swagger_e2e_test

@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
import tempfile
from typing import Any, List, Optional
from typing import Any
import ruamel.yaml as ruamel
@ -21,11 +19,13 @@ def get_nested_key(obj: Any, path: str) -> Any:
def mitmproxy2swagger_e2e_test(
input_file: str, url_prefix: str, extra_args: Optional[List[str]] = None
input_file: str, url_prefix: str, extra_args: list[str] | None = None
) -> Any:
"""Runs mitmproxy2swagger on the given input file twice, and returns the detected
endpoints."""
yaml_tmp_path = tempfile.mktemp(suffix=".yaml", prefix="sklep.lisek.")
yaml_tmp_path = tempfile.NamedTemporaryFile(
suffix=".yaml", prefix="sklep.lisek.", delete=False
).name
main(
[
"-i",
@ -41,7 +41,7 @@ def mitmproxy2swagger_e2e_test(
data = None
# try to parse the file
with open(yaml_tmp_path, "r") as f:
with open(yaml_tmp_path) as f:
data = yaml.load(f.read())
assert data is not None
assert "x-path-templates" in data
@ -69,6 +69,6 @@ def mitmproxy2swagger_e2e_test(
)
# load the file again
with open(yaml_tmp_path, "r") as f:
with open(yaml_tmp_path) as f:
data = yaml.load(f.read())
return data

@ -5,10 +5,10 @@ description = ""
authors = [{name = "alufers", email = "alufers@wp.pl"}]
readme = "README.md"
license = {text = "MIT"}
requires-python = ">=3.10"
requires-python = ">=3.12"
dependencies = [
"mitmproxy>=11.0.2,<12",
"ruamel.yaml>=0.17.32,<0.19.0",
"mitmproxy>=12.2.3",
"ruamel.yaml>=0.17.32",
"json-stream>=2.3.2,<3",
"msgpack>=1.0.7,<2",
]
@ -18,36 +18,26 @@ mitmproxy2swagger = "mitmproxy2swagger.mitmproxy2swagger:main"
[dependency-groups]
dev = [
"black>=24.10.0,<25",
"isort>=5.12.0,<6",
"mypy>=1.13.0,<2",
"flake8>=7.1.1,<8",
"docformatter[tomli]>=1.7.1,<2",
"pre-commit>=4.0.1,<5",
"ruff>=0.15.14",
"ty>=0.0.39",
"pytest>=8.3.3,<9",
"pytest-asyncio>=0.20.3,<0.25.0",
"vermin>=1.5.1,<2",
"openapi-spec-validator>=0.5.6,<0.8.0",
"pytest-cov>=6.0.0,<7",
"prek>=0.4.1",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.black]
[tool.ruff]
line-length = 88
target-version = ['py310']
target-version = "py310"
[tool.isort]
profile = "black"
[tool.mypy]
python_version = "3.10"
[tool.docformatter]
recursive = true
wrap-summaries = 88
[tool.ruff.lint]
select = ["E", "F", "I", "W", "UP"]
ignore = ["E501"]
[tool.pytest.ini_options]
asyncio_default_fixture_loop_scope = "function"

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import json
from testclient import testclient

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
import json
from testserver import TestServerHandler, launchServerWith

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import msgpack
from testclient import testclient

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
import msgpack
from testserver import TestServerHandler, launchServerWith

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from typing import Any, Callable
from collections.abc import Callable
from typing import Any
import requests # type: ignore
import requests
def testclient(

@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
import http.server
import socketserver
from typing import Type
class TestServerHandler(http.server.BaseHTTPRequestHandler):
@ -18,7 +16,7 @@ class TestServerHandler(http.server.BaseHTTPRequestHandler):
# Send the response
self.send_response(200)
self.send_header("Content-type", self.headers["Content-type"])
self.send_header("Content-length", len(modified_data))
self.send_header("Content-length", str(len(modified_data)))
self.end_headers()
self.wfile.write(modified_data)
@ -33,7 +31,7 @@ class TestServerHandler(http.server.BaseHTTPRequestHandler):
raise NotImplementedError("Subclasses must implement this method")
def launchServerWith(handler: Type[TestServerHandler]):
def launchServerWith(handler: type[TestServerHandler]):
PORT = 8082
with socketserver.TCPServer(("", PORT), handler) as httpd:
print(f"Serving on port {PORT}")

1693
uv.lock

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save