feat: code quality (#34)

* feat: code quality
This commit is contained in:
dni ⚡ 2024-08-29 12:18:49 +02:00 committed by GitHub
parent 3df2a56ca2
commit 400b39211d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 2823 additions and 69 deletions

10
.github/workflows/lint.yml vendored Normal file
View file

@ -0,0 +1,10 @@
name: lint
on:
push:
branches:
- main
pull_request:
jobs:
lint:
uses: lnbits/lnbits/.github/workflows/lint.yml@dev

View file

@ -1,10 +1,9 @@
on:
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+"
- 'v[0-9]+.[0-9]+.[0-9]+'
jobs:
release:
runs-on: ubuntu-latest
steps:
@ -34,12 +33,12 @@ jobs:
- name: Create pull request in extensions repo
env:
GH_TOKEN: ${{ secrets.EXT_GITHUB }}
repo_name: "${{ github.event.repository.name }}"
tag: "${{ github.ref_name }}"
branch: "update-${{ github.event.repository.name }}-${{ github.ref_name }}"
title: "[UPDATE] ${{ github.event.repository.name }} to ${{ github.ref_name }}"
body: "https://github.com/lnbits/${{ github.event.repository.name }}/releases/${{ github.ref_name }}"
archive: "https://github.com/lnbits/${{ github.event.repository.name }}/archive/refs/tags/${{ github.ref_name }}.zip"
repo_name: '${{ github.event.repository.name }}'
tag: '${{ github.ref_name }}'
branch: 'update-${{ github.event.repository.name }}-${{ github.ref_name }}'
title: '[UPDATE] ${{ github.event.repository.name }} to ${{ github.ref_name }}'
body: 'https://github.com/lnbits/${{ github.event.repository.name }}/releases/${{ github.ref_name }}'
archive: 'https://github.com/lnbits/${{ github.event.repository.name }}/archive/refs/tags/${{ github.ref_name }}.zip'
run: |
cd lnbits-extensions
git checkout -b $branch

3
.gitignore vendored
View file

@ -1 +1,4 @@
__pycache__
node_modules
.mypy_cache
.venv

12
.prettierrc Normal file
View file

@ -0,0 +1,12 @@
{
"semi": false,
"arrowParens": "avoid",
"insertPragma": false,
"printWidth": 80,
"proseWrap": "preserve",
"singleQuote": true,
"trailingComma": "none",
"useTabs": false,
"bracketSameLine": false,
"bracketSpacing": false
}

47
Makefile Normal file
View file

@ -0,0 +1,47 @@
all: format check
format: prettier black ruff
check: mypy pyright checkblack checkruff checkprettier
prettier:
poetry run ./node_modules/.bin/prettier --write .
pyright:
poetry run ./node_modules/.bin/pyright
mypy:
poetry run mypy .
black:
poetry run black .
ruff:
poetry run ruff check . --fix
checkruff:
poetry run ruff check .
checkprettier:
poetry run ./node_modules/.bin/prettier --check .
checkblack:
poetry run black --check .
checkeditorconfig:
editorconfig-checker
test:
PYTHONUNBUFFERED=1 \
DEBUG=true \
poetry 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
pre-commit:
poetry run pre-commit run --all-files
checkbundle:
@echo "skipping checkbundle"

View file

@ -1,16 +1,16 @@
import asyncio
from loguru import logger
from fastapi import APIRouter
from loguru import logger
from lnbits.db import Database
from lnbits.helpers import template_renderer
from lnbits.tasks import create_permanent_unique_task
db = Database("ext_events")
from .crud import db
from .tasks import wait_for_paid_invoices
from .views import events_generic_router
from .views_api import events_api_router
events_ext: APIRouter = APIRouter(prefix="/events", tags=["Events"])
events_ext.include_router(events_generic_router)
events_ext.include_router(events_api_router)
events_static_files = [
{
@ -19,18 +19,9 @@ events_static_files = [
}
]
def events_renderer():
return template_renderer(["events/templates"])
from .tasks import wait_for_paid_invoices
from .views import * # noqa: F401,F403
from .views_api import * # noqa: F401,F403
scheduled_tasks: list[asyncio.Task] = []
def events_stop():
for task in scheduled_tasks:
try:
@ -38,6 +29,12 @@ def events_stop():
except Exception as ex:
logger.warning(ex)
def events_start():
from lnbits.tasks import create_permanent_unique_task
task = create_permanent_unique_task("ext_events", wait_for_paid_invoices)
scheduled_tasks.append(task)
__all__ = ["db", "events_ext", "events_static_files", "events_start", "events_stop"]

22
crud.py
View file

@ -1,12 +1,12 @@
from typing import List, Optional, Union
from datetime import datetime, timedelta
from typing import List, Optional, Union
from lnbits.db import Database
from lnbits.helpers import urlsafe_short_hash
from . import db
from .models import CreateEvent, Event, Ticket
# TICKETS
db = Database("ext_events")
async def create_ticket(
@ -90,7 +90,8 @@ async def purge_unpaid_tickets(event_id: str) -> None:
time_diff = datetime.now() - timedelta(hours=24)
await db.execute(
f"""
DELETE FROM events.ticket WHERE event = ? AND paid = false AND time < {db.timestamp_placeholder}
DELETE FROM events.ticket WHERE event = ? AND paid = false
AND time < {db.timestamp_placeholder}
""",
(
event_id,
@ -99,14 +100,14 @@ async def purge_unpaid_tickets(event_id: str) -> None:
)
# EVENTS
async def create_event(data: CreateEvent) -> Event:
event_id = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO events.events (id, wallet, name, info, banner, closing_date, event_start_date, event_end_date, currency, amount_tickets, price_per_ticket, sold)
INSERT INTO events.events (
id, wallet, name, info, banner, closing_date, event_start_date,
event_end_date, currency, amount_tickets, price_per_ticket, sold
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
@ -174,7 +175,10 @@ async def get_event_tickets(event_id: str, wallet_id: str) -> List[Ticket]:
async def reg_ticket(ticket_id: str) -> List[Ticket]:
await db.execute(
f"UPDATE events.ticket SET registered = ?, reg_timestamp = {db.timestamp_now} WHERE id = ?",
f"""
UPDATE events.ticket SET registered = ?,
reg_timestamp = {db.timestamp_now} WHERE id = ?
""",
(True, ticket_id),
)
ticket = await db.fetchone("SELECT * FROM events.ticket WHERE id = ?", (ticket_id,))

View file

@ -2,4 +2,4 @@ Sell tickets for events and use the built-in scanner for registering attendants
Events alows you to make tickets for an event. Each ticket is in the form of a uniqque QR code. After registering, and paying for ticket, the user gets a QR code to present at registration/entrance.
Events includes a shareable ticket scanner, which can be used to register attendees.
Events includes a shareable ticket scanner, which can be used to register attendees.

View file

@ -1,6 +1,7 @@
from typing import Optional
from fastapi import Query
from pydantic import BaseModel, EmailStr
from typing import Optional
class CreateEvent(BaseModel):

59
package-lock.json generated Normal file
View file

@ -0,0 +1,59 @@
{
"name": "events",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "events",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"prettier": "^3.2.5",
"pyright": "^1.1.358"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/prettier": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
"integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/pyright": {
"version": "1.1.374",
"resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.374.tgz",
"integrity": "sha512-ISbC1YnYDYrEatoKKjfaA5uFIp0ddC/xw9aSlN/EkmwupXUMVn41Jl+G6wHEjRhC+n4abHZeGpEvxCUus/K9dA==",
"bin": {
"pyright": "index.js",
"pyright-langserver": "langserver.index.js"
},
"engines": {
"node": ">=14.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
}
}
}
}

15
package.json Normal file
View file

@ -0,0 +1,15 @@
{
"name": "events",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"prettier": "^3.2.5",
"pyright": "^1.1.358"
}
}

2492
poetry.lock generated Normal file

File diff suppressed because it is too large Load diff

94
pyproject.toml Normal file
View file

@ -0,0 +1,94 @@
[tool.poetry]
name = "lnbits-events"
version = "0.0.0"
description = "LNbits, free and open-source Lightning wallet and accounts system."
authors = ["Alan Bits <alan@lnbits.com>"]
[tool.poetry.dependencies]
python = "^3.10 | ^3.9"
lnbits = "*"
[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"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.mypy]
exclude = "(nostr/*)"
[[tool.mypy.overrides]]
module = [
"lnbits.*",
"lnurl.*",
"loguru.*",
"fastapi.*",
"pydantic.*",
"pyqrcode.*",
"shortuuid.*",
"httpx.*",
]
ignore_missing_imports = "True"
[tool.pytest.ini_options]
log_cli = false
testpaths = [
"tests"
]
[tool.black]
line-length = 88
[tool.ruff]
# Same as Black. + 10% rule of black
line-length = 88
exclude = [
"nostr",
]
[tool.ruff.lint]
# Enable:
# F - pyflakes
# E - pycodestyle errors
# W - pycodestyle warnings
# I - isort
# A - flake8-builtins
# C - mccabe
# N - naming
# UP - pyupgrade
# RUF - ruff
# B - bugbear
select = ["F", "E", "W", "I", "A", "C", "N", "UP", "RUF", "B"]
ignore = ["C901"]
# Allow autofix for all enabled rules (when `--fix`) is provided.
fixable = ["ALL"]
unfixable = []
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
# needed for pydantic
[tool.ruff.lint.pep8-naming]
classmethod-decorators = [
"root_validator",
]
# Ignore unused imports in __init__.py files.
# [tool.ruff.lint.extend-per-file-ignores]
# "__init__.py" = ["F401", "F403"]
# [tool.ruff.lint.mccabe]
# max-complexity = 10
[tool.ruff.lint.flake8-bugbear]
# Allow default arguments like, e.g., `data: List[str] = fastapi.Query(None)`.
extend-immutable-calls = [
"fastapi.Depends",
"fastapi.Query",
]

0
tests/__init__.py Normal file
View file

11
tests/test_init.py Normal file
View file

@ -0,0 +1,11 @@
import pytest
from fastapi import APIRouter
from .. import events_ext
# just import router and add it to a test router
@pytest.mark.asyncio
async def test_router():
router = APIRouter()
router.include_router(events_ext)

9
toc.md
View file

@ -1,22 +1,29 @@
# Terms and Conditions for LNbits Extension
## 1. Acceptance of Terms
By installing and using the LNbits extension ("Extension"), you agree to be bound by these terms and conditions ("Terms"). If you do not agree to these Terms, do not use the Extension.
## 2. License
The Extension is free and open-source software, released under [specify the FOSS license here, e.g., GPL-3.0, MIT, etc.]. You are permitted to use, copy, modify, and distribute the Extension under the terms of that license.
## 3. No Warranty
The Extension is provided "as is" and with all faults, and the developer expressly disclaims all warranties of any kind, whether express, implied, statutory, or otherwise, including but not limited to warranties of merchantability, fitness for a particular purpose, non-infringement, and any warranties arising out of course of dealing or usage of trade. No advice or information, whether oral or written, obtained from the developer or elsewhere will create any warranty not expressly stated in this Terms.
## 4. Limitation of Liability
In no event will the developer be liable to you or any third party for any direct, indirect, incidental, special, consequential, or punitive damages, including lost profit, lost revenue, loss of data, or other damages arising out of or in connection with your use of the Extension, even if the developer has been advised of the possibility of such damages. The foregoing limitation of liability shall apply to the fullest extent permitted by law in the applicable jurisdiction.
## 5. Modification of Terms
The developer reserves the right to modify these Terms at any time. You are advised to review these Terms periodically for any changes. Changes to these Terms are effective when they are posted on the appropriate location within or associated with the Extension.
## 6. General Provisions
If any provision of these Terms is held to be invalid or unenforceable, that provision will be enforced to the maximum extent permissible, and the other provisions of these Terms will remain in full force and effect. These Terms constitute the entire agreement between you and the developer regarding the use of the Extension.
## 7. Contact Information
If you have any questions about these Terms, please contact the developer at [developer's contact information].
If you have any questions about these Terms, please contact the developer at [developer's contact information].

View file

@ -1,28 +1,32 @@
from datetime import date, datetime
from http import HTTPStatus
from fastapi import Depends, Request
from fastapi import APIRouter, Depends, Request
from fastapi.templating import Jinja2Templates
from lnbits.core.models import User
from lnbits.decorators import check_user_exists
from lnbits.helpers import template_renderer
from starlette.exceptions import HTTPException
from starlette.responses import HTMLResponse
from lnbits.core.models import User
from lnbits.decorators import check_user_exists
from . import events_ext, events_renderer
from .crud import get_event, get_ticket
events_generic_router = APIRouter()
templates = Jinja2Templates(directory="templates")
@events_ext.get("/", response_class=HTMLResponse)
def events_renderer():
return template_renderer(["events/templates"])
@events_generic_router.get("/", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_user_exists)):
return events_renderer().TemplateResponse(
"events/index.html", {"request": request, "user": user.dict()}
)
@events_ext.get("/{event_id}", response_class=HTMLResponse)
@events_generic_router.get("/{event_id}", response_class=HTMLResponse)
async def display(request: Request, event_id):
event = await get_event(event_id)
if not event:
@ -63,7 +67,7 @@ async def display(request: Request, event_id):
)
@events_ext.get("/ticket/{ticket_id}", response_class=HTMLResponse)
@events_generic_router.get("/ticket/{ticket_id}", response_class=HTMLResponse)
async def ticket(request: Request, ticket_id):
ticket = await get_ticket(ticket_id)
if not ticket:
@ -88,7 +92,7 @@ async def ticket(request: Request, ticket_id):
)
@events_ext.get("/register/{event_id}", response_class=HTMLResponse)
@events_generic_router.get("/register/{event_id}", response_class=HTMLResponse)
async def register(request: Request, event_id):
event = await get_event(event_id)
if not event:

View file

@ -1,23 +1,20 @@
from http import HTTPStatus
from fastapi import Depends, Query
from starlette.exceptions import HTTPException
from fastapi import APIRouter, Depends, Query
from lnbits.core.crud import get_standalone_payment, get_user
from lnbits.core.models import WalletTypeInfo
from lnbits.core.services import create_invoice
from lnbits.decorators import (
WalletTypeInfo,
get_key_type,
require_admin_key,
require_invoice_key,
)
from lnbits.utils.exchange_rates import (
currencies,
fiat_amount_as_satoshis,
get_fiat_rate_satoshis,
)
from starlette.exceptions import HTTPException
from . import events_ext
from .crud import (
create_event,
create_ticket,
@ -29,17 +26,17 @@ from .crud import (
get_events,
get_ticket,
get_tickets,
purge_unpaid_tickets,
reg_ticket,
set_ticket_paid,
update_event,
purge_unpaid_tickets,
)
from .models import CreateEvent, CreateTicket
# Events
events_api_router = APIRouter()
@events_ext.get("/api/v1/events")
@events_api_router.get("/api/v1/events")
async def api_events(
all_wallets: bool = Query(False), wallet: WalletTypeInfo = Depends(get_key_type)
):
@ -52,8 +49,8 @@ async def api_events(
return [event.dict() for event in await get_events(wallet_ids)]
@events_ext.post("/api/v1/events")
@events_ext.put("/api/v1/events/{event_id}")
@events_api_router.post("/api/v1/events")
@events_api_router.put("/api/v1/events/{event_id}")
async def api_event_create(
data: CreateEvent,
event_id=None,
@ -77,7 +74,7 @@ async def api_event_create(
return event.dict()
@events_ext.delete("/api/v1/events/{event_id}")
@events_api_router.delete("/api/v1/events/{event_id}")
async def api_form_delete(
event_id, wallet: WalletTypeInfo = Depends(require_admin_key)
):
@ -98,7 +95,7 @@ async def api_form_delete(
#########Tickets##########
@events_ext.get("/api/v1/tickets")
@events_api_router.get("/api/v1/tickets")
async def api_tickets(
all_wallets: bool = Query(False), wallet: WalletTypeInfo = Depends(get_key_type)
):
@ -111,14 +108,14 @@ async def api_tickets(
return [ticket.dict() for ticket in await get_tickets(wallet_ids)]
@events_ext.post("/api/v1/tickets/{event_id}")
@events_api_router.post("/api/v1/tickets/{event_id}")
async def api_ticket_create(event_id: str, data: CreateTicket):
name = data.name
email = data.email
return await api_ticket_make_ticket(event_id, name, email)
@events_ext.get("/api/v1/tickets/{event_id}/{name}/{email}")
@events_api_router.get("/api/v1/tickets/{event_id}/{name}/{email}")
async def api_ticket_make_ticket(event_id, name, email):
event = await get_event(event_id)
if not event:
@ -151,12 +148,14 @@ async def api_ticket_make_ticket(event_id, name, email):
name=name,
email=email,
)
except Exception as e:
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
except Exception as exc:
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(exc)
) from exc
return {"payment_hash": payment_hash, "payment_request": payment_request}
@events_ext.post("/api/v1/tickets/{event_id}/{payment_hash}")
@events_api_router.post("/api/v1/tickets/{event_id}/{payment_hash}")
async def api_ticket_send_ticket(event_id, payment_hash):
event = await get_event(event_id)
if not event:
@ -189,7 +188,7 @@ async def api_ticket_send_ticket(event_id, payment_hash):
return {"paid": False}
@events_ext.delete("/api/v1/tickets/{ticket_id}")
@events_api_router.delete("/api/v1/tickets/{ticket_id}")
async def api_ticket_delete(ticket_id, wallet: WalletTypeInfo = Depends(get_key_type)):
ticket = await get_ticket(ticket_id)
if not ticket:
@ -204,7 +203,7 @@ async def api_ticket_delete(ticket_id, wallet: WalletTypeInfo = Depends(get_key_
return "", HTTPStatus.NO_CONTENT
@events_ext.get("/api/v1/purge/{event_id}")
@events_api_router.get("/api/v1/purge/{event_id}")
async def api_event_purge_tickets(event_id):
event = await get_event(event_id)
if not event:
@ -217,7 +216,7 @@ async def api_event_purge_tickets(event_id):
# Event Tickets
@events_ext.get("/api/v1/eventtickets/{wallet_id}/{event_id}")
@events_api_router.get("/api/v1/eventtickets/{wallet_id}/{event_id}")
async def api_event_tickets(wallet_id, event_id):
return [
ticket.dict()
@ -225,7 +224,7 @@ async def api_event_tickets(wallet_id, event_id):
]
@events_ext.get("/api/v1/register/ticket/{ticket_id}")
@events_api_router.get("/api/v1/register/ticket/{ticket_id}")
async def api_event_register_ticket(ticket_id):
ticket = await get_ticket(ticket_id)
@ -247,6 +246,6 @@ async def api_event_register_ticket(ticket_id):
return [ticket.dict() for ticket in await reg_ticket(ticket_id)]
@events_ext.get("/api/v1/currencies")
@events_api_router.get("/api/v1/currencies")
async def api_list_currencies_available():
return list(currencies.keys())