Skip to content

Commit a96f382

Browse files
authored
Merge pull request #15 from mosquito/migrate-to-uv
Migrate to UV
2 parents dc8c6a4 + 1473851 commit a96f382

17 files changed

Lines changed: 900 additions & 973 deletions

File tree

.github/workflows/publish.yml

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,14 @@ jobs:
1313
steps:
1414
- uses: actions/checkout@v4
1515

16-
- name: Setup python
17-
uses: actions/setup-python@v5
18-
with:
19-
python-version: "3.12"
20-
21-
- name: Install poetry
22-
run: python -m pip install poetry
16+
- name: Setup uv
17+
uses: astral-sh/setup-uv@v6
2318

2419
- name: Set version from release tag
25-
run: poetry version "${GITHUB_REF_NAME#v}"
20+
run: uv version "${GITHUB_REF_NAME#v}"
2621

2722
- name: Build package
28-
run: poetry build
23+
run: uv build
2924

3025
- uses: actions/upload-artifact@v4
3126
with:

.github/workflows/pythonpackage.yml

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,27 @@ on:
66
pull_request:
77
branches: [ master ]
88

9+
env:
10+
FORCE_COLOR: 1
11+
912
jobs:
13+
lint:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v4
17+
18+
- name: Setup uv
19+
uses: astral-sh/setup-uv@v6
20+
21+
- name: Install dependencies
22+
run: uv sync --frozen
23+
24+
- name: Run ruff check
25+
run: uv run ruff check jwt_rsa tests
26+
27+
- name: Run ruff format check
28+
run: uv run ruff format --check jwt_rsa tests
29+
1030
mypy:
1131
runs-on: ubuntu-latest
1232
strategy:
@@ -15,23 +35,19 @@ jobs:
1535
steps:
1636
- uses: actions/checkout@v4
1737

38+
- name: Setup uv
39+
uses: astral-sh/setup-uv@v6
40+
1841
- name: Setup python3.10
1942
uses: actions/setup-python@v5
2043
with:
2144
python-version: "3.10"
2245

23-
- name: Install poetry
24-
run: python -m pip install poetry
25-
2646
- name: Install dependencies
27-
run: poetry install
28-
env:
29-
FORCE_COLOR: yes
47+
run: uv sync --frozen
3048

3149
- name: Run mypy
32-
run: poetry run mypy jwt_rsa
33-
env:
34-
FORCE_COLOR: yes
50+
run: uv run mypy jwt_rsa
3551

3652
tests:
3753
runs-on: ubuntu-latest
@@ -48,20 +64,16 @@ jobs:
4864
steps:
4965
- uses: actions/checkout@v4
5066

67+
- name: Setup uv
68+
uses: astral-sh/setup-uv@v6
69+
5170
- name: Setup python${{ matrix.python }}
5271
uses: actions/setup-python@v5
5372
with:
5473
python-version: "${{ matrix.python }}"
5574

56-
- name: Install poetry
57-
run: python -m pip install poetry
58-
5975
- name: Install dependencies
60-
run: poetry install
61-
env:
62-
FORCE_COLOR: yes
76+
run: uv sync --frozen
6377

6478
- name: Run tests
65-
run: poetry run pytest
66-
env:
67-
FORCE_COLOR: yes
79+
run: uv run pytest

CLAUDE.md

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,29 @@ pyjwt-rsa — Python library and CLI for JWT token management with RSA cryptogra
1010

1111
```bash
1212
# Install dependencies
13-
poetry install
13+
uv sync
1414

15-
# Run all tests (includes coverage + pylama linting)
16-
poetry run pytest
15+
# Run all checks (lint + mypy + tests)
16+
make test
17+
18+
# Run tests only (includes coverage)
19+
uv run pytest
1720

1821
# Run tests without coverage
19-
poetry run pytest --no-cov
22+
uv run pytest --no-cov
2023

2124
# Run a single test file or test
22-
poetry run pytest tests/test_rsa.py
23-
poetry run pytest tests/test_rsa.py::test_jwt_token
25+
uv run pytest tests/test_rsa.py
26+
uv run pytest tests/test_rsa.py::test_jwt_token
2427

2528
# Type checking (strict mode)
26-
poetry run mypy jwt_rsa
29+
uv run mypy jwt_rsa
2730

2831
# Linting
29-
poetry run pylama jwt_rsa tests
32+
uv run ruff check jwt_rsa tests
3033

3134
# Build
32-
poetry build
35+
uv build
3336
```
3437

3538
## Architecture
@@ -43,6 +46,6 @@ poetry build
4346
## Code Quality
4447

4548
- **mypy strict** — all public functions must have full type annotations. Tests have `ignore_errors = true`.
46-
- **pylama** — pycodestyle + pyflakes + mccabe. Max line length 119. Ignored: E704.
47-
- **pytest**`addopts` integrates coverage, doctests, and pylama in a single run.
49+
- **ruff** — pycodestyle + pyflakes + isort + pyupgrade + flake8-bugbear. Max line length 119. Ignored: E501, E704.
50+
- **pytest**`addopts` integrates coverage and doctests in a single run.
4851
- Python 3.10+ required. CI tests on 3.10–3.13.

