chore: update pyright to latest and fix new issues (#3978)
This commit is contained in:
+3
-3
@@ -131,15 +131,15 @@ class FiatSubscriptionResponse(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class FiatPaymentSuccessStatus(FiatPaymentStatus):
|
class FiatPaymentSuccessStatus(FiatPaymentStatus):
|
||||||
paid = True
|
paid = True # type: ignore[reportIncompatibleVariableOverride]
|
||||||
|
|
||||||
|
|
||||||
class FiatPaymentFailedStatus(FiatPaymentStatus):
|
class FiatPaymentFailedStatus(FiatPaymentStatus):
|
||||||
paid = False
|
paid = False # type: ignore[reportIncompatibleVariableOverride]
|
||||||
|
|
||||||
|
|
||||||
class FiatPaymentPendingStatus(FiatPaymentStatus):
|
class FiatPaymentPendingStatus(FiatPaymentStatus):
|
||||||
paid = None
|
paid = None # type: ignore[reportIncompatibleVariableOverride]
|
||||||
|
|
||||||
|
|
||||||
class FiatProvider(ABC):
|
class FiatProvider(ABC):
|
||||||
|
|||||||
+3
-3
@@ -1047,7 +1047,7 @@ class EditableSettings(
|
|||||||
|
|
||||||
|
|
||||||
class UpdateSettings(EditableSettings):
|
class UpdateSettings(EditableSettings):
|
||||||
class Config:
|
class Config(EditableSettings.Config):
|
||||||
extra = Extra.forbid
|
extra = Extra.forbid
|
||||||
|
|
||||||
|
|
||||||
@@ -1198,11 +1198,11 @@ class ReadOnlySettings(
|
|||||||
|
|
||||||
|
|
||||||
class Settings(EditableSettings, ReadOnlySettings, TransientSettings, BaseSettings):
|
class Settings(EditableSettings, ReadOnlySettings, TransientSettings, BaseSettings):
|
||||||
class Config:
|
class Config(EditableSettings.Config, BaseSettings.Config): # type: ignore[misc]
|
||||||
env_file = ".env"
|
env_file = ".env"
|
||||||
env_file_encoding = "utf-8"
|
env_file_encoding = "utf-8"
|
||||||
case_sensitive = False
|
case_sensitive = False
|
||||||
json_loads = list_parse_fallback
|
json_loads = list_parse_fallback # type: ignore[assignment]
|
||||||
|
|
||||||
def is_user_allowed(self, user_id: str) -> bool:
|
def is_user_allowed(self, user_id: str) -> bool:
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -94,15 +94,15 @@ class PaymentStatus(NamedTuple):
|
|||||||
|
|
||||||
|
|
||||||
class PaymentSuccessStatus(PaymentStatus):
|
class PaymentSuccessStatus(PaymentStatus):
|
||||||
paid = True
|
paid = True # type: ignore[reportIncompatibleVariableOverride]
|
||||||
|
|
||||||
|
|
||||||
class PaymentFailedStatus(PaymentStatus):
|
class PaymentFailedStatus(PaymentStatus):
|
||||||
paid = False
|
paid = False # type: ignore[reportIncompatibleVariableOverride]
|
||||||
|
|
||||||
|
|
||||||
class PaymentPendingStatus(PaymentStatus):
|
class PaymentPendingStatus(PaymentStatus):
|
||||||
paid = None
|
paid = None # type: ignore[reportIncompatibleVariableOverride]
|
||||||
|
|
||||||
|
|
||||||
class Wallet(ABC):
|
class Wallet(ABC):
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from loguru import logger
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from websockets import Subprotocol, connect
|
from websockets import Subprotocol, connect
|
||||||
|
|
||||||
from lnbits import bolt11
|
from lnbits import bolt11 as bolt11_lib
|
||||||
from lnbits.helpers import normalize_endpoint
|
from lnbits.helpers import normalize_endpoint
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
@@ -164,15 +164,13 @@ class BlinkWallet(Wallet):
|
|||||||
ok=False, error_message=f"Unable to connect to {self.endpoint}."
|
ok=False, error_message=f"Unable to connect to {self.endpoint}."
|
||||||
)
|
)
|
||||||
|
|
||||||
async def pay_invoice(
|
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||||
self, bolt11_invoice: str, fee_limit_msat: int
|
|
||||||
) -> PaymentResponse:
|
|
||||||
# https://dev.blink.sv/api/btc-ln-send
|
# https://dev.blink.sv/api/btc-ln-send
|
||||||
# Future: add check fee estimate is < fee_limit_msat before paying invoice
|
# Future: add check fee estimate is < fee_limit_msat before paying invoice
|
||||||
|
|
||||||
payment_variables = {
|
payment_variables = {
|
||||||
"input": {
|
"input": {
|
||||||
"paymentRequest": bolt11_invoice,
|
"paymentRequest": bolt11,
|
||||||
"walletId": self.wallet_id,
|
"walletId": self.wallet_id,
|
||||||
"memo": "Payment memo",
|
"memo": "Payment memo",
|
||||||
}
|
}
|
||||||
@@ -190,7 +188,7 @@ class BlinkWallet(Wallet):
|
|||||||
error_message = errors[0].get("message")
|
error_message = errors[0].get("message")
|
||||||
return PaymentResponse(ok=False, error_message=error_message)
|
return PaymentResponse(ok=False, error_message=error_message)
|
||||||
|
|
||||||
checking_id = bolt11.decode(bolt11_invoice).payment_hash
|
checking_id = bolt11_lib.decode(bolt11).payment_hash
|
||||||
|
|
||||||
payment_status = await self.get_payment_status(checking_id)
|
payment_status = await self.get_payment_status(checking_id)
|
||||||
fee_msat = payment_status.fee_msat
|
fee_msat = payment_status.fee_msat
|
||||||
@@ -199,7 +197,7 @@ class BlinkWallet(Wallet):
|
|||||||
ok=True, checking_id=checking_id, fee_msat=fee_msat, preimage=preimage
|
ok=True, checking_id=checking_id, fee_msat=fee_msat, preimage=preimage
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.info(f"Failed to pay invoice {bolt11_invoice}")
|
logger.info(f"Failed to pay invoice {bolt11}")
|
||||||
logger.warning(exc)
|
logger.warning(exc)
|
||||||
return PaymentResponse(
|
return PaymentResponse(
|
||||||
error_message=f"Unable to connect to {self.endpoint}."
|
error_message=f"Unable to connect to {self.endpoint}."
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ else:
|
|||||||
|
|
||||||
from bolt11 import Bolt11Exception
|
from bolt11 import Bolt11Exception
|
||||||
from bolt11 import decode as bolt11_decode
|
from bolt11 import decode as bolt11_decode
|
||||||
from breez_sdk import (
|
from breez_sdk import ( # type: ignore[reportMissingImports]
|
||||||
BreezEvent,
|
BreezEvent,
|
||||||
ConnectRequest,
|
ConnectRequest,
|
||||||
EnvironmentType,
|
EnvironmentType,
|
||||||
@@ -39,7 +39,9 @@ else:
|
|||||||
default_config,
|
default_config,
|
||||||
mnemonic_to_seed,
|
mnemonic_to_seed,
|
||||||
)
|
)
|
||||||
from breez_sdk import PaymentStatus as BreezPaymentStatus
|
from breez_sdk import (
|
||||||
|
PaymentStatus as BreezPaymentStatus, # type: ignore[reportMissingImports]
|
||||||
|
)
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ else:
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from bolt11 import decode as bolt11_decode
|
from bolt11 import decode as bolt11_decode
|
||||||
from breez_sdk_liquid import (
|
from breez_sdk_liquid import ( # type: ignore[reportMissingImports]
|
||||||
ConnectRequest,
|
ConnectRequest,
|
||||||
EventListener,
|
EventListener,
|
||||||
GetInfoResponse,
|
GetInfoResponse,
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ class FakeWallet(Wallet):
|
|||||||
preimage=preimage.hex(),
|
preimage=preimage.hex(),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def pay_invoice(self, bolt11: str, _: int) -> PaymentResponse:
|
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
||||||
try:
|
try:
|
||||||
invoice = decode(bolt11)
|
invoice = decode(bolt11)
|
||||||
except Bolt11Exception as exc:
|
except Bolt11Exception as exc:
|
||||||
@@ -130,7 +130,7 @@ class FakeWallet(Wallet):
|
|||||||
return PaymentPendingStatus()
|
return PaymentPendingStatus()
|
||||||
return PaymentFailedStatus()
|
return PaymentFailedStatus()
|
||||||
|
|
||||||
async def get_payment_status(self, _: str) -> PaymentStatus:
|
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||||
return PaymentPendingStatus()
|
return PaymentPendingStatus()
|
||||||
|
|
||||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ class LndWallet(Wallet):
|
|||||||
|
|
||||||
cert = open(cert_path, "rb").read()
|
cert = open(cert_path, "rb").read()
|
||||||
creds = grpc.ssl_channel_credentials(cert)
|
creds = grpc.ssl_channel_credentials(cert)
|
||||||
auth_creds = grpc.metadata_call_credentials(self.metadata_callback)
|
auth_creds = grpc.metadata_call_credentials(self.metadata_callback) # type: ignore[reportArgumentType]
|
||||||
composite_creds = grpc.composite_channel_credentials(creds, auth_creds)
|
composite_creds = grpc.composite_channel_credentials(creds, auth_creds)
|
||||||
channel = grpc.aio.secure_channel(
|
channel = grpc.aio.secure_channel(
|
||||||
f"{self.endpoint}:{self.port}", composite_creds
|
f"{self.endpoint}:{self.port}", composite_creds
|
||||||
|
|||||||
Generated
+8
-5
@@ -24,7 +24,7 @@
|
|||||||
"clean-css-cli": "^5.6.3",
|
"clean-css-cli": "^5.6.3",
|
||||||
"concat": "^1.0.3",
|
"concat": "^1.0.3",
|
||||||
"prettier": "^3.8.3",
|
"prettier": "^3.8.3",
|
||||||
"pyright": "1.1.289",
|
"pyright": "1.1.409",
|
||||||
"sass": "^1.99.0",
|
"sass": "^1.99.0",
|
||||||
"terser": "^5.47.1"
|
"terser": "^5.47.1"
|
||||||
}
|
}
|
||||||
@@ -1808,9 +1808,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pyright": {
|
"node_modules/pyright": {
|
||||||
"version": "1.1.289",
|
"version": "1.1.409",
|
||||||
"resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.289.tgz",
|
"resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.409.tgz",
|
||||||
"integrity": "sha512-fG3STxnwAt3i7bxbXUPJdYNFrcOWHLwCSEOySH2foUqtYdzWLcxDez0Kgl1X8LMQx0arMJ6HRkKghxfRD1/z6g==",
|
"integrity": "sha512-13VFQyw4mJzshZxcxiYbNjo1hG/WHSRDj70Y3lbJEHqCkI2dvBAUTti8VV6Ezsr5gT93pFvC0e/jAQS4JdHarA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -1818,7 +1818,10 @@
|
|||||||
"pyright-langserver": "langserver.index.js"
|
"pyright-langserver": "langserver.index.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.0.0"
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/qrcode.vue": {
|
"node_modules/qrcode.vue": {
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@
|
|||||||
"clean-css-cli": "^5.6.3",
|
"clean-css-cli": "^5.6.3",
|
||||||
"concat": "^1.0.3",
|
"concat": "^1.0.3",
|
||||||
"prettier": "^3.8.3",
|
"prettier": "^3.8.3",
|
||||||
"pyright": "1.1.289",
|
"pyright": "1.1.409",
|
||||||
"sass": "^1.99.0",
|
"sass": "^1.99.0",
|
||||||
"terser": "^5.47.1"
|
"terser": "^5.47.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from typing import cast
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -106,7 +107,7 @@ async def test_lnurl_api_auth_and_pay_flow(mocker):
|
|||||||
await api_perform_lnurlauth(auth_response, wallet_info)
|
await api_perform_lnurlauth(auth_response, wallet_info)
|
||||||
|
|
||||||
action_response = LnurlPayActionResponse(
|
action_response = LnurlPayActionResponse(
|
||||||
pr=LightningInvoice(TEST_BOLT11),
|
pr=cast(LightningInvoice, LightningInvoice(TEST_BOLT11)),
|
||||||
disposable=False,
|
disposable=False,
|
||||||
successAction=parse_obj_as(MessageAction, {"message": "paid"}),
|
successAction=parse_obj_as(MessageAction, {"message": "paid"}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ async def test_payment_api_fee_reserve_and_hold_invoice_actions(mocker):
|
|||||||
wallet.id, CreateInvoice(out=False, amount=42, memo="reserve")
|
wallet.id, CreateInvoice(out=False, amount=42, memo="reserve")
|
||||||
)
|
)
|
||||||
reserve = await api_payments_fee_reserve(invoice.bolt11)
|
reserve = await api_payments_fee_reserve(invoice.bolt11)
|
||||||
assert json.loads(reserve.body)["fee_reserve"] >= 0
|
assert json.loads(bytes(reserve.body))["fee_reserve"] >= 0
|
||||||
|
|
||||||
with pytest.raises(HTTPException, match="Invoice has no amount."):
|
with pytest.raises(HTTPException, match="Invoice has no amount."):
|
||||||
await api_payments_fee_reserve(ZERO_AMOUNT_INVOICE)
|
await api_payments_fee_reserve(ZERO_AMOUNT_INVOICE)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from typing import cast
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -86,7 +87,9 @@ async def test_get_pr_from_lnurl_success_and_error(mocker: MockerFixture):
|
|||||||
mocker.patch(
|
mocker.patch(
|
||||||
"lnbits.core.services.lnurl.execute_pay_request",
|
"lnbits.core.services.lnurl.execute_pay_request",
|
||||||
mocker.AsyncMock(
|
mocker.AsyncMock(
|
||||||
return_value=LnurlPayActionResponse(pr=LightningInvoice(TEST_BOLT11))
|
return_value=LnurlPayActionResponse(
|
||||||
|
pr=cast(LightningInvoice, LightningInvoice(TEST_BOLT11))
|
||||||
|
)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -106,7 +109,7 @@ async def test_fetch_lnurl_pay_request_converts_currency_and_stores_paylink(
|
|||||||
):
|
):
|
||||||
pay_response = make_lnurl_pay_response(min_sendable_msat=1, text="Test")
|
pay_response = make_lnurl_pay_response(min_sendable_msat=1, text="Test")
|
||||||
action_response = LnurlPayActionResponse(
|
action_response = LnurlPayActionResponse(
|
||||||
pr=LightningInvoice(TEST_BOLT11), disposable=False
|
pr=cast(LightningInvoice, LightningInvoice(TEST_BOLT11)), disposable=False
|
||||||
)
|
)
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
"lnbits.core.services.lnurl.fiat_amount_as_satoshis",
|
"lnbits.core.services.lnurl.fiat_amount_as_satoshis",
|
||||||
@@ -143,7 +146,7 @@ async def test_store_paylink_appends_and_updates_existing():
|
|||||||
wallet = await _create_wallet()
|
wallet = await _create_wallet()
|
||||||
pay_response = make_lnurl_pay_response(min_sendable_msat=1, text="Test")
|
pay_response = make_lnurl_pay_response(min_sendable_msat=1, text="Test")
|
||||||
action_response = LnurlPayActionResponse(
|
action_response = LnurlPayActionResponse(
|
||||||
pr=LightningInvoice(TEST_BOLT11), disposable=False
|
pr=cast(LightningInvoice, LightningInvoice(TEST_BOLT11)), disposable=False
|
||||||
)
|
)
|
||||||
|
|
||||||
await store_paylink(
|
await store_paylink(
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from typing import Any
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -66,7 +67,7 @@ async def test_create_user_account_no_check_rejects_duplicate_identity_fields(
|
|||||||
existing = _account(**existing_data)
|
existing = _account(**existing_data)
|
||||||
await create_account(existing)
|
await create_account(existing)
|
||||||
|
|
||||||
resolved = {
|
resolved: dict[str, Any] = {
|
||||||
key: (value(existing) if callable(value) else value)
|
key: (value(existing) if callable(value) else value)
|
||||||
for key, value in new_data.items()
|
for key, value in new_data.items()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from pytest_mock.plugin import MockerFixture
|
from pytest_mock.plugin import MockerFixture
|
||||||
|
|
||||||
@@ -14,22 +16,22 @@ from lnbits.settings import (
|
|||||||
set_cli_settings,
|
set_cli_settings,
|
||||||
)
|
)
|
||||||
|
|
||||||
lnurlp_redirect_path = {
|
lnurlp_redirect_path: dict[str, Any] = {
|
||||||
"from_path": "/.well-known/lnurlp",
|
"from_path": "/.well-known/lnurlp",
|
||||||
"redirect_to_path": "/api/v1/well-known",
|
"redirect_to_path": "/api/v1/well-known",
|
||||||
}
|
}
|
||||||
lnurlp_redirect_path_with_headers = {
|
lnurlp_redirect_path_with_headers: dict[str, Any] = {
|
||||||
"from_path": "/.well-known/lnurlp",
|
"from_path": "/.well-known/lnurlp",
|
||||||
"redirect_to_path": "/api/v1/well-known",
|
"redirect_to_path": "/api/v1/well-known",
|
||||||
"header_filters": {"accept": "application/nostr+json"},
|
"header_filters": {"accept": "application/nostr+json"},
|
||||||
}
|
}
|
||||||
|
|
||||||
lnaddress_redirect_path = {
|
lnaddress_redirect_path: dict[str, Any] = {
|
||||||
"from_path": "/.well-known/lnurlp",
|
"from_path": "/.well-known/lnurlp",
|
||||||
"redirect_to_path": "/api/v1/well-known",
|
"redirect_to_path": "/api/v1/well-known",
|
||||||
}
|
}
|
||||||
|
|
||||||
nostrrelay_redirect_path = {
|
nostrrelay_redirect_path: dict[str, Any] = {
|
||||||
"from_path": "/",
|
"from_path": "/",
|
||||||
"redirect_to_path": "/api/v1/relay-info",
|
"redirect_to_path": "/api/v1/relay-info",
|
||||||
"header_filters": {"accept": "application/nostr+json"},
|
"header_filters": {"accept": "application/nostr+json"},
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -29,7 +30,7 @@ logger.info(f"settings.blink_api_endpoint: {settings.blink_api_endpoint}")
|
|||||||
logger.info(f"settings.blink_token: {settings.blink_token}")
|
logger.info(f"settings.blink_token: {settings.blink_token}")
|
||||||
|
|
||||||
set_funding_source()
|
set_funding_source()
|
||||||
funding_source = get_funding_source()
|
funding_source = cast(BlinkWallet, get_funding_source())
|
||||||
assert isinstance(funding_source, BlinkWallet)
|
assert isinstance(funding_source, BlinkWallet)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import importlib
|
import importlib
|
||||||
|
from typing import Any
|
||||||
from unittest.mock import AsyncMock, Mock
|
from unittest.mock import AsyncMock, Mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -80,7 +81,9 @@ def _check_calls(expected_calls):
|
|||||||
for func_call in func_calls:
|
for func_call in func_calls:
|
||||||
req = func_call["request_data"]
|
req = func_call["request_data"]
|
||||||
args = req["args"] if "args" in req else {}
|
args = req["args"] if "args" in req else {}
|
||||||
kwargs = _eval_dict(req["kwargs"]) if "kwargs" in req else {}
|
kwargs: dict[str, Any] = (
|
||||||
|
_eval_dict(req["kwargs"]) or {} if "kwargs" in req else {}
|
||||||
|
)
|
||||||
|
|
||||||
if "klass" in req:
|
if "klass" in req:
|
||||||
*rest, cls = req["klass"].split(".")
|
*rest, cls = req["klass"].split(".")
|
||||||
@@ -166,7 +169,7 @@ def _mock_field(field):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
def _eval_dict(data: dict | None) -> dict | None:
|
def _eval_dict(data: dict | None) -> dict[str, Any] | None:
|
||||||
fn_prefix = "__eval__:"
|
fn_prefix = "__eval__:"
|
||||||
if not data:
|
if not data:
|
||||||
return data
|
return data
|
||||||
@@ -215,9 +218,9 @@ def _data_mock(data: dict) -> Mock:
|
|||||||
def _raise(error: dict | None):
|
def _raise(error: dict | None):
|
||||||
if not error:
|
if not error:
|
||||||
return Exception()
|
return Exception()
|
||||||
data = error["data"] if "data" in error else None
|
data: dict[str, Any] = error["data"] if "data" in error else {}
|
||||||
if "module" not in error or "class" not in error:
|
if "module" not in error or "class" not in error:
|
||||||
return Exception(data)
|
return Exception(data or None)
|
||||||
|
|
||||||
error_module = importlib.import_module(error["module"])
|
error_module = importlib.import_module(error["module"])
|
||||||
error_class = getattr(error_module, error["class"])
|
error_class = getattr(error_module, error["class"])
|
||||||
|
|||||||
Reference in New Issue
Block a user