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 +8 -0
- hevn_cli/api/__init__.py +6 -0
- hevn_cli/api/app.py +163 -0
- hevn_cli/api/base.py +40 -0
- hevn_cli/api/mcp.py +50 -0
- hevn_cli/api/public.py +16 -0
- hevn_cli/client.py +123 -0
- hevn_cli/commands/__init__.py +1 -0
- hevn_cli/commands/account.py +451 -0
- hevn_cli/commands/auth.py +217 -0
- hevn_cli/commands/balance.py +237 -0
- hevn_cli/commands/banks.py +233 -0
- hevn_cli/commands/cards.py +442 -0
- hevn_cli/commands/contacts.py +444 -0
- hevn_cli/commands/contractors.py +224 -0
- hevn_cli/commands/contracts.py +528 -0
- hevn_cli/commands/invoices.py +676 -0
- hevn_cli/commands/status.py +180 -0
- hevn_cli/commands/transfer.py +494 -0
- hevn_cli/config.py +73 -0
- hevn_cli/env.py +75 -0
- hevn_cli/formatters/__init__.py +1 -0
- hevn_cli/formatters/invoices.py +61 -0
- hevn_cli/main.py +167 -0
- hevn_cli/normalize.py +24 -0
- hevn_cli/output.py +26 -0
- hevn_cli/parsing.py +57 -0
- hevn_cli/progress.py +52 -0
- hevn_cli/prompts.py +54 -0
- hevn_cli/render.py +54 -0
- hevn_cli/res/__init__.py +0 -0
- hevn_cli/res/login_complete.html +11 -0
- hevn_cli-0.1.0.dist-info/METADATA +189 -0
- hevn_cli-0.1.0.dist-info/RECORD +37 -0
- hevn_cli-0.1.0.dist-info/WHEEL +4 -0
- hevn_cli-0.1.0.dist-info/entry_points.txt +3 -0
- hevn_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
hevn_cli/__init__.py
ADDED
hevn_cli/api/__init__.py
ADDED
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."""
|