fype-sdk-beta 1.0.0__py3-none-any.whl

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.
fype/__init__.py ADDED
@@ -0,0 +1,17 @@
1
+ from fype.client import Fype
2
+ from fype.errors import (
3
+ ApiConnectionError,
4
+ AuthenticationError,
5
+ FypeError,
6
+ InvalidRequestError,
7
+ SignatureVerificationError,
8
+ )
9
+
10
+ __all__ = [
11
+ "ApiConnectionError",
12
+ "AuthenticationError",
13
+ "Fype",
14
+ "FypeError",
15
+ "InvalidRequestError",
16
+ "SignatureVerificationError",
17
+ ]
fype/client.py ADDED
@@ -0,0 +1,110 @@
1
+ from typing import Any
2
+
3
+ import httpx
4
+
5
+ from fype.errors import ApiConnectionError, AuthenticationError, FypeError, InvalidRequestError
6
+ from fype.resources import (
7
+ EntitlementsResource,
8
+ PaymentsResource,
9
+ PlansResource,
10
+ RefundsResource,
11
+ SubscriptionsResource,
12
+ UsageResource,
13
+ WebhooksResource,
14
+ )
15
+
16
+
17
+ class Fype:
18
+ """
19
+ The official Fype Python client SDK.
20
+ Provides type-safe access to payments, refunds, and webhook verification resources.
21
+ """
22
+
23
+ def __init__(self, api_key: str, base_url: str = "http://localhost:8000/v1"):
24
+ if not api_key:
25
+ raise ValueError("Fype API Key must be supplied.")
26
+
27
+ self.api_key = api_key
28
+ # Enforce clean suffix formatting on base url
29
+ self.base_url = base_url.rstrip("/")
30
+
31
+ # Initialize resources
32
+ self.payments = PaymentsResource(self)
33
+ self.refunds = RefundsResource(self)
34
+ self.webhooks = WebhooksResource()
35
+ self.subscriptions = SubscriptionsResource(self)
36
+ self.plans = PlansResource(self)
37
+ self.entitlements = EntitlementsResource(self)
38
+ self.usage = UsageResource(self)
39
+
40
+ def _request(
41
+ self,
42
+ method: str,
43
+ path: str,
44
+ params: dict[str, Any] | None = None,
45
+ json: dict[str, Any] | None = None,
46
+ headers: dict[str, str] | None = None,
47
+ ) -> Any:
48
+ """
49
+ Helper handler that signs and executes HTTP request and
50
+ translates responses/exceptions.
51
+ """
52
+ url = f"{self.base_url}{path}"
53
+
54
+ request_headers = {
55
+ "Authorization": f"Bearer {self.api_key}",
56
+ "Content-Type": "application/json",
57
+ "User-Agent": "Fype-PythonSDK/v1.0",
58
+ }
59
+ if headers:
60
+ request_headers.update(headers)
61
+
62
+ try:
63
+ with httpx.Client(timeout=15.0) as client:
64
+ response = client.request(
65
+ method=method,
66
+ url=url,
67
+ params=params,
68
+ json=json,
69
+ headers=request_headers,
70
+ )
71
+
72
+ # Check for client and server error codes
73
+ if 400 <= response.status_code < 600:
74
+ self._handle_error_response(response)
75
+
76
+ return response.json()
77
+ except httpx.RequestError as e:
78
+ raise ApiConnectionError(
79
+ f"Fype connection error: {e!s}",
80
+ ) from e
81
+
82
+ def _handle_error_response(self, response: httpx.Response):
83
+ """Map HTTP error status codes to dedicated Python exception classes."""
84
+ status_code = response.status_code
85
+ text = response.text
86
+
87
+ try:
88
+ error_data = response.json()
89
+ message = error_data.get("message") or error_data.get("detail") or text
90
+ except Exception:
91
+ message = text
92
+
93
+ if status_code == 401:
94
+ raise AuthenticationError(
95
+ f"Authentication failed (API Key is invalid or expired): {message}",
96
+ status_code,
97
+ text,
98
+ )
99
+ elif status_code in (400, 422, 404, 409):
100
+ raise InvalidRequestError(
101
+ f"Invalid request parameters: {message}",
102
+ status_code,
103
+ text,
104
+ )
105
+ else:
106
+ raise FypeError(
107
+ f"Fype server error (HTTP {status_code}): {message}",
108
+ status_code,
109
+ text,
110
+ )
fype/errors.py ADDED
@@ -0,0 +1,37 @@
1
+ class FypeError(Exception):
2
+ """Base error class for all Fype SDK exceptions."""
3
+
4
+ def __init__(
5
+ self,
6
+ message: str,
7
+ status_code: int | None = None,
8
+ response_body: str | None = None,
9
+ ):
10
+ super().__init__(message)
11
+ self.message = message
12
+ self.status_code = status_code
13
+ self.response_body = response_body
14
+
15
+
16
+ class AuthenticationError(FypeError):
17
+ """Exception raised when API authentication fails."""
18
+
19
+ pass
20
+
21
+
22
+ class InvalidRequestError(FypeError):
23
+ """Exception raised when the request contains invalid parameters."""
24
+
25
+ pass
26
+
27
+
28
+ class ApiConnectionError(FypeError):
29
+ """Exception raised when communication with the Fype servers fails."""
30
+
31
+ pass
32
+
33
+
34
+ class SignatureVerificationError(FypeError):
35
+ """Exception raised when webhook signature verification fails."""
36
+
37
+ pass
fype/resources.py ADDED
@@ -0,0 +1,234 @@
1
+ import hashlib
2
+ import hmac
3
+ from typing import Any
4
+
5
+
6
+ class PaymentsResource:
7
+ """Helper resource class for creating, fetching, and listing payments."""
8
+
9
+ def __init__(self, client):
10
+ self._client = client
11
+
12
+ def create(
13
+ self,
14
+ amount: int,
15
+ currency: str,
16
+ customer_email: str,
17
+ success_url: str,
18
+ cancel_url: str,
19
+ reference_id: str | None = None,
20
+ metadata: dict[str, Any] | None = None,
21
+ ) -> dict[str, Any]:
22
+ """
23
+ Create a new payment order.
24
+ Returns the created Payment dictionary containing visual checkout_url.
25
+ """
26
+ payload = {
27
+ "amount": amount,
28
+ "currency": currency,
29
+ "customer_email": customer_email,
30
+ "success_url": success_url,
31
+ "cancel_url": cancel_url,
32
+ }
33
+ if reference_id is not None:
34
+ payload["reference_id"] = reference_id
35
+ if metadata is not None:
36
+ payload["metadata"] = metadata
37
+
38
+ return self._client._request("POST", "/payments", json=payload)
39
+
40
+ def retrieve(self, payment_id: str) -> dict[str, Any]:
41
+ """Retrieve details for a specific Fype payment ID."""
42
+ return self._client._request("GET", f"/payments/{payment_id}")
43
+
44
+ def list(self, limit: int = 20, offset: int = 0) -> list[dict[str, Any]]:
45
+ """List paginated payments (separate test/live lists based on key authorization)."""
46
+ params = {"limit": limit, "offset": offset}
47
+ return self._client._request("GET", "/payments", params=params)
48
+
49
+
50
+ class RefundsResource:
51
+ """Helper resource class for executing refunds."""
52
+
53
+ def __init__(self, client):
54
+ self._client = client
55
+
56
+ def create(self, payment_id: str, amount: int | None = None) -> dict[str, Any]:
57
+ """
58
+ Initiate a refund for a successful payment.
59
+ If amount is not specified, performs a full refund.
60
+ """
61
+ payload = {}
62
+ if amount is not None:
63
+ payload["amount"] = amount
64
+ return self._client._request("POST", f"/payments/{payment_id}/refund", json=payload)
65
+
66
+
67
+ class WebhooksResource:
68
+ """Helper utility for webhook validation and parsing."""
69
+
70
+ @staticmethod
71
+ def verify_signature(raw_payload: bytes, signature_header: str, secret: str) -> bool:
72
+ """
73
+ Verify incoming webhook X-Fype-Signature header against the configured webhook secret.
74
+ Returns True if signature is valid, raises SignatureVerificationError otherwise.
75
+ """
76
+ from fype.errors import SignatureVerificationError
77
+
78
+ if not signature_header:
79
+ raise SignatureVerificationError("Webhook signature header is missing.")
80
+
81
+ mac = hmac.new(
82
+ secret.encode("utf-8"),
83
+ msg=raw_payload,
84
+ digestmod=hashlib.sha256,
85
+ )
86
+ expected_signature = mac.hexdigest()
87
+
88
+ if not hmac.compare_digest(expected_signature, signature_header):
89
+ raise SignatureVerificationError("Webhook signature verification failed.")
90
+
91
+ return True
92
+
93
+
94
+ class SubscriptionsResource:
95
+ """Helper resource class for managing subscriptions."""
96
+
97
+ def __init__(self, client):
98
+ self._client = client
99
+
100
+ def create(
101
+ self,
102
+ customer_id: str,
103
+ plan_id: str,
104
+ price_id: str,
105
+ quantity: int = 1,
106
+ metadata: dict[str, Any] | None = None,
107
+ ) -> dict[str, Any]:
108
+ """Creates a subscription and returns its checkout session or mandate details."""
109
+ payload = {
110
+ "customer_id": customer_id,
111
+ "plan_id": plan_id,
112
+ "price_id": price_id,
113
+ "quantity": quantity,
114
+ }
115
+ if metadata is not None:
116
+ payload["metadata"] = metadata
117
+ return self._client._request("POST", "/subscriptions", json=payload)
118
+
119
+ def retrieve(self, subscription_id: str) -> dict[str, Any]:
120
+ """Retrieve details for a specific Fype subscription ID."""
121
+ return self._client._request("GET", f"/subscriptions/{subscription_id}")
122
+
123
+ def list(
124
+ self,
125
+ limit: int = 20,
126
+ cursor: str | None = None,
127
+ customer_id: str | None = None,
128
+ ) -> dict[str, Any]:
129
+ """List paginated subscriptions for the merchant, optionally filtered by customer_id."""
130
+ params: dict[str, Any] = {"limit": limit}
131
+ if cursor is not None:
132
+ params["cursor"] = cursor
133
+ if customer_id is not None:
134
+ params["customer_id"] = customer_id
135
+ return self._client._request("GET", "/subscriptions", params=params)
136
+
137
+ def cancel(self, subscription_id: str, cancel_at_period_end: bool = False) -> dict[str, Any]:
138
+ """Cancels a subscription immediately or at the end of the current period."""
139
+ params = {"cancel_at_period_end": str(cancel_at_period_end).lower()}
140
+ return self._client._request(
141
+ "POST", f"/subscriptions/{subscription_id}/cancel", params=params
142
+ )
143
+
144
+ def pause(self, subscription_id: str) -> dict[str, Any]:
145
+ """Pauses billing cycles for an active subscription."""
146
+ return self._client._request("POST", f"/subscriptions/{subscription_id}/pause")
147
+
148
+ def resume(self, subscription_id: str) -> dict[str, Any]:
149
+ """Resumes billing cycles for a paused subscription."""
150
+ return self._client._request("POST", f"/subscriptions/{subscription_id}/resume")
151
+
152
+ def change_plan(self, subscription_id: str, plan_id: str, price_id: str) -> dict[str, Any]:
153
+ """Upgrades or downgrades a subscription plan/price mid-cycle with proration."""
154
+ payload = {"plan_id": plan_id, "price_id": price_id}
155
+ return self._client._request(
156
+ "POST", f"/subscriptions/{subscription_id}/change-plan", json=payload
157
+ )
158
+
159
+ def get_events(self, subscription_id: str) -> "list[dict[str, Any]]":
160
+ """Retrieves the complete state transition history for a subscription."""
161
+ return self._client._request("GET", f"/subscriptions/{subscription_id}/events")
162
+
163
+
164
+ class PlansResource:
165
+ """Helper resource class for plans catalog management."""
166
+
167
+ def __init__(self, client):
168
+ self._client = client
169
+
170
+ def create(
171
+ self,
172
+ product_id: str,
173
+ name: str,
174
+ description: str | None = None,
175
+ billing_frequency: str = "monthly",
176
+ billing_interval: int = 1,
177
+ trial_period_days: int = 0,
178
+ metadata_json: dict[str, Any] | None = None,
179
+ ) -> dict[str, Any]:
180
+ """Create a new billing plan under a product."""
181
+ payload = {
182
+ "name": name,
183
+ "billing_frequency": billing_frequency,
184
+ "billing_interval": billing_interval,
185
+ "trial_period_days": trial_period_days,
186
+ }
187
+ if description is not None:
188
+ payload["description"] = description
189
+ if metadata_json is not None:
190
+ payload["metadata_json"] = metadata_json
191
+ return self._client._request("POST", f"/products/{product_id}/plans", json=payload)
192
+
193
+ def retrieve(self, plan_id: str) -> dict[str, Any]:
194
+ """Retrieve a specific plan configuration."""
195
+ return self._client._request("GET", f"/plans/{plan_id}")
196
+
197
+
198
+ class EntitlementsResource:
199
+ """Helper resource class for verifying feature gates/entitlements."""
200
+
201
+ def __init__(self, client):
202
+ self._client = client
203
+
204
+ def check(self, customer_id: str, entitlement_identifier: str) -> bool:
205
+ """Verifies if a customer has an active entitlement."""
206
+ res = self._client._request("GET", f"/subscriptions/customer/{customer_id}/entitlements")
207
+ active_entitlements = res.get("active_entitlements", [])
208
+ return entitlement_identifier in active_entitlements
209
+
210
+
211
+ class UsageResource:
212
+ """Helper resource class for reporting metered usage."""
213
+
214
+ def __init__(self, client):
215
+ self._client = client
216
+
217
+ def report(
218
+ self,
219
+ subscription_id: str,
220
+ event_type: str,
221
+ quantity: float,
222
+ unit: str = "tokens",
223
+ metadata: dict[str, Any] | None = None,
224
+ ) -> dict[str, Any]:
225
+ """Reports a metered usage event for a subscription."""
226
+ payload = {
227
+ "subscription_id": subscription_id,
228
+ "event_type": event_type,
229
+ "quantity": quantity,
230
+ "unit": unit,
231
+ }
232
+ if metadata is not None:
233
+ payload["metadata"] = metadata
234
+ return self._client._request("POST", "/usage/events", json=payload)
@@ -0,0 +1,145 @@
1
+ Metadata-Version: 2.4
2
+ Name: fype-sdk-beta
3
+ Version: 1.0.0
4
+ Summary: Official Python SDK for Fype Payments Layer
5
+ Author-email: Fype Engineering <engineering@fype.dev>
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Operating System :: OS Independent
9
+ Requires-Python: >=3.8
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: httpx>=0.23.0
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
14
+ Requires-Dist: pytest-asyncio>=0.20.0; extra == "dev"
15
+
16
+ # Fype Python Client SDK
17
+
18
+ The official Python client library for the [Fype Payments Orchestration Layer](https://fype.dev). Fully type-safe, async-friendly, and lightweight, it simplifies payment creations, refunds, and signed webhook signature verifications under a unified API interface.
19
+
20
+ ---
21
+
22
+ ## Installation
23
+
24
+ Install the package directly from your local virtual environment:
25
+ ```bash
26
+ pip install -e packages/python-sdk
27
+ ```
28
+
29
+ *(Or via PyPI once distributed)*:
30
+ ```bash
31
+ pip install fype
32
+ ```
33
+
34
+ ---
35
+
36
+ ## Quick Start
37
+
38
+ ### 1. Initialize Client
39
+ Expose your Fype developer API key (use test key for sandbox or live key for production):
40
+ ```python
41
+ from fype import Fype
42
+
43
+ # Initialize the client
44
+ fype = Fype(api_key="fype_test_your_secret_api_key_here")
45
+ ```
46
+
47
+ ### 2. Create Hosted Checkout Session
48
+ Initiate a payment order. Fype will automatically contact gateway adapters under the hood and return a public buyer-facing checkout URL:
49
+ ```python
50
+ payment = fype.payments.create(
51
+ amount=25000, # Amount in paise (₹250.00 INR)
52
+ currency="INR",
53
+ customer_email="buyer@email.com",
54
+ success_url="https://yourwebsite.com/payment/success",
55
+ cancel_url="https://yourwebsite.com/payment/cancel",
56
+ reference_id="order_ref_1092", # Optional merchant order ID
57
+ provider="cashfree" # Optional: Explicitly choose gateway (razorpay/cashfree)
58
+ )
59
+
60
+ print(f"Transaction ID: {payment['id']}")
61
+ print(f"Checkout URL: {payment['checkout_url']}")
62
+ ```
63
+
64
+ ### 3. Dynamic Redirect Placeholders
65
+ Like Stripe, Fype natively supports dynamic placeholders inside `success_url` and `cancel_url` redirect strings. This allows you to build frictionless, stateless checkout loops (e.g. for login-free purchases).
66
+
67
+ Fype will automatically replace these placeholders before redirecting the buyer back to your website:
68
+ * **`{CHECKOUT_SESSION_ID}`**: Replaced with the actual Fype Checkout Session UUID.
69
+ * **`{PAYMENT_ID}`**: Replaced with the actual Fype Payment UUID (useful for direct API validation).
70
+
71
+ **Example:**
72
+ ```python
73
+ payment = fype.payments.create(
74
+ amount=25000,
75
+ currency="INR",
76
+ customer_email="buyer@email.com",
77
+ success_url="https://yourwebsite.com/payment/success?fype_session_id={PAYMENT_ID}",
78
+ cancel_url="https://yourwebsite.com/payment/cancel"
79
+ )
80
+ ```
81
+
82
+ ---
83
+
84
+ ## 4. Retrieve Payment Details
85
+ Audit checkout session status at any time:
86
+ ```python
87
+ payment = fype.payments.retrieve("pay_test_abc123")
88
+ print(f"Current Status: {payment['status']}") # 'created', 'succeeded', 'failed'
89
+ ```
90
+
91
+ ### 4. Issue a Refund
92
+ Perform partial or full refunds for successful payments:
93
+ ```python
94
+ # Full Refund (omit amount parameter)
95
+ refund = fype.refunds.create(payment_id="pay_test_abc123")
96
+
97
+ # Partial Refund (specify amount in paise)
98
+ partial_refund = fype.refunds.create(payment_id="pay_test_abc123", amount=5000)
99
+ ```
100
+
101
+ ---
102
+
103
+ ## Webhook Signature Verification
104
+
105
+ Incoming HTTP webhook events are cryptographically signed by Fype. Always verify signatures against your webhook signing secret (`whsec_...`) to prevent spoofing. The SDK handles constant-time HMAC comparison internally to guard against side-channel timing analysis attacks:
106
+
107
+ ```python
108
+ from fype.errors import SignatureVerificationError
109
+
110
+ raw_body = request.body() # Get raw request bytes
111
+ signature = request.headers.get("X-Fype-Signature")
112
+ secret = "whsec_your_webhook_signing_secret"
113
+
114
+ try:
115
+ fype.webhooks.verify_signature(raw_body, signature, secret)
116
+ print("Webhook signature is valid and authentic!")
117
+ # Proceed to handle events (e.g. payment.succeeded)
118
+ except SignatureVerificationError as e:
119
+ print(f"Webhook signature mismatch: {e}")
120
+ ```
121
+
122
+ ---
123
+
124
+ ## Error Handling
125
+
126
+ The SDK exposes explicit, catchable exception classes to let you handle failures gracefully:
127
+
128
+ ```python
129
+ from fype.errors import AuthenticationError, InvalidRequestError, ApiConnectionError, FypeError
130
+
131
+ try:
132
+ payment = fype.payments.create(...)
133
+ except AuthenticationError:
134
+ # Handle bad API keys
135
+ print("Invalid Fype API key.")
136
+ except InvalidRequestError as e:
137
+ # Handle validation errors (e.g. invalid currency, negative amount)
138
+ print(f"Request failed validation: {e}")
139
+ except ApiConnectionError:
140
+ # Handle network timeouts or unreachable hosts
141
+ print("Connection to Fype server timed out.")
142
+ except FypeError as e:
143
+ # Handle server-side errors
144
+ print(f"Fype gateway error: {e}")
145
+ ```
@@ -0,0 +1,8 @@
1
+ fype/__init__.py,sha256=Pr381WnL1dALziXLNQ9wt3Y2lkH62_Y81ECN19pUtv4,336
2
+ fype/client.py,sha256=kpv1_nUP62uk6AVdxwCxiydf1Wa0iYPOzYBrhDPcyKQ,3474
3
+ fype/errors.py,sha256=hS1-iqaJIsGhPzOjzqyLJ1ERA7WFTVQ79t0v65oqjxM,857
4
+ fype/resources.py,sha256=2e4I5jC1F4xr2aK4X2vKMaPe_zEAozemcOz_4Fk4hE8,8472
5
+ fype_sdk_beta-1.0.0.dist-info/METADATA,sha256=6mkpcZHTytwoRbMPkqJVAbgGsOscljk9tc6A-M44dIQ,4907
6
+ fype_sdk_beta-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
7
+ fype_sdk_beta-1.0.0.dist-info/top_level.txt,sha256=YW9pJqQELeT-cEOCXG2__k6tPyBy-6GGT4WSqfsY2-0,5
8
+ fype_sdk_beta-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ fype