Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 7 additions & 10 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,16 @@ jobs:
id-token: write
steps:
- name: "Checkout"
uses: "actions/checkout@v3"
uses: "actions/checkout@v5"
with:
fetch-depth: 10
- name: "Setup Python"
uses: "actions/setup-python@v4"
- name: "Set up Python"
uses: "actions/setup-python@v5"
- name: "Install uv"
uses: "astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57" # v8.0.0
with:
cache: "pipenv"
cache-dependency-path: "sdk/Pipfile.lock"
- name: "Install pipenv"
run: "pip install pipenv wheel"
- name: "Install dependencies"
run: "rm -rf $(pipenv --venv) && pipenv install --dev"
version: "0.11.2"
- name: "Build dist"
run: "pipenv run python setup.py sdist --format=zip"
run: "uv run --frozen python setup.py sdist --format=zip"
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
35 changes: 17 additions & 18 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,30 @@ jobs:
runs-on: "ubuntu-22.04"
strategy:
matrix:
python_version: ["3.8", "3.10"]
python_version: ["3.8", "3.10", "3.12"]
steps:
- name: "Checkout"
uses: "actions/checkout@v3"
uses: "actions/checkout@v5"
with:
fetch-depth: 10
- name: "Setup Python"
uses: "actions/setup-python@v4"
- name: "Set up Python"
uses: "actions/setup-python@v5"
with:
python-version: "${{ matrix.python_version }}"
cache: "pipenv"
cache-dependency-path: "Pipfile.lock"
- name: "Install pipenv"
run: "pip install pipenv wheel"
- name: "Install dependencies"
run: "rm -rf $(pipenv --venv) && pipenv --python ${{ matrix.python_version }} install --dev"
- name: "Run pyre"
run: |
set -o pipefail
pipenv run pyre | tee >(sed 's, ,:,' | awk -F: '{sub(" ", "", $5); print "::error file=" ENVIRON["PWD"] "/" $1 ",line=" $2 ",col=" $3 ",title=" $4 "::" $5}')
- name: "Install uv and set the python version"
uses: "astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57" # v8.0.0
with:
python-version: "${{ matrix.python-version }}"
version: "0.11.2"
working-directory: "lightspark"
- name: "Run ty"
working-directory: "lightspark"
run: "uv run ty check --output-format github ."
- name: "Run pytest"
run: "PYTHONPATH=. pipenv run pytest"
run: "PYTHONPATH=. uv run pytest"
- name: "Run pylint"
run: "PYTHONPATH=. pipenv run pylint --jobs 0 --score n --msg-template='::{category} file={abspath},line={line},col={column},title={msg_id} {symbol}::{msg}' lightspark"
- name: "Run black"
run: "PYTHONPATH=. uv run pylint --jobs 0 --score n --msg-template='::{category} file={abspath},line={line},col={column},title={msg_id} {symbol}::{msg}' lightspark"
- name: "Run ruff format"
run: |
set -o pipefail
pipenv run black --check --diff . | tee >(pipenv run ../scripts/diff2annotation.py)
uv run --frozen ruff format --diff . | tee >(uv run --frozen ./scripts/diff2annotation.py)
3 changes: 3 additions & 0 deletions .pyre_configuration
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,8 @@
"site_package_search_strategy": "pep561",
"source_directories": [
"."
],
"ignore_all_errors": [
".venv/**"
]
}
23 changes: 0 additions & 23 deletions Pipfile

This file was deleted.

874 changes: 0 additions & 874 deletions Pipfile.lock

This file was deleted.

16 changes: 7 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ The Lightspark Python SDK provides a convenient way to interact with the Lightsp

To install the SDK, simply run:

```bash
```shell
pip install lightspark
```

Expand All @@ -19,16 +19,14 @@ The documentation for this SDK (installation, usage, etc.) is available at https
For your convenience, we included an example that shows you how to use the SDK.
Open the file `example.py` and make sure to update the variables at the top of the page with your information, then run it using pipenv:

```python
pipenv install
pipenv run python -m examples.example
```shell
uv sync
uv run python -m examples.example
```

