satgate 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
satgate-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,121 @@
1
+ Metadata-Version: 2.4
2
+ Name: satgate
3
+ Version: 0.1.0
4
+ Summary: Python SDK for SatGate - Stripe for AI Agents. L402 micropayments for APIs.
5
+ Home-page: https://github.com/SatGate-io/satgate
6
+ Author: SatGate Team
7
+ Author-email: contact@satgate.io
8
+ Project-URL: Homepage, https://satgate.io
9
+ Project-URL: Documentation, https://satgate.io/playground
10
+ Project-URL: Repository, https://github.com/SatGate-io/satgate
11
+ Keywords: l402 lightning bitcoin micropayments api ai agents langchain
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.8
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Internet :: WWW/HTTP
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Requires-Python: >=3.8
24
+ Description-Content-Type: text/markdown
25
+ Requires-Dist: requests>=2.25.0
26
+ Provides-Extra: langchain
27
+ Requires-Dist: langchain>=0.1.0; extra == "langchain"
28
+ Requires-Dist: pydantic>=2.0.0; extra == "langchain"
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest; extra == "dev"
31
+ Requires-Dist: responses; extra == "dev"
32
+ Requires-Dist: flask; extra == "dev"
33
+ Dynamic: author
34
+ Dynamic: author-email
35
+ Dynamic: classifier
36
+ Dynamic: description
37
+ Dynamic: description-content-type
38
+ Dynamic: home-page
39
+ Dynamic: keywords
40
+ Dynamic: project-url
41
+ Dynamic: provides-extra
42
+ Dynamic: requires-dist
43
+ Dynamic: requires-python
44
+ Dynamic: summary
45
+
46
+ # SatGate Python SDK
47
+
48
+ **Stripe for AI Agents** — L402 micropayments for APIs.
49
+
50
+ [![PyPI version](https://badge.fury.io/py/satgate.svg)](https://pypi.org/project/satgate/)
51
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
52
+
53
+ ## Installation
54
+
55
+ ```bash
56
+ pip install satgate
57
+ ```
58
+
59
+ For LangChain integration:
60
+
61
+ ```bash
62
+ pip install satgate[langchain]
63
+ ```
64
+
65
+ ## Quick Start
66
+
67
+ ```python
68
+ from satgate import SatGateSession, LightningWallet
69
+
70
+ # Implement your wallet (or use a library like lndgrpc, pyln-client)
71
+ class MyWallet(LightningWallet):
72
+ def pay_invoice(self, invoice: str) -> str:
73
+ # Pay the invoice and return the preimage
74
+ return your_lightning_node.pay(invoice)
75
+
76
+ # Create a session that auto-pays 402 responses
77
+ session = SatGateSession(wallet=MyWallet())
78
+
79
+ # Use like requests - payments happen automatically!
80
+ response = session.get("https://api.example.com/premium/data")
81
+ print(response.json())
82
+ ```
83
+
84
+ ## LangChain Integration
85
+
86
+ ```python
87
+ from satgate import SatGateTool
88
+
89
+ # Create a tool your AI agent can use
90
+ tool = SatGateTool(wallet=MyWallet())
91
+
92
+ # Add to your LangChain agent
93
+ agent = initialize_agent(
94
+ tools=[tool],
95
+ llm=ChatOpenAI(),
96
+ agent=AgentType.OPENAI_FUNCTIONS
97
+ )
98
+
99
+ # The agent can now access paid APIs automatically!
100
+ agent.run("Fetch premium market data from https://api.example.com/insights")
101
+ ```
102
+
103
+ ## How It Works
104
+
105
+ 1. Your code makes a request to a paid API
106
+ 2. The API returns `402 Payment Required` with a Lightning invoice
107
+ 3. SatGate automatically pays the invoice via your wallet
108
+ 4. The request is retried with the L402 token
109
+ 5. You get your data ✨
110
+
111
+ ## Links
112
+
113
+ - 🌐 Website: [satgate.io](https://satgate.io)
114
+ - 📖 Playground: [satgate.io/playground](https://satgate.io/playground)
115
+ - 💻 GitHub: [github.com/SatGate-io/satgate](https://github.com/SatGate-io/satgate)
116
+ - 📧 Contact: contact@satgate.io
117
+
118
+ ## License
119
+
120
+ MIT License - © 2025 SatGate. Patent Pending.
121
+
@@ -0,0 +1,76 @@
1
+ # SatGate Python SDK
2
+
3
+ **Stripe for AI Agents** — L402 micropayments for APIs.
4
+
5
+ [![PyPI version](https://badge.fury.io/py/satgate.svg)](https://pypi.org/project/satgate/)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ pip install satgate
12
+ ```
13
+
14
+ For LangChain integration:
15
+
16
+ ```bash
17
+ pip install satgate[langchain]
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ```python
23
+ from satgate import SatGateSession, LightningWallet
24
+
25
+ # Implement your wallet (or use a library like lndgrpc, pyln-client)
26
+ class MyWallet(LightningWallet):
27
+ def pay_invoice(self, invoice: str) -> str:
28
+ # Pay the invoice and return the preimage
29
+ return your_lightning_node.pay(invoice)
30
+
31
+ # Create a session that auto-pays 402 responses
32
+ session = SatGateSession(wallet=MyWallet())
33
+
34
+ # Use like requests - payments happen automatically!
35
+ response = session.get("https://api.example.com/premium/data")
36
+ print(response.json())
37
+ ```
38
+
39
+ ## LangChain Integration
40
+
41
+ ```python
42
+ from satgate import SatGateTool
43
+
44
+ # Create a tool your AI agent can use
45
+ tool = SatGateTool(wallet=MyWallet())
46
+
47
+ # Add to your LangChain agent
48
+ agent = initialize_agent(
49
+ tools=[tool],
50
+ llm=ChatOpenAI(),
51
+ agent=AgentType.OPENAI_FUNCTIONS
52
+ )
53
+
54
+ # The agent can now access paid APIs automatically!
55
+ agent.run("Fetch premium market data from https://api.example.com/insights")
56
+ ```
57
+
58
+ ## How It Works
59
+
60
+ 1. Your code makes a request to a paid API
61
+ 2. The API returns `402 Payment Required` with a Lightning invoice
62
+ 3. SatGate automatically pays the invoice via your wallet
63
+ 4. The request is retried with the L402 token
64
+ 5. You get your data ✨
65
+
66
+ ## Links
67
+
68
+ - 🌐 Website: [satgate.io](https://satgate.io)
69
+ - 📖 Playground: [satgate.io/playground](https://satgate.io/playground)
70
+ - 💻 GitHub: [github.com/SatGate-io/satgate](https://github.com/SatGate-io/satgate)
71
+ - 📧 Contact: contact@satgate.io
72
+
73
+ ## License
74
+
75
+ MIT License - © 2025 SatGate. Patent Pending.
76
+
@@ -0,0 +1,4 @@
1
+ from .client import SatGateSession, LightningWallet
2
+
3
+ __all__ = ["SatGateSession", "LightningWallet"]
4
+
@@ -0,0 +1,99 @@
1
+ import requests
2
+ import re
3
+ from abc import ABC, abstractmethod
4
+ from typing import Optional, Dict, Any
5
+
6
+ # --- 1. Wallet Interface (Plug & Play) ---
7
+
8
+ class LightningWallet(ABC):
9
+ """Abstract Base Class for any Lightning Wallet"""
10
+
11
+ @abstractmethod
12
+ def pay_invoice(self, invoice: str) -> str:
13
+ """Pays the invoice and returns the preimage (hex string)"""
14
+ pass
15
+
16
+ # --- 2. The Intelligent Session ---
17
+
18
+ class SatGateSession(requests.Session):
19
+ def __init__(self, wallet: LightningWallet):
20
+ super().__init__()
21
+ self.wallet = wallet
22
+
23
+ def request(self, method: str, url: str, *args, **kwargs) -> requests.Response:
24
+ # 1. Attempt the request normally
25
+ try:
26
+ response = super().request(method, url, *args, **kwargs)
27
+ except requests.exceptions.RequestException as e:
28
+ # If we can't even connect, re-raise
29
+ raise e
30
+
31
+ # 2. Intercept 402 Errors
32
+ if response.status_code == 402:
33
+ return self._handle_payment_flow(response, method, url, *args, **kwargs)
34
+
35
+ return response
36
+
37
+ def _handle_payment_flow(self, response: requests.Response, method: str, url: str, *args, **kwargs) -> requests.Response:
38
+ """The magic loop: Parse -> Pay -> Retry"""
39
+
40
+ # A. Parse the L402 Header
41
+ auth_header = response.headers.get("WWW-Authenticate")
42
+ if not auth_header:
43
+ # Some servers might not send WWW-Authenticate or send it differently
44
+ # Try to see if it's in the body or standard L402
45
+ print("⚠️ 402 received but no WWW-Authenticate header found.")
46
+ return response
47
+
48
+ # Check for L402 or LSAT
49
+ if "L402" not in auth_header and "LSAT" not in auth_header:
50
+ print("⚠️ 402 received but header does not contain L402/LSAT scheme.")
51
+ return response
52
+
53
+ # Extract Invoice and Macaroon (Regex to handle standard L402 format)
54
+ # Example: L402 macaroon="...", invoice="lnbc..."
55
+ # Using non-greedy match for values in quotes
56
+ macaroon_match = re.search(r'macaroon="([^"]+)"', auth_header)
57
+ invoice_match = re.search(r'invoice="([^"]+)"', auth_header)
58
+
59
+ if not macaroon_match or not invoice_match:
60
+ print("❌ Invalid L402 header format: could not find macaroon or invoice.")
61
+ return response
62
+
63
+ macaroon = macaroon_match.group(1)
64
+ invoice = invoice_match.group(1)
65
+
66
+ print(f"⚡ 402 Detected. Price: Unknown (check invoice).")
67
+ print(f" Invoice: {invoice[:20]}...{invoice[-10:]}")
68
+
69
+ # B. Pay the Invoice (User's Wallet Implementation)
70
+ try:
71
+ preimage = self.wallet.pay_invoice(invoice)
72
+ if not preimage:
73
+ raise ValueError("Wallet returned empty preimage")
74
+ print(f"✅ Payment Confirmed. Preimage: {preimage[:10]}...")
75
+ except Exception as e:
76
+ print(f"❌ Payment Failed: {e}")
77
+ return response
78
+
79
+ # C. Retry with Authorization
80
+ # Format: Authorization: L402 <macaroon>:<preimage>
81
+ # Note: Some implementations might use LSAT, but L402 is the standard.
82
+ # We'll use L402 as the prefix.
83
+ l402_token = f"L402 {macaroon}:{preimage}"
84
+
85
+ # Merge headers if they exist, or create new dict
86
+ # We need to be careful not to modify the original kwargs['headers'] in place
87
+ # if it's reused elsewhere, though usually fine here.
88
+ req_headers = kwargs.get("headers", {})
89
+ if req_headers is None:
90
+ req_headers = {}
91
+
92
+ # Copy to avoid side effects
93
+ req_headers = req_headers.copy()
94
+ req_headers["Authorization"] = l402_token
95
+ kwargs["headers"] = req_headers
96
+
97
+ print("🔄 Retrying request with L402 Token...")
98
+ return super().request(method, url, *args, **kwargs)
99
+
@@ -0,0 +1,46 @@
1
+ from typing import Optional, Type, Any
2
+ from langchain.tools import BaseTool
3
+ from pydantic import BaseModel, Field
4
+
5
+ from .client import SatGateSession, LightningWallet
6
+
7
+ class SatGateToolInput(BaseModel):
8
+ endpoint: str = Field(description="The full URL of the premium API endpoint to fetch data from.")
9
+
10
+ class SatGateTool(BaseTool):
11
+ name: str = "satgate_api_browser"
12
+ description: str = (
13
+ "Useful for fetching data from paid/premium APIs that require Lightning Network payments. "
14
+ "Use this tool when you need to access high-value data, reports, or analytics "
15
+ "that are behind a paywall. The tool handles payment automatically."
16
+ )
17
+ args_schema: Type[BaseModel] = SatGateToolInput
18
+
19
+ # We exclude session from Pydantic fields since it's not a model field
20
+ # but an internal component. However, LangChain tools often want fields to be Pydantic compatible.
21
+ # We'll mark it as PrivateAttr or just exclude it from init if possible,
22
+ # but BaseTool inherits from BaseModel.
23
+ # The standard way is to treat it as a private attribute or configured via init.
24
+
25
+ _session: SatGateSession
26
+
27
+ def __init__(self, wallet: LightningWallet, **kwargs):
28
+ super().__init__(**kwargs)
29
+ self._session = SatGateSession(wallet=wallet)
30
+
31
+ def _run(self, endpoint: str) -> str:
32
+ """Synchronous execution"""
33
+ try:
34
+ response = self._session.get(endpoint)
35
+ # Raise error for 4xx/5xx if not handled (though 402 is handled inside)
36
+ response.raise_for_status()
37
+ return response.text
38
+ except Exception as e:
39
+ return f"Error fetching data: {str(e)}"
40
+
41
+ async def _arun(self, endpoint: str) -> str:
42
+ """Async support (Critical for high-performance agents)"""
43
+ # For MVP, we can just wrap the sync call or use aiohttp later
44
+ # Since SatGateSession is sync (requests), we just call _run.
45
+ return self._run(endpoint)
46
+
@@ -0,0 +1,121 @@
1
+ Metadata-Version: 2.4
2
+ Name: satgate
3
+ Version: 0.1.0
4
+ Summary: Python SDK for SatGate - Stripe for AI Agents. L402 micropayments for APIs.
5
+ Home-page: https://github.com/SatGate-io/satgate
6
+ Author: SatGate Team
7
+ Author-email: contact@satgate.io
8
+ Project-URL: Homepage, https://satgate.io
9
+ Project-URL: Documentation, https://satgate.io/playground
10
+ Project-URL: Repository, https://github.com/SatGate-io/satgate
11
+ Keywords: l402 lightning bitcoin micropayments api ai agents langchain
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.8
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Internet :: WWW/HTTP
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Requires-Python: >=3.8
24
+ Description-Content-Type: text/markdown
25
+ Requires-Dist: requests>=2.25.0
26
+ Provides-Extra: langchain
27
+ Requires-Dist: langchain>=0.1.0; extra == "langchain"
28
+ Requires-Dist: pydantic>=2.0.0; extra == "langchain"
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest; extra == "dev"
31
+ Requires-Dist: responses; extra == "dev"
32
+ Requires-Dist: flask; extra == "dev"
33
+ Dynamic: author
34
+ Dynamic: author-email
35
+ Dynamic: classifier
36
+ Dynamic: description
37
+ Dynamic: description-content-type
38
+ Dynamic: home-page
39
+ Dynamic: keywords
40
+ Dynamic: project-url
41
+ Dynamic: provides-extra
42
+ Dynamic: requires-dist
43
+ Dynamic: requires-python
44
+ Dynamic: summary
45
+
46
+ # SatGate Python SDK
47
+
48
+ **Stripe for AI Agents** — L402 micropayments for APIs.
49
+
50
+ [![PyPI version](https://badge.fury.io/py/satgate.svg)](https://pypi.org/project/satgate/)
51
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
52
+
53
+ ## Installation
54
+
55
+ ```bash
56
+ pip install satgate
57
+ ```
58
+
59
+ For LangChain integration:
60
+
61
+ ```bash
62
+ pip install satgate[langchain]
63
+ ```
64
+
65
+ ## Quick Start
66
+
67
+ ```python
68
+ from satgate import SatGateSession, LightningWallet
69
+
70
+ # Implement your wallet (or use a library like lndgrpc, pyln-client)
71
+ class MyWallet(LightningWallet):
72
+ def pay_invoice(self, invoice: str) -> str:
73
+ # Pay the invoice and return the preimage
74
+ return your_lightning_node.pay(invoice)
75
+
76
+ # Create a session that auto-pays 402 responses
77
+ session = SatGateSession(wallet=MyWallet())
78
+
79
+ # Use like requests - payments happen automatically!
80
+ response = session.get("https://api.example.com/premium/data")
81
+ print(response.json())
82
+ ```
83
+
84
+ ## LangChain Integration
85
+
86
+ ```python
87
+ from satgate import SatGateTool
88
+
89
+ # Create a tool your AI agent can use
90
+ tool = SatGateTool(wallet=MyWallet())
91
+
92
+ # Add to your LangChain agent
93
+ agent = initialize_agent(
94
+ tools=[tool],
95
+ llm=ChatOpenAI(),
96
+ agent=AgentType.OPENAI_FUNCTIONS
97
+ )
98
+
99
+ # The agent can now access paid APIs automatically!
100
+ agent.run("Fetch premium market data from https://api.example.com/insights")
101
+ ```
102
+
103
+ ## How It Works
104
+
105
+ 1. Your code makes a request to a paid API
106
+ 2. The API returns `402 Payment Required` with a Lightning invoice
107
+ 3. SatGate automatically pays the invoice via your wallet
108
+ 4. The request is retried with the L402 token
109
+ 5. You get your data ✨
110
+
111
+ ## Links
112
+
113
+ - 🌐 Website: [satgate.io](https://satgate.io)
114
+ - 📖 Playground: [satgate.io/playground](https://satgate.io/playground)
115
+ - 💻 GitHub: [github.com/SatGate-io/satgate](https://github.com/SatGate-io/satgate)
116
+ - 📧 Contact: contact@satgate.io
117
+
118
+ ## License
119
+
120
+ MIT License - © 2025 SatGate. Patent Pending.
121
+
@@ -0,0 +1,11 @@
1
+ README.md
2
+ setup.py
3
+ satgate/__init__.py
4
+ satgate/client.py
5
+ satgate/langchain_integrations.py
6
+ satgate.egg-info/PKG-INFO
7
+ satgate.egg-info/SOURCES.txt
8
+ satgate.egg-info/dependency_links.txt
9
+ satgate.egg-info/requires.txt
10
+ satgate.egg-info/top_level.txt
11
+ tests/test_sdk.py
@@ -0,0 +1,10 @@
1
+ requests>=2.25.0
2
+
3
+ [dev]
4
+ pytest
5
+ responses
6
+ flask
7
+
8
+ [langchain]
9
+ langchain>=0.1.0
10
+ pydantic>=2.0.0
@@ -0,0 +1 @@
1
+ satgate
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
satgate-0.1.0/setup.py ADDED
@@ -0,0 +1,41 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="satgate",
5
+ version="0.1.0",
6
+ description="Python SDK for SatGate - Stripe for AI Agents. L402 micropayments for APIs.",
7
+ long_description=open("README.md").read() if __import__("os").path.exists("README.md") else "",
8
+ long_description_content_type="text/markdown",
9
+ author="SatGate Team",
10
+ author_email="contact@satgate.io",
11
+ url="https://github.com/SatGate-io/satgate",
12
+ project_urls={
13
+ "Homepage": "https://satgate.io",
14
+ "Documentation": "https://satgate.io/playground",
15
+ "Repository": "https://github.com/SatGate-io/satgate",
16
+ },
17
+ packages=find_packages(),
18
+ install_requires=[
19
+ "requests>=2.25.0",
20
+ ],
21
+ extras_require={
22
+ "langchain": ["langchain>=0.1.0", "pydantic>=2.0.0"],
23
+ "dev": ["pytest", "responses", "flask"],
24
+ },
25
+ python_requires=">=3.8",
26
+ classifiers=[
27
+ "Development Status :: 4 - Beta",
28
+ "Intended Audience :: Developers",
29
+ "License :: OSI Approved :: MIT License",
30
+ "Programming Language :: Python :: 3",
31
+ "Programming Language :: Python :: 3.8",
32
+ "Programming Language :: Python :: 3.9",
33
+ "Programming Language :: Python :: 3.10",
34
+ "Programming Language :: Python :: 3.11",
35
+ "Programming Language :: Python :: 3.12",
36
+ "Topic :: Internet :: WWW/HTTP",
37
+ "Topic :: Software Development :: Libraries :: Python Modules",
38
+ ],
39
+ keywords="l402 lightning bitcoin micropayments api ai agents langchain",
40
+ )
41
+
@@ -0,0 +1,107 @@
1
+ import threading
2
+ import time
3
+ import requests
4
+ import unittest
5
+ from http.server import HTTPServer, BaseHTTPRequestHandler
6
+ from satgate.client import SatGateSession, LightningWallet
7
+
8
+ # --- Mock Server ---
9
+
10
+ class MockL402Handler(BaseHTTPRequestHandler):
11
+ protocol_version = 'HTTP/1.1'
12
+
13
+ def do_GET(self):
14
+ # Check for Authorization header
15
+ auth_header = self.headers.get("Authorization")
16
+
17
+ if auth_header and auth_header.startswith("L402"):
18
+ # Validate token (Mock validation)
19
+ # Expected: L402 macaroon:preimage
20
+ token = auth_header.split(" ")[1]
21
+ if ":" in token:
22
+ macaroon, preimage = token.split(":")
23
+ if macaroon == "test_macaroon" and preimage == "test_preimage":
24
+ self.send_response(200)
25
+ self.send_header("Content-type", "application/json")
26
+ self.end_headers()
27
+ self.wfile.write(b'{"status": "success", "data": "premium_content"}')
28
+ return
29
+
30
+ # Default: Return 402
31
+ self.send_response(402)
32
+ self.send_header("WWW-Authenticate", 'L402 macaroon="test_macaroon", invoice="lnbc_test_invoice"')
33
+ self.send_header("Content-type", "text/plain")
34
+ self.end_headers()
35
+ self.wfile.write(b'Payment Required')
36
+
37
+ def log_message(self, format, *args):
38
+ # Silence logs
39
+ pass
40
+
41
+ class MockServer:
42
+ def __init__(self):
43
+ self.server = HTTPServer(('localhost', 0), MockL402Handler)
44
+ self.port = self.server.server_port
45
+ self.thread = threading.Thread(target=self.server.serve_forever)
46
+ self.thread.daemon = True
47
+
48
+ def start(self):
49
+ self.thread.start()
50
+ # Give it a moment to start
51
+ time.sleep(0.1)
52
+
53
+ def stop(self):
54
+ self.server.shutdown()
55
+ self.server.server_close()
56
+
57
+ # --- Mock Wallet ---
58
+
59
+ class MockWallet(LightningWallet):
60
+ def pay_invoice(self, invoice: str) -> str:
61
+ if invoice == "lnbc_test_invoice":
62
+ return "test_preimage"
63
+ raise ValueError("Unknown invoice")
64
+
65
+ # --- Integration Test ---
66
+
67
+ class TestSatGateSDK(unittest.TestCase):
68
+ @classmethod
69
+ def setUpClass(cls):
70
+ cls.server = MockServer()
71
+ cls.server.start()
72
+ cls.base_url = f"http://localhost:{cls.server.port}"
73
+
74
+ @classmethod
75
+ def tearDownClass(cls):
76
+ cls.server.stop()
77
+
78
+ def test_l402_flow(self):
79
+ # 1. Setup Wallet and Session
80
+ wallet = MockWallet()
81
+ session = SatGateSession(wallet=wallet)
82
+
83
+ # 2. Make Request
84
+ print(f"\nTesting request to {self.base_url}...")
85
+ response = session.get(self.base_url)
86
+
87
+ # 3. Verify Result
88
+ self.assertEqual(response.status_code, 200)
89
+ self.assertEqual(response.json(), {"status": "success", "data": "premium_content"})
90
+ print("✅ L402 Flow Verified: 402 -> Pay -> 200")
91
+
92
+ def test_payment_failure(self):
93
+ # Wallet that fails
94
+ class BrokeWallet(LightningWallet):
95
+ def pay_invoice(self, invoice: str) -> str:
96
+ raise Exception("Insufficient funds")
97
+
98
+ session = SatGateSession(wallet=BrokeWallet())
99
+
100
+ # Should return the original 402 response
101
+ response = session.get(self.base_url)
102
+ self.assertEqual(response.status_code, 402)
103
+ print("✅ Failure Handling Verified")
104
+
105
+ if __name__ == "__main__":
106
+ unittest.main()
107
+