Makefile

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
.PHONY: test tests mypy pytest lint format
2+
3+
format:
4+
uv run --group dev ruff check --fix jwt_rsa tests
5+
uv run --group dev ruff format jwt_rsa tests
6+
7+
lint:
8+
uv run --group dev ruff check jwt_rsa tests
9+
uv run --group dev ruff format --check jwt_rsa tests
10+
11+
mypy:
12+
uv run --group dev mypy jwt_rsa
13+
14+
pytest:
15+
uv run --group dev pytest
16+
17+
test: lint mypy pytest
18+
tests: test

jwt_rsa/__init__.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
from .rsa import (
2-
RSAJWKPrivateKey, RSAJWKPublicKey, generate_rsa, load_private_key,
3-
load_public_key, rsa_to_jwk,
2+
RSAJWKPrivateKey,
3+
RSAJWKPublicKey,
4+
generate_rsa,
5+
load_private_key,
6+
load_public_key,
7+
rsa_to_jwk,
48
)
59
from .token import JWT, JWTDecoder, JWTSigner
610
from .types import RSAPrivateKey, RSAPublicKey
711

8-
912
__all__ = (
1013
"JWT",
1114
"JWTDecoder",

jwt_rsa/cli.py

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
from pathlib import Path
66

77
from jwt_rsa.issue import parse_interval
8-
from . import convert, issue, key_tester, keygen, pubkey, verify, jwks
8+
9+
from . import convert, issue, jwks, key_tester, keygen, pubkey, verify
910
from .token import ALGORITHMS
1011

1112

@@ -14,24 +15,32 @@ class Formatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionH
1415

1516

1617
parser = ArgumentParser(formatter_class=Formatter)
18+
parser.add_argument("-a", "--algorithm", choices=ALGORITHMS, help="Algorithm for JWT keys", default="RS512")
1719
parser.add_argument(
18-
"-a", "--algorithm", choices=ALGORITHMS,
19-
help="Algorithm for JWT keys", default="RS512"
20-
)
21-
parser.add_argument(
22-
"--log-level", choices=["debug", "info", "warning", "error", "critical"],
23-
type=lambda x: getattr(logging, x.upper(), logging.INFO), default=logging.INFO,
20+
"--log-level",
21+
choices=["debug", "info", "warning", "error", "critical"],
22+
type=lambda x: getattr(logging, x.upper(), logging.INFO),
23+
default=logging.INFO,
2424
)
2525

2626
subparsers = parser.add_subparsers(dest="command", required=True)
2727

2828
keygen_parser = subparsers.add_parser("keygen", help="Generate a new RSA key pair", formatter_class=Formatter)
2929
keygen_parser.set_defaults(func=keygen.main)
3030
keygen_parser.add_argument(
31-
"-b", "--bits", dest="bits", type=int, default=2048, choices=[2 ** i for i in range(10, 14)],
31+
"-b",
32+
"--bits",
33+
dest="bits",
34+
type=int,
35+
default=2048,
36+
choices=[2**i for i in range(10, 14)],
3237
)
3338
keygen_parser.add_argument(
34-
"--kid", dest="kid", type=str, default="", help="Key ID, will be generated if missing",
39+
"--kid",
40+
dest="kid",
41+
type=str,
42+
default="",
43+
help="Key ID, will be generated if missing",
3544
)
3645
keygen_parser.add_argument("-u", "--use", dest="use", type=str, default="sig", choices=["sig", "enc"])
3746
keygen_parser.add_argument("-o", "--format", choices=["pem", "jwk", "base64"], default="jwk")
@@ -68,23 +77,28 @@ class Formatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionH
6877
" * 'y' means years\n"
6978
)
7079
issue_parser = subparsers.add_parser(
71-
"issue",
72-
help="Issue a new JWT token\n",
73-
description=ISSUE_PARSER_DESCRIPTION,
74-
formatter_class=Formatter
80+
"issue", help="Issue a new JWT token\n", description=ISSUE_PARSER_DESCRIPTION, formatter_class=Formatter
7581
)
7682
issue_parser.add_argument(
77-
"-K", "--private-key", required=True,
78-
help="Private JWT key", type=Path,
83+
"-K",
84+
"--private-key",
85+
required=True,
86+
help="Private JWT key",
87+
type=Path,
7988
)
8089
issue_parser.add_argument("--expired", help="Token expiration", type=parse_interval, default="+1M")
8190
issue_parser.add_argument("--nbf", help="Token nbf claim", type=parse_interval, default="-1m")
8291
issue_parser.add_argument(
83-
"-I", "--no-interactive", action="store_false", dest="interactive",
92+
"-I",
93+
"--no-interactive",
94+
action="store_false",
95+
dest="interactive",
8496
help="No interactive mode, do not open editor for claims, just read JSON from stdin",
8597
)
8698
issue_parser.add_argument(
87-
"-e", "--editor", help="Editor to use in interactive mode",
99+
"-e",
100+
"--editor",
101+
help="Editor to use in interactive mode",
88102
default=os.getenv("EDITOR", "vim"),
89103
)
90104
issue_parser.set_defaults(func=issue.main)
@@ -95,7 +109,10 @@ class Formatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionH
95109
verify_parser.add_argument("-k", "--public-key", required=False, help="Public key", type=Path)
96110
verify_parser.add_argument("-V", "--no-verify", action="store_false", help="No verify signature", dest="verify")
97111
verify_parser.add_argument(
98-
"-I", "--no-interactive", action="store_false", dest="interactive",
112+
"-I",
113+
"--no-interactive",
114+
action="store_false",
115+
dest="interactive",
99116
help="Interactive mode or raw read token from stdin",
100117
)
101118
verify_parser.set_defaults(func=verify.main)