There are also a few examples of webservers for demonstrating webhooks and LNURLs. These can similarly be run through Flask:

```python
pipenv install -d
pipenv run flask --app examples.flask_lnurl_server run
```shell
uv sync --dev
uv run flask --app examples.flask_lnurl_server run
```

Note that Flask requires Python >= 3.8, so these examples will not work if running Python 3.7.
6 changes: 3 additions & 3 deletions examples/flask_lnurl_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@
##
## export LIGHTSPARK_API_TOKEN_CLIENT_ID=<client_id>
## export LIGHTSPARK_API_TOKEN_CLIENT_SECRET=<client_secret>
API_TOKEN_CLIENT_ID = os.environ.get("LIGHTSPARK_API_TOKEN_CLIENT_ID")
API_TOKEN_CLIENT_SECRET = os.environ.get("LIGHTSPARK_API_TOKEN_CLIENT_SECRET")
API_TOKEN_CLIENT_ID = os.environ["LIGHTSPARK_API_TOKEN_CLIENT_ID"]
API_TOKEN_CLIENT_SECRET = os.environ["LIGHTSPARK_API_TOKEN_CLIENT_SECRET"]
##
## This example also assumes you already know your node UUID. Generally, an LNURL API would serve
## many different usernames while maintaining some internal mapping from username to node UUID. For
## simplicity, this example works with a single username and node UUID.
##
## export LIGHTSPARK_LNURL_NODE_UUID=0187c4d6-704b-f96b-0000-a2e8145bc1f9
LNURL_NODE_UUID = os.environ.get("LIGHTSPARK_LNURL_NODE_UUID")
LNURL_NODE_UUID = os.environ["LIGHTSPARK_LNURL_NODE_UUID"]
LNURL_USERNAME = os.environ.get("LIGHTSPARK_LNURL_USERNAME", "ls_test")
##
## To run the webserver, run this command from the root of the SDK folder:
Expand Down
18 changes: 9 additions & 9 deletions examples/flask_remote_signing_server.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Copyright ©, 2022-present, Lightspark Group, Inc. - All Rights Reserved
"""Sample Lightspark Webhooks Flask App.

Install Flask (pip install flask) and then run this like:
Install Flask (uv add flask) and then run this like:

pipenv run flask --app examples.flask_remote_signin_server run --port 5001
uv run flask --app examples.flask_remote_signin_server run --port 5001
"""

import os
Expand All @@ -13,22 +13,22 @@
app = Flask(__name__)

# Get this from the API Configuration page.
webhook_secret = os.environ.get("RK_WEBHOOK_SECRET")
webhook_secret = os.environ["RK_WEBHOOK_SECRET"]

api_token_client_id = os.environ.get("RK_API_CLIENT_ID")
api_token_client_secret = os.environ.get("RK_API_CLIENT_SECRET")
api_token_client_id = os.environ["RK_API_CLIENT_ID"]
api_token_client_secret = os.environ["RK_API_CLIENT_SECRET"]

master_seed = os.environ.get("RK_MASTER_SEED_HEX")
master_seed = os.environ["RK_MASTER_SEED_HEX"]


@app.route("/ping", methods=["GET"])
def ping():
def ping() -> str:
print("ping")
return "OK"


@app.route("/lightspark-webhook", methods=["POST"])
def webhook():
def webhook() -> str:
client = lightspark.LightsparkSyncClient(
api_token_client_id=api_token_client_id,
api_token_client_secret=api_token_client_secret,
Expand All @@ -40,6 +40,6 @@ def webhook():
)

handler.handle_remote_signing_webhook_request(
request.data, request.headers.get(lightspark.SIGNATURE_HEADER), webhook_secret
request.data, request.headers[lightspark.SIGNATURE_HEADER], webhook_secret
)
return "OK"
32 changes: 0 additions & 32 deletions examples/flask_webhook_server.py

This file was deleted.

18 changes: 10 additions & 8 deletions lightspark/__tests__/test_serialization.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
from unittest.mock import MagicMock
import json
from lightspark.objects.InvoiceData import from_json as InvoiceData_from_json
from lightspark.objects.Node import from_json as Node_from_json
from lightspark.objects import InvoiceData, Node


class TestSerialization:
def test_serialize_deserialize_invoice_data(self):
def test_serialize_deserialize_invoice_data(self) -> None:
requester = MagicMock()
serialized = '{"__typename": "InvoiceData", "invoice_data_encoded_payment_request":"lnbcrt34170n1pj5vdn4pp56jhw0672v566u4rvl333v8hwwuvavvu9gx4a2mqag4pkrvm0hwkqhp5xaz278y6cejcvpqnndl4wfq3slgthjduwlfksg778aevn23v2pdscqzpgxqyz5vqsp5ee5jezfvjqvvz7hfwta3ekk8hs6dq36szkgp40qh7twa8upquxlq9qyyssqjg2slc95falxf2t67y0wu2w43qwfcvfflwl8tn4ppqw9tumwqxk36qkfct9p2w8c3yy2ld7c6nacy4ssv2gl6qyqfpmhl4jmarnjf8cpvjlxek","invoice_data_bitcoin_network":"REGTEST","invoice_data_payment_hash":"d4aee7ebca6535ae546cfc63161eee7719d6338541abd56c1d454361b36fbbac","invoice_data_amount":{"currency_amount_original_value":3417,"currency_amount_original_unit":"SATOSHI","currency_amount_preferred_currency_unit":"USD","currency_amount_preferred_currency_value_rounded":118,"currency_amount_preferred_currency_value_approx":118.89352818371607},"invoice_data_created_at":"2023-11-04T12:17:57+00:00","invoice_data_expires_at":"2023-11-05T12:17:57+00:00","invoice_data_memo":null,"invoice_data_destination":{"graph_node_id":"GraphNode:0189a572-6dba-cf00-0000-ac0908d34ea6","graph_node_created_at":"2023-07-30T06:18:07.162759+00:00","graph_node_updated_at":"2023-11-04T12:01:04.015414+00:00","graph_node_alias":"ls_test_vSViIQitob_SE","graph_node_bitcoin_network":"REGTEST","graph_node_color":"#3399ff","graph_node_conductivity":null,"graph_node_display_name":"ls_test_vSViIQitob_SE","graph_node_public_key":"02253935a5703a6f0429081e08d2defce0faa15f4d75305302284751d53a4e0608", "__typename":"GraphNode"}}'
deserialized = InvoiceData_from_json(None, json.loads(serialized))
deserialized = InvoiceData.from_json(requester, json.loads(serialized))
reserialized = deserialized.to_json()
assert reserialized == json.loads(serialized)

deserialized_again = InvoiceData_from_json(None, reserialized)
deserialized_again = InvoiceData.from_json(requester, reserialized)
assert deserialized_again == deserialized

def test_serialize_deserialize_graph_node(self):
def test_serialize_deserialize_graph_node(self) -> None:
requester = MagicMock()
serialized = '{"graph_node_id":"GraphNode:0189a572-6dba-cf00-0000-ac0908d34ea6","graph_node_created_at":"2023-07-30T06:18:07.162759+00:00","graph_node_updated_at":"2023-11-04T12:01:04.015414+00:00","graph_node_alias":"ls_test_vSViIQitob_SE","graph_node_bitcoin_network":"REGTEST","graph_node_color":"#3399ff","graph_node_conductivity":null,"graph_node_display_name":"ls_test_vSViIQitob_SE","graph_node_public_key":"02253935a5703a6f0429081e08d2defce0faa15f4d75305302284751d53a4e0608", "__typename":"GraphNode"}'
deserialized = Node_from_json(None, json.loads(serialized))
deserialized = Node.from_json(requester, json.loads(serialized))
reserialized = deserialized.to_json()
assert reserialized == json.loads(serialized)

deserialized_again = Node_from_json(None, reserialized)
deserialized_again = Node.from_json(requester, reserialized)
assert deserialized_again == deserialized
7 changes: 4 additions & 3 deletions lightspark/remote_signing.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from typing import Any
from dataclasses import dataclass
import json
import lightspark_crypto as lsc # pyre-ignore[21]
import lightspark_crypto as lsc
import lightspark


class PositiveValidator(lsc.Validation): # pyre-ignore[11]
class PositiveValidator(lsc.Validation):
@staticmethod
def should_sign(webhook):
def should_sign(webhook: Any) -> bool: # ty:ignore[invalid-method-override]
return True


Expand Down
16 changes: 11 additions & 5 deletions lightspark/requests/requester.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Copyright ©, 2022-present, Lightspark Group, Inc. - All Rights Reserved
from __future__ import annotations

import json
import logging
Expand All @@ -10,12 +11,15 @@
from typing import Any, Mapping, Optional
from urllib.parse import urlparse

from urllib3.connectionpool import ConnectionPool

try:
from zstandard import ZstdCompressor
except ImportError:
ZstdCompressor = None
ZstdCompressor: None = None

import requests
import requests.adapters
from requests.auth import HTTPBasicAuth
from requests.utils import default_user_agent

Expand Down Expand Up @@ -61,7 +65,7 @@ def execute_graphql(
"variables": variables or {},
"nonce": secrets.randbits(64) if signing_key else None,
"expires_at": (
(datetime.utcnow() + timedelta(hours=1))
(datetime.now(timezone.utc) + timedelta(hours=1))
.replace(tzinfo=timezone.utc)
.isoformat()
if signing_key
Expand All @@ -82,7 +86,7 @@ def execute_graphql(
"Content-Type": "application/json",
"X-GraphQL-Operation": operation.group(1) if operation else None,
"X-Lightspark-Signing": signing,
"User-Agent": user_agent + f" {default_user_agent()}",
"User-Agent": f"{user_agent} {default_user_agent()}",
"X-Lightspark-SDK": user_agent,
}
if len(payload) > 1024:
Expand Down Expand Up @@ -120,7 +124,7 @@ def execute_graphql(
pass
raise e

def user_agent_string(self):
def user_agent_string(self) -> str:
# Will produce something like: lightspark-python-sdk/0.5.1 python/3.11.2 Darwin/22.1.0
return f"lightspark-python-sdk/{__version__} python/{python_version()} {system()}/{release()}"

Expand All @@ -130,7 +134,9 @@ def __init__(self, server_hostname: str, *args, **kwargs):
self.server_hostname = server_hostname
super().__init__(*args, **kwargs)

def get_connection(self, url, proxies=None):
def get_connection(
self, url: str, proxies: Mapping[str, str] | None = None
) -> ConnectionPool:
url = urlparse(url).geturl()
return self.poolmanager.connection_from_url(
url, pool_kwargs={"server_hostname": self.server_hostname}
Expand Down
28 changes: 28 additions & 0 deletions lightspark/scripts/diff2annotation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env python
# Copyright © 2022-present, Lightspark Group, Inc. - All Rights Reserved
from os import getcwd, path
from sys import stdin

from unidiff import PatchSet


def escape(s):
return s.replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A")


patchset = PatchSet.from_string(stdin.read())


for patch in patchset:
for hunk in patch:
context = [line.is_context for line in hunk]
before_context = context.index(False)
context.reverse()
after_context = len(context) - context.index(False)
diff = escape("".join(str(line) for line in hunk[before_context:after_context]))
start_line = hunk.source_start + before_context
end_line = hunk.source_start + hunk.source_length - context.index(False) - 1
filename = path.join(getcwd(), patch.source_file)
print(
f"::notice file={filename},line={start_line},endLine={end_line},title=Python formatting::{diff}"
)
Loading
Loading