pyvelv 0.1.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.
pyvelv/__init__.py ADDED
@@ -0,0 +1,22 @@
1
+ """
2
+ Pyvelv — Async Python SDK for the Velvpay payment gateway.
3
+
4
+ Usage:
5
+ from pyvelv import Velvpay
6
+
7
+ async with Velvpay(secret_key="sk_...") as client:
8
+ link = await client.payment_links.create(...)
9
+ """
10
+
11
+ from pyvelv.client import VelvpayClient
12
+
13
+ # Ergonomic alias so users can do: from pyvelv import Velvpay
14
+ Velvpay = VelvpayClient
15
+
16
+ __version__ = "0.1.0"
17
+
18
+ __all__ = [
19
+ "Velvpay",
20
+ "VelvpayClient",
21
+ "__version__",
22
+ ]
pyvelv/client.py ADDED
@@ -0,0 +1,203 @@
1
+ """
2
+ Async HTTP client for the Velvpay payment gateway.
3
+
4
+ Usage::
5
+
6
+ async with VelvpayClient(
7
+ secret_key="sk_...",
8
+ public_key="pk_...",
9
+ encryption_key="enc_...",
10
+ ) as client:
11
+ link = await client.payment_links.create(...)
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from typing import Any
17
+ import uuid
18
+
19
+ import httpx
20
+
21
+ from pyvelv.crypto import generate_api_key_header
22
+ from pyvelv.exceptions import VelvpayAPIError, VelvpayAuthError
23
+
24
+
25
+ class VelvpayClient:
26
+ """
27
+ Asynchronous client for the Velvpay API.
28
+
29
+ Args:
30
+ secret_key: Your Velvpay merchant secret key.
31
+ public_key: Your Velvpay merchant public key.
32
+ encryption_key: Your Velvpay encryption key/passphrase.
33
+ base_url: API base URL. If not provided, it is chosen based on the sandbox flag.
34
+ sandbox: If True, uses the test sandbox base URL.
35
+ timeout: Request timeout in seconds.
36
+ """
37
+
38
+ _PRODUCTION_BASE_URL = "https://api.velvpay.com/api/v1/service"
39
+ _SANDBOX_BASE_URL = "https://testapi.velpay.io"
40
+
41
+ def __init__(
42
+ self,
43
+ secret_key: str,
44
+ public_key: str,
45
+ encryption_key: str,
46
+ *,
47
+ base_url: str | None = None,
48
+ sandbox: bool = False,
49
+ timeout: float = 30.0,
50
+ ) -> None:
51
+ if not secret_key:
52
+ raise ValueError("secret_key must not be empty")
53
+ if not public_key:
54
+ raise ValueError("public_key must not be empty")
55
+ if not encryption_key:
56
+ raise ValueError("encryption_key must not be empty")
57
+
58
+ self._secret_key = secret_key
59
+ self._public_key = public_key
60
+ self._encryption_key = encryption_key
61
+
62
+
63
+
64
+ if base_url:
65
+ self._base_url = base_url.rstrip("/")
66
+ else:
67
+ self._base_url = (self._SANDBOX_BASE_URL if sandbox else self._PRODUCTION_BASE_URL).rstrip("/")
68
+
69
+ self._timeout = timeout
70
+ self._http: httpx.AsyncClient | None = None
71
+
72
+ # Lazily-initialised resource namespaces
73
+ self._payment_links: PaymentLinksResource | None = None
74
+ self._transactions: TransactionsResource | None = None
75
+
76
+ # ------------------------------------------------------------------
77
+ # HTTP transport helpers
78
+ # ------------------------------------------------------------------
79
+
80
+ @property
81
+ def http(self) -> httpx.AsyncClient:
82
+ """Return (or lazily create) the underlying ``httpx.AsyncClient``."""
83
+ if self._http is None or self._http.is_closed:
84
+ self._http = httpx.AsyncClient(
85
+ base_url=self._base_url,
86
+ timeout=self._timeout,
87
+ )
88
+ return self._http
89
+
90
+ async def _request(
91
+ self,
92
+ method: str,
93
+ path: str,
94
+ *,
95
+ json: Any | None = None,
96
+ params: dict[str, Any] | None = None,
97
+ ) -> dict[str, Any]:
98
+ """
99
+ Send an authenticated request to the Velvpay API.
100
+
101
+ Dynamic headers including the encrypted `api-key` and `reference-id`
102
+ are generated for each call.
103
+ """
104
+ reference_id = f"ref_{uuid.uuid4().hex}"
105
+ api_key = generate_api_key_header(
106
+ secret_key=self._secret_key,
107
+ public_key=self._public_key,
108
+ reference_id=reference_id,
109
+ encryption_key=self._encryption_key,
110
+ )
111
+
112
+ headers = {
113
+ "Content-Type": "application/json",
114
+ "api-key": api_key,
115
+ "public-key": self._public_key,
116
+ "reference-id": reference_id,
117
+ }
118
+
119
+ if method.upper() == "POST":
120
+ headers["idempotencykey"] = str(uuid.uuid4())
121
+
122
+ response = await self.http.request(
123
+ method,
124
+ path,
125
+ json=json,
126
+ params=params,
127
+ headers=headers,
128
+ )
129
+
130
+ # --- Error handling ---
131
+ if response.status_code in (401, 403):
132
+ body = self._safe_json(response)
133
+ raise VelvpayAuthError(
134
+ message=body.get("reason", "Authentication failed") if isinstance(body, dict) else "Authentication failed",
135
+ status_code=response.status_code,
136
+ response_body=body,
137
+ )
138
+
139
+ if not (200 <= response.status_code < 300):
140
+ body = self._safe_json(response)
141
+ msg = None
142
+ if isinstance(body, dict):
143
+ msg = body.get("reason") or body.get("msg") or body.get("err") or body.get("message")
144
+ raise VelvpayAPIError(
145
+ message=msg or response.reason_phrase or "API request failed",
146
+ status_code=response.status_code,
147
+ response_body=body,
148
+ )
149
+
150
+ return response.json()
151
+
152
+ @staticmethod
153
+ def _safe_json(response: httpx.Response) -> Any:
154
+ """Attempt to parse JSON from a response, falling back to the raw text."""
155
+ try:
156
+ return response.json()
157
+ except Exception:
158
+ return response.text
159
+
160
+ # ------------------------------------------------------------------
161
+ # Resource namespaces (lazy properties)
162
+ # ------------------------------------------------------------------
163
+
164
+ @property
165
+ def payment_links(self) -> PaymentLinksResource:
166
+ """Access payment link operations."""
167
+ if self._payment_links is None:
168
+ from pyvelv.resources.payment_links import PaymentLinksResource
169
+ self._payment_links = PaymentLinksResource(self)
170
+ return self._payment_links
171
+
172
+ @property
173
+ def transactions(self) -> TransactionsResource:
174
+ """Access transaction operations."""
175
+ if self._transactions is None:
176
+ from pyvelv.resources.transactions import TransactionsResource
177
+ self._transactions = TransactionsResource(self)
178
+ return self._transactions
179
+
180
+ # ------------------------------------------------------------------
181
+ # Context manager & lifecycle
182
+ # ------------------------------------------------------------------
183
+
184
+ async def close(self) -> None:
185
+ """Close the underlying HTTP client and release resources."""
186
+ if self._http and not self._http.is_closed:
187
+ await self._http.aclose()
188
+
189
+ async def __aenter__(self) -> VelvpayClient:
190
+ return self
191
+
192
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
193
+ await self.close()
194
+
195
+ def __repr__(self) -> str:
196
+ return f"<VelvpayClient base_url={self._base_url!r}>"
197
+
198
+
199
+ # Forward-reference imports so the type annotations above resolve at runtime.
200
+ # Actual classes are imported lazily inside the properties to avoid circular
201
+ # imports.
202
+ from pyvelv.resources.payment_links import PaymentLinksResource # noqa: E402
203
+ from pyvelv.resources.transactions import TransactionsResource # noqa: E402
pyvelv/crypto.py ADDED
@@ -0,0 +1,140 @@
1
+ """
2
+ Crypto module for Velvpay API key header generation.
3
+
4
+ Implements the legacy OpenSSL EVP_BytesToKey key derivation routine
5
+ for AES-256-CBC encryption. The generated ciphertext is used as the
6
+ `api-key` header value on every API request.
7
+
8
+ OpenSSL envelope format:
9
+ base64( b"Salted__" + salt(8 bytes) + ciphertext )
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import base64
15
+ import hashlib
16
+ import json
17
+ import os
18
+ from typing import Any
19
+
20
+ from Crypto.Cipher import AES
21
+ from Crypto.Util.Padding import pad, unpad
22
+
23
+
24
+ def _evp_bytes_to_key(
25
+ password: bytes,
26
+ salt: bytes | None,
27
+ key_len: int = 32,
28
+ iv_len: int = 16,
29
+ ) -> tuple[bytes, bytes]:
30
+ """
31
+ Derive key and IV using the OpenSSL EVP_BytesToKey algorithm.
32
+
33
+ This replicates the behaviour of ``openssl enc -aes-256-cbc`` with MD5 as
34
+ the digest function.
35
+
36
+ Args:
37
+ password: The passphrase as raw bytes.
38
+ salt: An 8-byte salt, or ``None`` for unsalted derivation.
39
+ key_len: Desired key length in bytes (32 for AES-256).
40
+ iv_len: Desired IV length in bytes (16 for AES-CBC).
41
+
42
+ Returns:
43
+ A ``(key, iv)`` tuple of the derived key and initialisation vector.
44
+ """
45
+ d_tot = b""
46
+ d_list: list[bytes] = []
47
+
48
+ while len(d_tot) < (key_len + iv_len):
49
+ prev = d_list[-1] if d_list else b""
50
+ d_i = hashlib.md5(prev + password + (salt or b"")).digest()
51
+ d_tot += d_i
52
+ d_list.append(d_i)
53
+
54
+ return d_tot[:key_len], d_tot[key_len : key_len + iv_len]
55
+
56
+
57
+ def encrypt_aes_256_cbc(plaintext: str, passphrase: str) -> str:
58
+ """
59
+ Encrypt *plaintext* with AES-256-CBC using the OpenSSL envelope format.
60
+
61
+ 1. Generate a random 8-byte salt.
62
+ 2. Derive ``(key, iv)`` via :func:`_evp_bytes_to_key`.
63
+ 3. PKCS#7-pad the plaintext and encrypt.
64
+ 4. Return ``base64(b"Salted__" + salt + ciphertext)``.
65
+
66
+ Args:
67
+ plaintext: The data to encrypt (UTF-8 string).
68
+ passphrase: The secret passphrase used for key derivation.
69
+
70
+ Returns:
71
+ A base64-encoded string in the standard OpenSSL format.
72
+ """
73
+ salt = os.urandom(8)
74
+ key, iv = _evp_bytes_to_key(passphrase.encode("utf-8"), salt)
75
+
76
+ cipher = AES.new(key, AES.MODE_CBC, iv)
77
+ padded = pad(plaintext.encode("utf-8"), AES.block_size)
78
+ ciphertext = cipher.encrypt(padded)
79
+
80
+ # OpenSSL envelope: "Salted__" magic + salt + ciphertext
81
+ envelope = b"Salted__" + salt + ciphertext
82
+ return base64.b64encode(envelope).decode("ascii")
83
+
84
+
85
+ def decrypt_aes_256_cbc(encoded: str, passphrase: str) -> str:
86
+ """
87
+ Decrypt an OpenSSL-format AES-256-CBC ciphertext.
88
+
89
+ This is the inverse of :func:`encrypt_aes_256_cbc` and is provided
90
+ primarily for testing round-trip correctness.
91
+
92
+ Args:
93
+ encoded: The base64-encoded OpenSSL envelope.
94
+ passphrase: The passphrase that was used for encryption.
95
+
96
+ Returns:
97
+ The original plaintext string.
98
+
99
+ Raises:
100
+ ValueError: If the envelope does not start with ``Salted__``.
101
+ """
102
+ raw = base64.b64decode(encoded)
103
+
104
+ if not raw.startswith(b"Salted__"):
105
+ raise ValueError("Invalid OpenSSL envelope: missing 'Salted__' magic header")
106
+
107
+ salt = raw[8:16]
108
+ ciphertext = raw[16:]
109
+
110
+ key, iv = _evp_bytes_to_key(passphrase.encode("utf-8"), salt)
111
+ cipher = AES.new(key, AES.MODE_CBC, iv)
112
+ plaintext = unpad(cipher.decrypt(ciphertext), AES.block_size)
113
+
114
+ return plaintext.decode("utf-8")
115
+
116
+
117
+ def generate_api_key_header(
118
+ secret_key: str,
119
+ public_key: str,
120
+ reference_id: str,
121
+ encryption_key: str,
122
+ ) -> str:
123
+ """
124
+ Generate the ``api-key`` header value for a Velvpay API request.
125
+
126
+ The payload (secretKey + publicKey + referenceId) is encrypted
127
+ using AES-256-CBC with the merchant's encryption key.
128
+
129
+ Args:
130
+ secret_key: The merchant's Velvpay secret key.
131
+ public_key: The merchant's Velvpay public key.
132
+ reference_id: The unique reference ID for the request.
133
+ encryption_key: The merchant's Velvpay encryption key.
134
+
135
+ Returns:
136
+ A base64-encoded encrypted string suitable for the ``api-key`` header.
137
+ """
138
+ payload = secret_key + public_key + reference_id
139
+ return encrypt_aes_256_cbc(payload, encryption_key)
140
+
pyvelv/exceptions.py ADDED
@@ -0,0 +1,67 @@
1
+ """
2
+ Exception hierarchy for the Pyvelv SDK.
3
+
4
+ All exceptions inherit from :class:`VelvpayError` so callers can catch
5
+ a single base class for broad error handling.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+
13
+ class VelvpayError(Exception):
14
+ """Base exception for all Velvpay SDK errors."""
15
+
16
+ def __init__(self, message: str = "An unexpected Velvpay error occurred") -> None:
17
+ self.message = message
18
+ super().__init__(self.message)
19
+
20
+
21
+ class VelvpayAPIError(VelvpayError):
22
+ """
23
+ Raised when the Velvpay API returns a non-2xx HTTP response.
24
+
25
+ Attributes:
26
+ status_code: The HTTP status code.
27
+ response_body: The raw response body (decoded JSON or string).
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ message: str,
33
+ status_code: int,
34
+ response_body: Any = None,
35
+ ) -> None:
36
+ self.status_code = status_code
37
+ self.response_body = response_body
38
+ super().__init__(f"[HTTP {status_code}] {message}")
39
+
40
+
41
+ class VelvpayAuthError(VelvpayAPIError):
42
+ """
43
+ Raised when the API rejects the request due to authentication failure.
44
+
45
+ Triggered by HTTP 401 (Unauthorized) and 403 (Forbidden) responses.
46
+ """
47
+
48
+ def __init__(
49
+ self,
50
+ message: str = "Authentication failed",
51
+ status_code: int = 401,
52
+ response_body: Any = None,
53
+ ) -> None:
54
+ super().__init__(message, status_code, response_body)
55
+
56
+
57
+ class VelvpayValidationError(VelvpayError):
58
+ """
59
+ Raised when request data fails Pydantic validation before being sent.
60
+
61
+ Attributes:
62
+ errors: The list of validation error details from Pydantic.
63
+ """
64
+
65
+ def __init__(self, message: str, errors: list[Any] | None = None) -> None:
66
+ self.errors = errors or []
67
+ super().__init__(message)
pyvelv/models.py ADDED
@@ -0,0 +1,137 @@
1
+ """
2
+ Pydantic v2 schemas for Velvpay API request and response payloads.
3
+
4
+ All models use ``populate_by_name=True`` so they accept both the Python
5
+ attribute name and the optional JSON alias.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from decimal import Decimal
11
+ from typing import Any, Generic, TypeVar
12
+
13
+ from pydantic import BaseModel, ConfigDict, Field
14
+
15
+ T = TypeVar("T")
16
+
17
+
18
+ # ---------------------------------------------------------------------------
19
+ # Generic API response wrapper
20
+ # ---------------------------------------------------------------------------
21
+
22
+ class APIResponse(BaseModel, Generic[T]):
23
+ """
24
+ Standard envelope returned by every Velvpay API endpoint.
25
+ """
26
+
27
+ model_config = ConfigDict(populate_by_name=True)
28
+
29
+ status: str | None = None
30
+ msg: str | None = None
31
+ success: bool | None = None
32
+ message: str | None = None
33
+ reason: str | None = None
34
+ err: str | None = None
35
+ data: T | None = None
36
+
37
+ @property
38
+ def is_success(self) -> bool:
39
+ """Helper property to check if the API call was successful."""
40
+ if self.status == "success":
41
+ return True
42
+ if self.success is True:
43
+ return True
44
+ if self.message == "Successful" or self.msg == "successful":
45
+ return True
46
+ return False
47
+
48
+
49
+ # ---------------------------------------------------------------------------
50
+ # Payment Links / Initiation
51
+ # ---------------------------------------------------------------------------
52
+
53
+ class CreatePaymentLinkRequest(BaseModel):
54
+ """Request body for ``POST /payment/initiate``."""
55
+
56
+ model_config = ConfigDict(populate_by_name=True)
57
+
58
+ title: str = Field(..., description="Payment title or name")
59
+ description: str = Field(..., description="Human-readable description")
60
+ amount: Decimal = Field(..., description="Payment amount in kobo or Naira")
61
+ is_naira: bool = Field(
62
+ default=False,
63
+ alias="isNaira",
64
+ description="True if amount is in Naira, False if in Kobo",
65
+ )
66
+ charge_customer: bool = Field(
67
+ default=True,
68
+ alias="chargeCustomer",
69
+ description="True if transaction fees should be charged to the customer",
70
+ )
71
+ post_payment_instructions: str | None = Field(
72
+ default=None,
73
+ alias="postPaymentInstructions",
74
+ description="Instructions to show after payment is complete",
75
+ )
76
+
77
+
78
+ class PaymentLink(BaseModel):
79
+ """A Velvpay payment link / virtual account initiation object."""
80
+
81
+ model_config = ConfigDict(populate_by_name=True)
82
+
83
+ status: str
84
+ message: str | None = None
85
+ account_number: str = Field(alias="accountNumber")
86
+ reference: str
87
+ amount: Decimal
88
+ transaction_id: str = Field(alias="transactionId")
89
+ bank: str
90
+ validity_time: int = Field(alias="validityTime")
91
+
92
+
93
+ # ---------------------------------------------------------------------------
94
+ # Transactions
95
+ # ---------------------------------------------------------------------------
96
+
97
+ class TransactionData(BaseModel):
98
+ """Inner transaction details data."""
99
+
100
+ model_config = ConfigDict(populate_by_name=True)
101
+
102
+ status: str
103
+ message: str
104
+ account_number: str = Field(alias="accountNumber")
105
+ reference: str
106
+
107
+
108
+ class Transaction(BaseModel):
109
+ """A Velvpay transaction record."""
110
+
111
+ model_config = ConfigDict(populate_by_name=True)
112
+
113
+ id: str = Field(alias="_id")
114
+ account_number: str = Field(alias="accountNumber")
115
+ tx_id: str = Field(alias="txId")
116
+ link: str | None = None
117
+ name: str | None = None
118
+ amount: Decimal
119
+ status: str
120
+ channel: str
121
+ type: str | None = None
122
+ method: str | None = None
123
+ date: str | None = None
124
+ description: str | None = None
125
+ metadata: list[dict[str, Any]] | None = None
126
+ webhook_url: str | None = Field(default=None, alias="webhook_url")
127
+ data: TransactionData | None = None
128
+
129
+
130
+ class TransactionList(BaseModel):
131
+ """Paginated list of transactions."""
132
+
133
+ model_config = ConfigDict(populate_by_name=True)
134
+
135
+ data: list[Transaction]
136
+ total: int
137
+ page: int
@@ -0,0 +1,11 @@
1
+ """Resource namespace package for Velvpay API endpoints."""
2
+
3
+ from pyvelv.resources.base import BaseResource
4
+ from pyvelv.resources.payment_links import PaymentLinksResource
5
+ from pyvelv.resources.transactions import TransactionsResource
6
+
7
+ __all__ = [
8
+ "BaseResource",
9
+ "PaymentLinksResource",
10
+ "TransactionsResource",
11
+ ]
@@ -0,0 +1,27 @@
1
+ """
2
+ Base resource class that all Velvpay API resource namespaces inherit from.
3
+
4
+ Each resource receives a reference to the parent :class:`~pyvelv.client.VelvpayClient`
5
+ so it can make authenticated HTTP requests through the shared transport.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import TYPE_CHECKING
11
+
12
+ if TYPE_CHECKING:
13
+ from pyvelv.client import VelvpayClient
14
+
15
+
16
+ class BaseResource:
17
+ """
18
+ Abstract base for API resource namespaces.
19
+
20
+ Subclasses access ``self._client._request(...)`` to call the API.
21
+ """
22
+
23
+ def __init__(self, client: VelvpayClient) -> None:
24
+ self._client = client
25
+
26
+ def __repr__(self) -> str:
27
+ return f"<{self.__class__.__name__} client={self._client!r}>"
@@ -0,0 +1,42 @@
1
+ """
2
+ Payment Links resource — ``client.payment_links.*`` operations.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from pyvelv.models import APIResponse, CreatePaymentLinkRequest, PaymentLink
8
+ from pyvelv.resources.base import BaseResource
9
+
10
+
11
+ class PaymentLinksResource(BaseResource):
12
+ """
13
+ Operations on Velvpay payment links (Payment Initiation).
14
+ """
15
+
16
+ async def create(self, data: CreatePaymentLinkRequest) -> APIResponse[PaymentLink]:
17
+ """
18
+ Initiate a new payment.
19
+
20
+ Args:
21
+ data: The payment initiation payload.
22
+
23
+ Returns:
24
+ An :class:`~pyvelv.models.APIResponse` wrapping a :class:`~pyvelv.models.PaymentLink`.
25
+ """
26
+ payload = data.model_dump(mode="json", by_alias=True, exclude_none=True)
27
+ raw = await self._client._request("POST", "/payment/initiate", json=payload)
28
+ return APIResponse[PaymentLink].model_validate(raw)
29
+
30
+ async def get(self, transaction_id: str) -> APIResponse[PaymentLink]:
31
+ """
32
+ Retrieve details of an initiated payment.
33
+
34
+ Args:
35
+ transaction_id: The unique transaction identifier (e.g. FR-...).
36
+ """
37
+ raw = await self._client._request(
38
+ "GET",
39
+ "/payment/collection/transaction/details",
40
+ params={"transaction_id": transaction_id},
41
+ )
42
+ return APIResponse[PaymentLink].model_validate(raw)
@@ -0,0 +1,42 @@
1
+ """
2
+ Transactions resource — ``client.transactions.*`` operations.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from pyvelv.models import APIResponse, Transaction
8
+ from pyvelv.resources.base import BaseResource
9
+
10
+
11
+ class TransactionsResource(BaseResource):
12
+ """
13
+ Operations on Velvpay transactions.
14
+ """
15
+
16
+ async def get(self, transaction_id: str) -> APIResponse[Transaction]:
17
+ """
18
+ Retrieve details of a transaction.
19
+
20
+ Args:
21
+ transaction_id: The unique transaction identifier (e.g. FR-...).
22
+ """
23
+ raw = await self._client._request(
24
+ "GET",
25
+ "/payment/collection/transaction/details",
26
+ params={"transaction_id": transaction_id},
27
+ )
28
+ return APIResponse[Transaction].model_validate(raw)
29
+
30
+ async def verify(self, transaction_id: str) -> APIResponse[Transaction]:
31
+ """
32
+ Resolve/verify a transaction status.
33
+
34
+ Args:
35
+ transaction_id: The unique transaction identifier (e.g. FR-...).
36
+ """
37
+ raw = await self._client._request(
38
+ "GET",
39
+ "/payment/collection/transaction/resolve",
40
+ params={"transaction_id": transaction_id},
41
+ )
42
+ return APIResponse[Transaction].model_validate(raw)
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyvelv
3
+ Version: 0.1.0
4
+ Summary: Async Python SDK for the Velvpay payment gateway
5
+ Author: Velvpay SDK Team
6
+ Requires-Python: >=3.10,<4.0
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3.10
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Requires-Dist: httpx (>=0.27.0)
14
+ Requires-Dist: pycryptodome (>=3.20.0)
15
+ Requires-Dist: pydantic (>=2.0)
16
+ Description-Content-Type: text/markdown
17
+
18
+ # Pyvelv
19
+
20
+ > Async Python SDK for the **Velvpay** payment gateway.
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ pip install pyvelv
26
+ # or
27
+ poetry add pyvelv
28
+ ```
29
+
30
+ ## Quick Start
31
+
32
+ ```python
33
+ import asyncio
34
+ from pyvelv import Velvpay
35
+ from pyvelv.models import CreatePaymentLinkRequest
36
+
37
+ async def main():
38
+ async with Velvpay(secret_key="your-secret-key") as client:
39
+ # Create a payment link
40
+ response = await client.payment_links.create(
41
+ CreatePaymentLinkRequest(
42
+ amount=1500.00,
43
+ currency="NGN",
44
+ description="Order #12345",
45
+ )
46
+ )
47
+ print(response.data.url)
48
+
49
+ # Verify a transaction
50
+ txn = await client.transactions.verify(reference="ref_abc123")
51
+ print(txn.data.status)
52
+
53
+ asyncio.run(main())
54
+ ```
55
+
56
+ ## Authentication
57
+
58
+ Pyvelv handles authentication automatically. The SDK generates the required `api-key` header using a legacy OpenSSL `EVP_BytesToKey` routine (AES-256-CBC) with a random 8-byte salt on every request.
59
+
60
+ Simply pass your Velvpay secret key when initializing the client:
61
+
62
+ ```python
63
+ client = Velvpay(secret_key="sk_live_...")
64
+ ```
65
+
66
+ ## Resources
67
+
68
+ | Resource | Methods |
69
+ |-----------------------|--------------------------------|
70
+ | `payment_links` | `create()`, `get()`, `list()` |
71
+ | `transactions` | `get()`, `list()`, `verify()` |
72
+
73
+ ## License
74
+
75
+ MIT
76
+
@@ -0,0 +1,12 @@
1
+ pyvelv/__init__.py,sha256=P5c0FHHOYhtiJRSYouhT7Yko3ljbtJK7W_USI09p0h4,439
2
+ pyvelv/client.py,sha256=F4JYemtoYuqnsKkrClTRzjGgPU5cHZPQiUaUOalV9jg,6756
3
+ pyvelv/crypto.py,sha256=UHEdtUAXwX-QJc6dLL5paFA92blKe331GznEFVSxtPA,4200
4
+ pyvelv/exceptions.py,sha256=71MSSrow-by6LaA0r1kFxu7nZ9fNxTciFGNgsWGm218,1796
5
+ pyvelv/models.py,sha256=hGEjL_7JJzAewU4FFf6Qm7AH2yMpgLWrhk3aWU3juFQ,4030
6
+ pyvelv/resources/__init__.py,sha256=4jq5OHs9cJQo9U5jCQBFR5qtSWZTKbGNizFy9hDw3_Q,326
7
+ pyvelv/resources/base.py,sha256=njtr8k56QLARN4O3qKz2W7JEb3jzYpnuUYbB7xe85Po,721
8
+ pyvelv/resources/payment_links.py,sha256=j9oHc4Z2nOQokO0bGBImBeyVqQg30V1ME8CsRN8Joks,1393
9
+ pyvelv/resources/transactions.py,sha256=Cqu3dzeaWLqVhIkNIjqRujrnffhpj7ehonKvCwDrSmE,1264
10
+ pyvelv-0.1.0.dist-info/METADATA,sha256=05qb4kSccdN30fIvoXA5Hm8UOdyBt6rM8gipRxiLjzU,2011
11
+ pyvelv-0.1.0.dist-info/WHEEL,sha256=EGEvSphFYqXKs23-kQBeyNoJP1nrT8ZJKQoi5p5DYL8,88
12
+ pyvelv-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.4.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any