jwt_rsa/convert.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@ def generate_kid(key: RSAPrivateKey) -> str:
1919

2020

2121
def convert(
22-
private: RSAPrivateKey, public: RSAPublicKey,
22+
private: RSAPrivateKey,
23+
public: RSAPublicKey,
2324
fmt: Literal["pem", "jwk", "base64"],
24-
pretty: bool = False, algorithm: AlgorithmType = "RS512",
25+
pretty: bool = False,
26+
algorithm: AlgorithmType = "RS512",
2527
) -> tuple[str, str]:
2628
if fmt == "pem":
2729
return (

jwt_rsa/issue.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
from .rsa import load_private_key
1515
from .token import JWT
1616

17-
1817
TEMPLATE = """# THIS FILE SUPPORTS COMMENTS AND TRAILING COMMAS
1918
# Actually it's a Python dictionary that will be evaluated as a JSON object
2019
# Anyway it supports Python-style single and double quotes, math expressions, etc.
@@ -97,7 +96,7 @@ def main(arguments: SimpleNamespace) -> None:
9796
load_private_key(arguments.private_key),
9897
expires=arguments.expired,
9998
nbf_delta=-arguments.nbf,
100-
algorithm=arguments.algorithm
99+
algorithm=arguments.algorithm,
101100
)
102101

103102
whoami = pwd.getpwuid(os.getuid())
@@ -125,7 +124,8 @@ def main(arguments: SimpleNamespace) -> None:
125124
with NamedTemporaryFile("wt", suffix=".py") as fp:
126125
if arguments.interactive:
127126
fp.write(
128-
TEMPLATE % {
127+
TEMPLATE
128+
% {
129129
"exp": arguments.expired,
130130
"nbf": arguments.nbf,
131131
"preamble": preable,

jwt_rsa/jwks.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from abc import ABC, abstractmethod
77
from contextlib import closing
88
from functools import lru_cache
9-
from typing import TypedDict, Any
9+
from typing import Any, TypedDict
1010
from urllib.parse import urlparse
1111

1212
from jwt.api_jws import PyJWS
@@ -43,25 +43,25 @@ def parse_jwk(self, jwk: dict[str, Any]) -> dict[str, RSAPublicKey]:
4343
if loaded_key is None:
4444
log.warning("Skipping JWK key: %s", key)
4545
continue
46-
keys[jwk['kid']] = loaded_key
46+
keys[jwk["kid"]] = loaded_key
4747
return keys
4848

4949
@abstractmethod
5050
def refresh(self) -> Any:
5151
raise NotImplementedError()
5252

53-
@lru_cache(1024)
53+
@lru_cache(1024) # noqa: B019
5454
def decoder(self, kid: str) -> JWTDecoder:
5555
if kid not in self.keys:
5656
raise ValueError(f"Key with kid '{kid}' is unknown")
5757
return JWTDecoder(key=self.keys[kid])
5858

5959
def decode(self, token: str) -> dict[str, Any]:
6060
header = self.jws.get_unverified_header(token)
61-
if 'kid' not in header:
61+
if "kid" not in header:
6262
raise ValueError("Token does not contain 'kid' header")
6363

64-
decoder = self.decoder(header['kid'])
64+
decoder = self.decoder(header["kid"])
6565
return decoder.decode(token)
6666

6767

@@ -74,11 +74,7 @@ def __init__(self, url: str, *, ssl_context: ssl.SSLContext | None = None, **kwa
7474

7575
def refresh(self) -> None:
7676
url_parts = urlparse(self.url)
77-
host, port = (
78-
url_parts.netloc.split(":")
79-
if ":" in url_parts.netloc
80-
else (url_parts.netloc, 443)
81-
)
77+
host, port = url_parts.netloc.split(":") if ":" in url_parts.netloc else (url_parts.netloc, 443)
8278
with closing(self.CLIENT_CLASS(host, int(port), context=self.ssl_context)) as client:
8379
client.request("GET", url_parts.path)
8480
response = client.getresponse()
@@ -91,6 +87,7 @@ def refresh(self) -> None:
9187

9288
def main(args: argparse.Namespace) -> None:
9389
from .rsa import rsa_to_jwk
90+
9491
fetcher = HTTPSJWKFetcher(args.url)
9592
fetcher.refresh()
9693

0 commit comments

Comments
 (0)