chore: add uv, linting, fixes (#39)
Some checks failed
CI / lint (push) Has been cancelled
/ release (push) Has been cancelled
CI / tests (push) Has been cancelled
/ pullrequest (push) Has been cancelled

* chore: add uv, linting, fixes
This commit is contained in:
dni ⚡ 2025-10-30 10:43:27 +01:00 committed by GitHub
parent 15079c3e58
commit 35584a230f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 2405 additions and 2752 deletions

View file

@ -11,14 +11,9 @@ jobs:
tests:
runs-on: ubuntu-latest
needs: [lint]
strategy:
matrix:
python-version: ['3.9', '3.10']
steps:
- uses: actions/checkout@v4
- uses: lnbits/lnbits/.github/actions/prepare@dev
with:
python-version: ${{ matrix.python-version }}
- name: Run pytest
uses: pavelzw/pytest-action@v2
env:
@ -30,5 +25,5 @@ jobs:
job-summary: true
emoji: false
click-to-expand: true
custom-pytest: poetry run pytest
report-title: 'test (${{ matrix.python-version }})'
custom-pytest: uv run pytest
report-title: 'test'

View file

@ -5,27 +5,27 @@ format: prettier black ruff
check: mypy pyright checkblack checkruff checkprettier
prettier:
poetry run ./node_modules/.bin/prettier --write .
uv run ./node_modules/.bin/prettier --write .
pyright:
poetry run ./node_modules/.bin/pyright
uv run ./node_modules/.bin/pyright
mypy:
poetry run mypy .
uv run mypy .
black:
poetry run black .
uv run black .
ruff:
poetry run ruff check . --fix
uv run ruff check . --fix
checkruff:
poetry run ruff check .
uv run ruff check .
checkprettier:
poetry run ./node_modules/.bin/prettier --check .
uv run ./node_modules/.bin/prettier --check .
checkblack:
poetry run black --check .
uv run black --check .
checkeditorconfig:
editorconfig-checker
@ -34,15 +34,15 @@ test:
LNBITS_DATA_FOLDER="./tests/data" \
PYTHONUNBUFFERED=1 \
DEBUG=true \
poetry run pytest
uv run pytest
install-pre-commit-hook:
@echo "Installing pre-commit hook to git"
@echo "Uninstall the hook with poetry run pre-commit uninstall"
poetry run pre-commit install
@echo "Uninstall the hook with uv run pre-commit uninstall"
uv run pre-commit install
pre-commit:
poetry run pre-commit run --all-files
uv run pre-commit run --all-files
checkbundle:

11
crud.py
View file

@ -1,5 +1,4 @@
import json
from typing import Optional
from lnbits.db import Database
@ -21,7 +20,7 @@ async def update_relay(relay: NostrRelay) -> NostrRelay:
return relay
async def get_relay(user_id: str, relay_id: str) -> Optional[NostrRelay]:
async def get_relay(user_id: str, relay_id: str) -> NostrRelay | None:
return await db.fetchone(
"SELECT * FROM nostrrelay.relays WHERE user_id = :user_id AND id = :id",
{"user_id": user_id, "id": relay_id},
@ -29,7 +28,7 @@ async def get_relay(user_id: str, relay_id: str) -> Optional[NostrRelay]:
)
async def get_relay_by_id(relay_id: str) -> Optional[NostrRelay]:
async def get_relay_by_id(relay_id: str) -> NostrRelay | None:
"""Note: it does not require `user_id`. Can read any relay. Use it with care."""
return await db.fetchone(
"SELECT * FROM nostrrelay.relays WHERE id = :id",
@ -58,7 +57,7 @@ async def get_config_for_all_active_relays() -> dict:
return active_relay_configs
async def get_public_relay(relay_id: str) -> Optional[dict]:
async def get_public_relay(relay_id: str) -> dict | None:
relay = await db.fetchone(
"SELECT * FROM nostrrelay.relays WHERE id = :id",
{"id": relay_id},
@ -130,7 +129,7 @@ async def get_events(
return events
async def get_event(relay_id: str, event_id: str) -> Optional[NostrEvent]:
async def get_event(relay_id: str, event_id: str) -> NostrEvent | None:
event = await db.fetchone(
"SELECT * FROM nostrrelay.events WHERE relay_id = :relay_id AND id = :id",
{"relay_id": relay_id, "id": event_id},
@ -286,7 +285,7 @@ async def delete_account(relay_id: str, pubkey: str):
async def get_account(
relay_id: str,
pubkey: str,
) -> Optional[NostrAccount]:
) -> NostrAccount | None:
return await db.fetchone(
"""
SELECT * FROM nostrrelay.accounts

View file

@ -1,5 +1,3 @@
from typing import Optional
from pydantic import BaseModel
@ -16,8 +14,8 @@ class BuyOrder(BaseModel):
class NostrPartialAccount(BaseModel):
relay_id: str
pubkey: str
allowed: Optional[bool] = None
blocked: Optional[bool] = None
allowed: bool | None = None
blocked: bool | None = None
class NostrAccount(BaseModel):
@ -44,4 +42,4 @@ class NostrEventTags(BaseModel):
event_id: str
name: str
value: str
extra: Optional[str] = None
extra: str | None = None

2629
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,37 +1,37 @@
[tool.poetry]
[project]
name = "nostrrelay"
version = "0.0.0"
description = "nostrrelay"
authors = ["dni <dni@lnbits.com>"]
requires-python = ">=3.10,<3.13"
description = "LNbits, free and open-source Lightning wallet and accounts system."
authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }]
urls = { Homepage = "https://lnbits.com", Repository = "https://github.com/lnbits/nostrrelay" }
dependencies = [ "lnbits>1" ]
[tool.poetry.dependencies]
python = "^3.10 | ^3.9"
lnbits = {allow-prereleases = true, version = "*"}
[tool.poetry]
package-mode = false
[tool.poetry.group.dev.dependencies]
black = "^24.3.0"
pytest-asyncio = "^0.21.0"
pytest = "^7.3.2"
mypy = "^1.5.1"
pre-commit = "^3.2.2"
ruff = "^0.3.2"
pytest-md = "^0.2.0"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[dependency-groups]
dev= [
"black",
"pytest-asyncio",
"pytest",
"mypy==1.17.1",
"pre-commit",
"ruff",
"pytest-md",
]
[tool.mypy]
exclude = [
"boltz_client"
]
plugins = ["pydantic.mypy"]
[tool.pydantic-mypy]
init_forbid_extra = true
init_typed = true
warn_required_dynamic_aliases = true
warn_untyped_fields = true
[[tool.mypy.overrides]]
module = [
"lnbits.*",
"loguru.*",
"fastapi.*",
"pydantic.*",
"embit.*",
"secp256k1.*",
]
ignore_missing_imports = "True"
@ -86,8 +86,8 @@ classmethod-decorators = [
# [tool.ruff.lint.extend-per-file-ignores]
# "views_api.py" = ["F401"]
# [tool.ruff.lint.mccabe]
# max-complexity = 10
[tool.ruff.lint.mccabe]
max-complexity = 11
[tool.ruff.lint.flake8-bugbear]
# Allow default arguments like, e.g., `data: List[str] = fastapi.Query(None)`.

View file

@ -1,6 +1,7 @@
import json
import time
from typing import Any, Awaitable, Callable, List, Optional
from collections.abc import Awaitable, Callable
from typing import Any
from fastapi import WebSocket
from lnbits.helpers import urlsafe_short_hash
@ -25,17 +26,17 @@ class NostrClientConnection:
def __init__(self, relay_id: str, websocket: WebSocket):
self.websocket = websocket
self.relay_id = relay_id
self.filters: List[NostrFilter] = []
self.auth_pubkey: Optional[str] = None # set if authenticated
self._auth_challenge: Optional[str] = None
self.filters: list[NostrFilter] = []
self.auth_pubkey: str | None = None # set if authenticated
self._auth_challenge: str | None = None
self._auth_challenge_created_at = 0
self.event_validator = EventValidator(self.relay_id)
self.broadcast_event: Optional[
Callable[[NostrClientConnection, NostrEvent], Awaitable[None]]
] = None
self.get_client_config: Optional[Callable[[], RelaySpec]] = None
self.broadcast_event: (
Callable[[NostrClientConnection, NostrEvent], Awaitable[None]] | None
) = None
self.get_client_config: Callable[[], RelaySpec] | None = None
async def start(self):
await self.websocket.accept()
@ -50,7 +51,7 @@ class NostrClientConnection:
except Exception as e:
logger.warning(e)
async def stop(self, reason: Optional[str]):
async def stop(self, reason: str | None):
message = reason if reason else "Server closed webocket"
try:
await self._send_msg(["NOTICE", message])
@ -98,7 +99,7 @@ class NostrClientConnection:
if self.broadcast_event:
await self.broadcast_event(self, e)
async def _handle_message(self, data: List) -> List:
async def _handle_message(self, data: list) -> list:
if len(data) < 2:
return []
@ -121,7 +122,9 @@ class NostrClientConnection:
# Handle multiple filters in REQ message
responses = []
for filter_data in data[2:]:
response = await self._handle_request(subscription_id, NostrFilter.parse_obj(filter_data))
response = await self._handle_request(
subscription_id, NostrFilter.parse_obj(filter_data)
)
responses.extend(response)
return responses
if message_type == NostrEventType.CLOSE:
@ -133,7 +136,7 @@ class NostrClientConnection:
async def _handle_event(self, e: NostrEvent):
logger.info(f"nostr event: [{e.kind}, {e.pubkey}, '{e.content}']")
resp_nip20: List[Any] = ["OK", e.id]
resp_nip20: list[Any] = ["OK", e.id]
if e.is_auth_response_event:
valid, message = self.event_validator.validate_auth_event(
@ -172,12 +175,12 @@ class NostrClientConnection:
if d_tag_value:
deletion_filter = NostrFilter(
kinds=[e.kind],
kinds=[e.kind],
authors=[e.pubkey],
**{"#d": [d_tag_value]},
until=e.created_at
**{"#d": [d_tag_value]}, # type: ignore
until=e.created_at,
)
await delete_events(self.relay_id, deletion_filter)
if not e.is_ephemeral_event:
await create_event(e)
@ -201,7 +204,7 @@ class NostrClientConnection:
raise Exception("Client not ready!")
return self.get_client_config()
async def _send_msg(self, data: List):
async def _send_msg(self, data: list):
await self.websocket.send_text(json.dumps(data))
async def _handle_delete_event(self, event: NostrEvent):
@ -214,7 +217,7 @@ class NostrClientConnection:
async def _handle_request(
self, subscription_id: str, nostr_filter: NostrFilter
) -> List:
) -> list:
if self.config.require_auth_filter:
if not self.auth_pubkey:
return [["AUTH", self._current_auth_challenge()]]

View file

@ -1,5 +1,3 @@
from typing import List
from ..crud import get_config_for_all_active_relays
from .client_connection import NostrClientConnection
from .event import NostrEvent
@ -47,7 +45,7 @@ class NostrClientManager:
def get_relay_config(self, relay_id: str) -> RelaySpec:
return self._active_relays[relay_id]
def clients(self, relay_id: str) -> List[NostrClientConnection]:
def clients(self, relay_id: str) -> list[NostrClientConnection]:
if relay_id not in self._clients:
self._clients[relay_id] = []
return self._clients[relay_id]

View file

@ -1,5 +1,5 @@
import time
from typing import Callable, Optional, Tuple
from collections.abc import Callable
from ..crud import get_account, get_storage_for_public_key, prune_old_events
from ..helpers import extract_domain
@ -15,11 +15,11 @@ class EventValidator:
self._last_event_timestamp = 0 # in hours
self._event_count_per_timestamp = 0
self.get_client_config: Optional[Callable[[], RelaySpec]] = None
self.get_client_config: Callable[[], RelaySpec] | None = None
async def validate_write(
self, e: NostrEvent, publisher_pubkey: str
) -> Tuple[bool, str]:
) -> tuple[bool, str]:
valid, message = self._validate_event(e)
if not valid:
return (valid, message)
@ -34,8 +34,8 @@ class EventValidator:
return True, ""
def validate_auth_event(
self, e: NostrEvent, auth_challenge: Optional[str]
) -> Tuple[bool, str]:
self, e: NostrEvent, auth_challenge: str | None
) -> tuple[bool, str]:
valid, message = self._validate_event(e)
if not valid:
return (valid, message)
@ -59,7 +59,7 @@ class EventValidator:
raise Exception("EventValidator not ready!")
return self.get_client_config()
def _validate_event(self, e: NostrEvent) -> Tuple[bool, str]:
def _validate_event(self, e: NostrEvent) -> tuple[bool, str]:
if self._exceeded_max_events_per_hour():
return False, "Exceeded max events per hour limit'!"
@ -76,7 +76,7 @@ class EventValidator:
async def _validate_storage(
self, pubkey: str, event_size_bytes: int
) -> Tuple[bool, str]:
) -> tuple[bool, str]:
if self.config.is_read_only_relay:
return False, "Cannot write event, relay is read-only"
@ -124,7 +124,7 @@ class EventValidator:
return self._event_count_per_timestamp > self.config.max_events_per_hour
def _created_at_in_range(self, created_at: int) -> Tuple[bool, str]:
def _created_at_in_range(self, created_at: int) -> tuple[bool, str]:
current_time = round(time.time())
if self.config.created_at_in_past != 0:
if created_at < (current_time - self.config.created_at_in_past):

View file

@ -1,5 +1,3 @@
from typing import Optional
from pydantic import BaseModel, Field
from .event import NostrEvent
@ -12,10 +10,10 @@ class NostrFilter(BaseModel):
ids: list[str] = []
authors: list[str] = []
kinds: list[int] = []
subscription_id: Optional[str] = None
since: Optional[int] = None
until: Optional[int] = None
limit: Optional[int] = None
subscription_id: str | None = None
since: int | None = None
until: int | None = None
limit: int | None = None
def matches(self, e: NostrEvent) -> bool:
# todo: starts with
@ -93,9 +91,12 @@ class NostrFilter(BaseModel):
if len(self.d):
d_s = ",".join([f"'{d}'" for d in self.d])
d_join = "INNER JOIN nostrrelay.event_tags d_tags ON nostrrelay.events.id = d_tags.event_id"
d_join = (
"INNER JOIN nostrrelay.event_tags d_tags "
"ON nostrrelay.events.id = d_tags.event_id"
)
d_where = f" d_tags.value in ({d_s}) AND d_tags.name = 'd'"
inner_joins.append(d_join)
where.append(d_where)

View file

@ -1,5 +1,3 @@
from typing import Optional
from pydantic import BaseModel, Field
@ -100,11 +98,11 @@ class RelaySpec(RelayPublicSpec, WalletSpec, AuthSpec):
class NostrRelay(BaseModel):
id: str
user_id: Optional[str] = None
user_id: str | None = None
name: str
description: Optional[str] = None
pubkey: Optional[str] = None
contact: Optional[str] = None
description: str | None = None
pubkey: str | None = None
contact: str | None = None
active: bool = False
meta: RelaySpec = RelaySpec()

View file

@ -1,7 +1,7 @@
import asyncio
import inspect
from typing import List, Optional
import pytest
import pytest_asyncio
from lnbits.db import Database
from loguru import logger
@ -14,11 +14,11 @@ from .helpers import get_fixtures
class EventFixture(BaseModel):
name: str
exception: Optional[str]
exception: str | None
data: NostrEvent
@pytest_asyncio.fixture(scope="session")
@pytest.fixture(scope="session")
def event_loop():
loop = asyncio.get_event_loop()
yield loop
@ -33,16 +33,16 @@ async def migrate_db():
print("### 1000")
logger.info(f"Running migration '{key}'.")
await migrate(db)
return migrations
return db
@pytest_asyncio.fixture(scope="session")
def valid_events(migrate_db) -> List[EventFixture]:
@pytest.fixture(scope="session")
def valid_events(migrate_db) -> list[EventFixture]:
data = get_fixtures("events")
return [EventFixture.parse_obj(e) for e in data["valid"]]
@pytest_asyncio.fixture(scope="session")
def invalid_events(migrate_db) -> List[EventFixture]:
@pytest.fixture(scope="session")
def invalid_events(migrate_db) -> list[EventFixture]:
data = get_fixtures("events")
return [EventFixture.parse_obj(e) for e in data["invalid"]]

View file

@ -1,6 +1,5 @@
import asyncio
from json import dumps, loads
from typing import Optional
import pytest
from fastapi import WebSocket
@ -41,7 +40,7 @@ class MockWebSocket(WebSocket):
async def wire_mock_data(self, data: dict):
await self.fake_wire.put(dumps(data))
async def close(self, code: int = 1000, reason: Optional[str] = None) -> None:
async def close(self, code: int = 1000, reason: str | None = None) -> None:
logger.info(f"{code}: {reason}")

View file

@ -1,5 +1,4 @@
import json
from typing import List
import pytest
from loguru import logger
@ -16,7 +15,7 @@ from .conftest import EventFixture
RELAY_ID = "r1"
def test_valid_event_id_and_signature(valid_events: List[EventFixture]):
def test_valid_event_id_and_signature(valid_events: list[EventFixture]):
for f in valid_events:
try:
f.data.check_signature()
@ -25,14 +24,14 @@ def test_valid_event_id_and_signature(valid_events: List[EventFixture]):
raise e
def test_invalid_event_id_and_signature(invalid_events: List[EventFixture]):
def test_invalid_event_id_and_signature(invalid_events: list[EventFixture]):
for f in invalid_events:
with pytest.raises(ValueError, match=f.exception):
f.data.check_signature()
@pytest.mark.asyncio
async def test_valid_event_crud(valid_events: List[EventFixture]):
async def test_valid_event_crud(valid_events: list[EventFixture]):
author = "a24496bca5dd73300f4e5d5d346c73132b7354c597fcbb6509891747b4689211"
event_id = "3219eec7427e365585d5adf26f5d2dd2709d3f0f2c0e1f79dc9021e951c67d96"
reply_event_id = "6b2b6cb9c72caaf3dfbc5baa5e68d75ac62f38ec011b36cc83832218c36e4894"
@ -65,7 +64,7 @@ async def get_by_id(data: NostrEvent, test_name: str):
), f"Restored event is different for fixture '{test_name}'"
async def filter_by_id(all_events: List[NostrEvent], data: NostrEvent, test_name: str):
async def filter_by_id(all_events: list[NostrEvent], data: NostrEvent, test_name: str):
nostr_filter = NostrFilter(ids=[data.id])
events = await get_events(RELAY_ID, nostr_filter)
@ -81,7 +80,7 @@ async def filter_by_id(all_events: List[NostrEvent], data: NostrEvent, test_name
), f"Filtered event is different for fixture '{test_name}'"
async def filter_by_author(all_events: List[NostrEvent], author):
async def filter_by_author(all_events: list[NostrEvent], author):
nostr_filter = NostrFilter(authors=[author])
events_by_author = await get_events(RELAY_ID, nostr_filter)
assert len(events_by_author) == 5, "Failed to query by authors"
@ -90,7 +89,7 @@ async def filter_by_author(all_events: List[NostrEvent], author):
assert len(filtered_events) == 5, "Failed to filter by authors"
async def filter_by_tag_p(all_events: List[NostrEvent], author):
async def filter_by_tag_p(all_events: list[NostrEvent], author):
# todo: check why constructor does not work for fields with aliases (#e, #p)
nostr_filter = NostrFilter()
nostr_filter.p.append(author)
@ -102,7 +101,7 @@ async def filter_by_tag_p(all_events: List[NostrEvent], author):
assert len(filtered_events) == 5, "Failed to filter by tag 'p'"
async def filter_by_tag_e(all_events: List[NostrEvent], event_id):
async def filter_by_tag_e(all_events: list[NostrEvent], event_id):
nostr_filter = NostrFilter()
nostr_filter.e.append(event_id)
@ -114,7 +113,7 @@ async def filter_by_tag_e(all_events: List[NostrEvent], event_id):
async def filter_by_tag_e_and_p(
all_events: List[NostrEvent], author, event_id, reply_event_id
all_events: list[NostrEvent], author, event_id, reply_event_id
):
nostr_filter = NostrFilter()
nostr_filter.p.append(author)
@ -134,7 +133,7 @@ async def filter_by_tag_e_and_p(
async def filter_by_tag_e_p_and_author(
all_events: List[NostrEvent], author, event_id, reply_event_id
all_events: list[NostrEvent], author, event_id, reply_event_id
):
nostr_filter = NostrFilter(authors=[author])
nostr_filter.p.append(author)

2293
uv.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,4 @@
from http import HTTPStatus
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Request, WebSocket
from lnbits.core.crud import get_user
@ -138,7 +137,7 @@ async def api_get_relay_info() -> JSONResponse:
@nostrrelay_api_router.get("/api/v1/relay/{relay_id}")
async def api_get_relay(
relay_id: str, wallet: WalletTypeInfo = Depends(require_invoice_key)
) -> Optional[NostrRelay]:
) -> NostrRelay | None:
relay = await get_relay(wallet.wallet.user, relay_id)
if not relay:
raise HTTPException(