hevn-cli 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.
hevn_cli/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """HEVN CLI package."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("hevn-cli")
7
+ except PackageNotFoundError:
8
+ __version__ = "0.0.0"
@@ -0,0 +1,6 @@
1
+ from hevn_cli.api.app import AppApi
2
+ from hevn_cli.api.base import ApiClient, AuthMode
3
+ from hevn_cli.api.mcp import McpApi
4
+ from hevn_cli.api.public import PublicApi
5
+
6
+ __all__ = ["ApiClient", "AppApi", "AuthMode", "McpApi", "PublicApi"]
hevn_cli/api/app.py ADDED
@@ -0,0 +1,163 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from hevn_cli.api.base import ApiClient, AuthMode
6
+
7
+
8
+ class AppApi:
9
+ def __init__(self, client: ApiClient | None = None) -> None:
10
+ self.client = client or ApiClient(AuthMode.APP)
11
+
12
+ def current_user(self) -> dict[str, Any]:
13
+ return self.client.get("/user")
14
+
15
+ def update_profile(self, payload: dict[str, Any]) -> dict[str, Any]:
16
+ return self.client.put("/user/kyc", json=payload)
17
+
18
+ def kyc_link(self) -> dict[str, Any]:
19
+ return self.client.post("/user/kyc_link")
20
+
21
+ def kyc_status(self, *, provider: str = "swipelux") -> dict[str, Any]:
22
+ return self.client.get("/user/kyc/status", params={"provider": provider})
23
+
24
+ def balance(self) -> dict[str, Any]:
25
+ return self.client.get("/balance")
26
+
27
+ def list_contacts(self, *, limit: int = 100, offset: int = 0) -> dict[str, Any]:
28
+ return self.client.get("/user/contacts", params={"limit": limit, "offset": offset})
29
+
30
+ def list_transactions(
31
+ self,
32
+ *,
33
+ limit: int = 50,
34
+ offset: int = 0,
35
+ transaction_type: str | None = None,
36
+ status: str | None = None,
37
+ income_only: bool | None = None,
38
+ bank_account_id: str | None = None,
39
+ ) -> dict[str, Any]:
40
+ return self.client.get(
41
+ "/transactions",
42
+ params={
43
+ "limit": limit,
44
+ "offset": offset,
45
+ "type": transaction_type,
46
+ "status": status,
47
+ "incomeOnly": income_only,
48
+ "bankAccountId": bank_account_id,
49
+ },
50
+ )
51
+
52
+ def create_contact(self, payload: dict[str, Any]) -> dict[str, Any]:
53
+ return self.client.post("/user/contact", json=payload)
54
+
55
+ def update_contact(self, contact_id: str, payload: dict[str, Any]) -> dict[str, Any]:
56
+ return self.client.patch(f"/user/contacts/{contact_id}", json=payload)
57
+
58
+ def delete_contact(self, contact_id: str) -> Any:
59
+ return self.client.delete(f"/user/contacts/{contact_id}")
60
+
61
+ def upload_document(self, payload: dict[str, Any]) -> dict[str, Any]:
62
+ return self.client.post("/documents/upload", json=payload)
63
+
64
+ def list_invoices(self) -> dict[str, Any]:
65
+ return self.client.get("/documents/contracts/invoices")
66
+
67
+ def get_invoice(self, invoice_id: str) -> dict[str, Any]:
68
+ return self.client.get(f"/documents/contracts/invoices/{invoice_id}")
69
+
70
+ def create_invoice(self, payload: dict[str, Any]) -> dict[str, Any]:
71
+ return self.client.post("/documents/contracts/invoices", json=payload)
72
+
73
+ def create_uploaded_invoice(self, payload: dict[str, Any]) -> dict[str, Any]:
74
+ return self.client.post("/documents/contracts/invoices/uploaded", json=payload)
75
+
76
+ def update_invoice(self, invoice_id: str, payload: dict[str, Any]) -> dict[str, Any]:
77
+ return self.client.put(f"/documents/contracts/invoices/{invoice_id}", json=payload)
78
+
79
+ def create_invoice_from_contract(self, contract_id: str, payload: dict[str, Any]) -> dict[str, Any]:
80
+ return self.client.post(f"/documents/contracts/{contract_id}/create-invoice", json=payload)
81
+
82
+ def batch_invoicing(self, payload: list[dict[str, Any]]) -> dict[str, Any]:
83
+ return self.client.post("/documents/contracts/invoices/batch_invoicing", json=payload)
84
+
85
+ def list_contracts(self) -> dict[str, Any]:
86
+ return self.client.get("/documents/contracts")
87
+
88
+ def get_contract(self, contract_id: str) -> dict[str, Any]:
89
+ return self.client.get(f"/documents/contracts/{contract_id}")
90
+
91
+ def preview_contract(self, contract_id: str) -> dict[str, Any]:
92
+ return self.client.get(f"/documents/contracts/{contract_id}/preview")
93
+
94
+ def create_contract(self, payload: dict[str, Any]) -> dict[str, Any]:
95
+ return self.client.post("/documents/contracts", json=payload)
96
+
97
+ def pause_contract(self, contract_id: str) -> dict[str, Any]:
98
+ return self.client.post(f"/documents/contracts/{contract_id}/pause")
99
+
100
+ def approve_contract(self, contract_id: str) -> dict[str, Any]:
101
+ return self.client.post(f"/documents/contracts/{contract_id}/approve")
102
+
103
+ def update_contract_payment_methods(
104
+ self, contract_id: str, payment_methods: list[dict[str, Any]]
105
+ ) -> dict[str, Any]:
106
+ return self.client.put(
107
+ f"/documents/contracts/{contract_id}/payment_methods",
108
+ json={"paymentMethods": payment_methods},
109
+ )
110
+
111
+ def delete_contract(self, contract_id: str) -> dict[str, Any]:
112
+ return self.client.delete(f"/documents/contracts/{contract_id}")
113
+
114
+ def list_banks(self) -> dict[str, Any]:
115
+ return self.client.get("/banks")
116
+
117
+ def activate_banks(self, rails: list[str]) -> dict[str, Any]:
118
+ return self.client.post("/banks/activate", json={"rails": rails})
119
+
120
+ def validate_bank(self, payload: dict[str, Any]) -> dict[str, Any]:
121
+ return self.client.post("/user/contact/bank/validate", json=payload)
122
+
123
+ def payin_quote(self, payload: dict[str, Any]) -> dict[str, Any]:
124
+ return self.client.post("/balance/payin/quote", json=payload)
125
+
126
+ def bank_payin_quote(self, payload: dict[str, Any]) -> dict[str, Any]:
127
+ return self.client.post("/banks/payin/quote", json=payload)
128
+
129
+ def submit_bank_payin_quote(self, payload: dict[str, Any]) -> dict[str, Any]:
130
+ return self.client.post("/banks/payin/quote/submit", json=payload)
131
+
132
+ def payout_quote(self, payload: dict[str, Any]) -> dict[str, Any]:
133
+ return self.client.post("/balance/payout/quote", json=payload)
134
+
135
+ def list_cards(self) -> dict[str, Any]:
136
+ return self.client.get("/cards")
137
+
138
+ def create_card(self, payload: dict[str, Any]) -> dict[str, Any]:
139
+ return self.client.post("/cards", json=payload)
140
+
141
+ def get_card(self, card_id: str) -> dict[str, Any]:
142
+ return self.client.get(f"/cards/{card_id}")
143
+
144
+ def card_details(self, card_id: str, payload: dict[str, Any]) -> dict[str, Any]:
145
+ return self.client.post(f"/cards/{card_id}/details", json=payload)
146
+
147
+ def update_card_label(self, card_id: str, label: str) -> dict[str, Any]:
148
+ return self.client.put(f"/cards/{card_id}/label", json={"label": label})
149
+
150
+ def update_card_limit(self, card_id: str, payload: dict[str, Any]) -> dict[str, Any]:
151
+ return self.client.put(f"/cards/{card_id}/limit", json=payload)
152
+
153
+ def freeze_card(self, card_id: str) -> Any:
154
+ return self.client.post(f"/cards/{card_id}/freeze")
155
+
156
+ def unfreeze_card(self, card_id: str) -> Any:
157
+ return self.client.post(f"/cards/{card_id}/unfreeze")
158
+
159
+ def card_kyc_link(self) -> dict[str, Any]:
160
+ return self.client.post("/cards/kyc/link")
161
+
162
+ def pre_approve_cards(self) -> dict[str, Any]:
163
+ return self.client.post("/cards/pre-approve")
hevn_cli/api/base.py ADDED
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+ from typing import Any
5
+
6
+ from hevn_cli.client import app_headers, mcp_headers, request
7
+
8
+
9
+ class AuthMode(str, Enum):
10
+ APP = "app"
11
+ MCP = "mcp"
12
+ PUBLIC = "public"
13
+
14
+
15
+ class ApiClient:
16
+ def __init__(self, auth_mode: AuthMode, *, idempotency_key: str | None = None) -> None:
17
+ self.auth_mode = auth_mode
18
+ self.idempotency_key = idempotency_key
19
+
20
+ def _headers(self) -> dict[str, str]:
21
+ if self.auth_mode == AuthMode.APP:
22
+ return app_headers()
23
+ if self.auth_mode == AuthMode.MCP:
24
+ return mcp_headers(self.idempotency_key)
25
+ return {"Accept": "application/json"}
26
+
27
+ def get(self, path: str, *, params: dict[str, Any] | None = None) -> Any:
28
+ return request("GET", path, headers=self._headers(), params=params)
29
+
30
+ def post(self, path: str, *, json: dict[str, Any] | None = None) -> Any:
31
+ return request("POST", path, headers=self._headers(), json_body=json)
32
+
33
+ def put(self, path: str, *, json: dict[str, Any] | None = None) -> Any:
34
+ return request("PUT", path, headers=self._headers(), json_body=json)
35
+
36
+ def patch(self, path: str, *, json: dict[str, Any] | None = None) -> Any:
37
+ return request("PATCH", path, headers=self._headers(), json_body=json)
38
+
39
+ def delete(self, path: str) -> Any:
40
+ return request("DELETE", path, headers=self._headers())
hevn_cli/api/mcp.py ADDED
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from hevn_cli.api.base import ApiClient, AuthMode
6
+
7
+
8
+ class McpApi:
9
+ def __init__(self, *, idempotency_key: str | None = None, client: ApiClient | None = None) -> None:
10
+ self.client = client or ApiClient(AuthMode.MCP, idempotency_key=idempotency_key)
11
+
12
+ def balance(self) -> dict[str, Any]:
13
+ return self.client.get("/mcp/get_balance")
14
+
15
+ def transfer(
16
+ self,
17
+ data: dict[str, Any] | None = None,
18
+ *,
19
+ contact_id: str | None = None,
20
+ invoice_id: str | None = None,
21
+ quote_id: str | None = None,
22
+ amount: float | None = None,
23
+ memo: str | None = None,
24
+ ) -> dict[str, Any]:
25
+ body = dict(data or {})
26
+ if contact_id:
27
+ body["contactId"] = contact_id
28
+ if invoice_id:
29
+ body["invoiceId"] = invoice_id
30
+ if quote_id:
31
+ body["quoteId"] = quote_id
32
+ if amount is not None:
33
+ body["amount"] = amount
34
+ if memo:
35
+ body["memo"] = memo
36
+ if not (body.get("contactId") or body.get("invoiceId") or body.get("quoteId")):
37
+ raise ValueError("MCP transfer requires contact_id, invoice_id, or quote_id.")
38
+ return self.client.post("/mcp/transfer", json=body)
39
+
40
+ def transfers(
41
+ self,
42
+ *,
43
+ limit: int = 50,
44
+ offset: int = 0,
45
+ idempotency_key: str | None = None,
46
+ ) -> dict[str, Any]:
47
+ return self.client.get(
48
+ "/mcp/transfers",
49
+ params={"limit": limit, "offset": offset, "idempotency_key": idempotency_key},
50
+ )
hevn_cli/api/public.py ADDED
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from hevn_cli.api.base import ApiClient, AuthMode
6
+
7
+
8
+ class PublicApi:
9
+ def __init__(self, client: ApiClient | None = None) -> None:
10
+ self.client = client or ApiClient(AuthMode.PUBLIC)
11
+
12
+ def rate(self, currency: str) -> dict[str, Any]:
13
+ return self.client.get(f"/utils/rates/{currency}")
14
+
15
+ def invoice(self, invoice_id: str) -> dict[str, Any]:
16
+ return self.client.get(f"/public/invoices/{invoice_id}")
hevn_cli/client.py ADDED
@@ -0,0 +1,123 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from typing import Any
5
+ from urllib.parse import urlparse
6
+
7
+ import httpx
8
+
9
+ from hevn_cli.config import get_config_value
10
+ from hevn_cli.env import api_url
11
+ from hevn_cli.progress import waiting
12
+
13
+ DEFAULT_BASE_URL = "https://api.hevn.finance/api/v1"
14
+
15
+
16
+ class HevnError(RuntimeError):
17
+ def __init__(
18
+ self,
19
+ message: str,
20
+ *,
21
+ status_code: int | None = None,
22
+ reason: str | None = None,
23
+ detail: Any | None = None,
24
+ method: str | None = None,
25
+ url: str | None = None,
26
+ ) -> None:
27
+ super().__init__(message)
28
+ self.message = message
29
+ self.status_code = status_code
30
+ self.reason = reason
31
+ self.detail = detail
32
+ self.method = method
33
+ self.url = url
34
+
35
+
36
+ def base_url() -> str:
37
+ try:
38
+ raw = api_url()
39
+ except ValueError as exc:
40
+ raise HevnError(str(exc)) from exc
41
+ parsed = urlparse(raw)
42
+ if parsed.scheme and parsed.netloc:
43
+ docs_paths = {"/docs", "/redoc", "/openapi.json"}
44
+ api_docs_paths = {f"/api/v1{path}" for path in docs_paths}
45
+ path = parsed.path.rstrip("/")
46
+ if path in docs_paths:
47
+ return f"{parsed.scheme}://{parsed.netloc}/api/v1"
48
+ if path in api_docs_paths:
49
+ return f"{parsed.scheme}://{parsed.netloc}/api/v1"
50
+ if parsed.scheme and parsed.netloc and parsed.path in ("", "/"):
51
+ return f"{raw}/api/v1"
52
+ return raw
53
+
54
+
55
+ def app_headers() -> dict[str, str]:
56
+ key = os.getenv("HEVN_API_KEY") or get_config_value("api_key")
57
+ if not key:
58
+ raise HevnError("This command needs app authorization (JWT). Run `hevn login` first or set HEVN_API_KEY.")
59
+ header = os.getenv("HEVN_API_KEY_HEADER", "Authorization")
60
+ if header.lower() == "authorization" and not key.lower().startswith("bearer "):
61
+ key = f"Bearer {key}"
62
+ return {
63
+ header: key,
64
+ "Accept": "application/json",
65
+ "device-id": os.getenv("HEVN_DEVICE_ID") or get_config_value("device_id") or "hevn-cli",
66
+ "device-type": "cli",
67
+ "device-name": "HEVN CLI",
68
+ }
69
+
70
+
71
+ def mcp_headers(idempotency_key: str | None = None) -> dict[str, str]:
72
+ key = os.getenv("HEVN_MCP_KEY") or get_config_value("mcp_key")
73
+ if not key:
74
+ raise HevnError("This command needs an MCP key. Run `hevn login` first or set HEVN_MCP_KEY.")
75
+ headers = {"X-API-Key": key, "Accept": "application/json"}
76
+ if idempotency_key:
77
+ headers["Idempotency-Key"] = idempotency_key
78
+ return headers
79
+
80
+
81
+ def request(
82
+ method: str,
83
+ path: str,
84
+ *,
85
+ headers: dict[str, str],
86
+ json_body: dict[str, Any] | None = None,
87
+ params: dict[str, Any] | None = None,
88
+ ) -> Any:
89
+ url = f"{base_url()}{path}"
90
+ try:
91
+ with waiting(f"Waiting for HEVN {method} {path}"):
92
+ with httpx.Client(timeout=60) as client:
93
+ response = client.request(
94
+ method,
95
+ url,
96
+ headers=headers,
97
+ json=json_body,
98
+ params={k: v for k, v in (params or {}).items() if v is not None},
99
+ )
100
+ except httpx.HTTPError as exc:
101
+ raise HevnError(f"Request failed: {exc}", method=method, url=url) from exc
102
+
103
+ if response.status_code >= 400:
104
+ try:
105
+ detail: Any = response.json()
106
+ except ValueError:
107
+ detail = response.text
108
+ message = detail.get("detail") if isinstance(detail, dict) else detail
109
+ if isinstance(message, dict):
110
+ message = message.get("detail") or message.get("message") or str(message)
111
+ if not isinstance(message, str):
112
+ message = str(message)
113
+ raise HevnError(
114
+ message,
115
+ status_code=response.status_code,
116
+ reason=response.reason_phrase,
117
+ detail=detail,
118
+ method=method,
119
+ url=str(response.request.url),
120
+ )
121
+ if not response.content:
122
+ return None
123
+ return response.json()
@@ -0,0 +1 @@
1
+ """CLI command groups."""