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

dependabot/pip/json-stream-gte-2.5.1-and-lt-3
alufers 4 months 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 name: Prek checks
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
name: Python on: [push, pull_request]
on:
push:
branches: [master]
pull_request:
branches: [master]
permissions:
contents: read
jobs: jobs:
build: prek:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- name: Set up Python 3.12 - uses: j178/prek-action@v2
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

@ -18,12 +18,12 @@ jobs:
steps: steps:
# Checkout project repository # Checkout project repository
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python 3.12 - name: Set up Python 3.14
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: "3.12" python-version: "3.14"
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@v4 uses: astral-sh/setup-uv@v6
with: with:
version: "latest" version: "latest"
- name: Install dependencies - name: Install dependencies
@ -31,7 +31,7 @@ jobs:
uv sync uv sync
- name: Run Python lint checks - name: Run Python lint checks
run: | run: |
uv run pre-commit run --all-files uv run prek run --all-files
- name: Run Python tests - name: Run Python tests
run: | run: |
uv run pytest --cov 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/.*)$ exclude: ^(api_protobuf/.*|docs/protobuf_docs\.md|dhaul_openapi/messages/.*)$
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0 rev: v6.0.0
hooks: hooks:
- id: check-yaml - id: check-yaml
- id: end-of-file-fixer - id: end-of-file-fixer
@ -12,53 +12,20 @@ repos:
exclude: ^testdata/.* exclude: ^testdata/.*
- id: check-json - id: check-json
- id: detect-private-key - id: detect-private-key
- id: fix-encoding-pragma
- id: check-merge-conflict - id: check-merge-conflict
- id: check-added-large-files - id: check-added-large-files
- repo: https://github.com/psf/black - repo: https://github.com/astral-sh/ruff-pre-commit
rev: 24.10.0 rev: v0.15.14
hooks: hooks:
- id: black - id: ruff
args: [--config=pyproject.toml] args: [--fix]
- repo: https://github.com/PyCQA/isort - id: ruff-format
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)
- repo: https://github.com/adrienverge/yamllint.git - repo: https://github.com/adrienverge/yamllint.git
rev: v1.35.1 rev: v1.38.0
hooks: hooks:
- id: yamllint - id: yamllint
- repo: https://github.com/igorshubovych/markdownlint-cli - repo: https://github.com/igorshubovych/markdownlint-cli
rev: v0.42.0 rev: v0.48.0
hooks: hooks:
- id: markdownlint-fix - id: markdownlint-fix
- id: markdownlint - 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/ COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
ENV UV_HTTP_TIMEOUT=100 \ ENV UV_HTTP_TIMEOUT=100 \
UV_NO_CACHE=1 \ UV_NO_CACHE=1 \
@ -7,9 +7,9 @@ WORKDIR /app
COPY pyproject.toml uv.lock ./ COPY pyproject.toml uv.lock ./
RUN uv sync --no-dev --frozen --no-install-project RUN uv sync --no-dev --frozen --no-install-project
COPY . . 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 \ ENV PYTHONFAULTHANDLER=1 \
PYTHONHASHSEED=random \ PYTHONHASHSEED=random \
PYTHONUNBUFFERED=1 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 [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 ## Development and contributing
This project uses: This project uses:
- [uv](https://docs.astral.sh/uv/) for dependency management - [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 - [pytest](https://docs.pytest.org/en/stable/) for unit testing
To install the dependencies: To install the dependencies:
@ -142,13 +142,13 @@ uv sync
Run linters: Run linters:
```bash ```bash
pre-commit run --all-files uv run prek run --all-files
``` ```
Install pre-commit hooks: Install prek hooks:
```bash ```bash
pre-commit install uv run prek install
``` ```
Run tests: Run tests:

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
import sys import sys
ANSI_RGB = "\033[38;2;{};{};{}m" ANSI_RGB = "\033[38;2;{};{};{}m"
@ -64,5 +63,5 @@ def print_progress_bar(progress=0.0):
progress_bar_contents += " " progress_bar_contents += " "
progress_bar_contents += ANSI_RESET 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() sys.stdout.flush()

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

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

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

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
import urllib import urllib
import urllib.parse
import uuid import uuid
from typing import Any, List from typing import Any
VERBS = [ VERBS = [
"add", "add",
@ -81,7 +81,7 @@ def url_to_params(url, path_template):
return params 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 """When given an url and its path template, generates the parameters section of the
request.""" request."""
params = [] params = []

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from .testing_util import get_nested_key, mitmproxy2swagger_e2e_test 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 openapi_spec_validator import validate_spec
from mitmproxy2swagger.testing_util import mitmproxy2swagger_e2e_test from mitmproxy2swagger.testing_util import mitmproxy2swagger_e2e_test

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

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

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

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

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

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

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

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

1693
uv.lock

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