cyclos-sdk-py 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.
- angstrom_cyclos/__init__.py +40 -0
- angstrom_cyclos/__main__.py +82 -0
- angstrom_cyclos/accounts/__init__.py +6 -0
- angstrom_cyclos/accounts/client.py +109 -0
- angstrom_cyclos/accounts/client_async.py +102 -0
- angstrom_cyclos/accounts/models.py +57 -0
- angstrom_cyclos/async_client.py +74 -0
- angstrom_cyclos/auth/__init__.py +6 -0
- angstrom_cyclos/auth/client.py +68 -0
- angstrom_cyclos/auth/client_async.py +48 -0
- angstrom_cyclos/auth/models.py +27 -0
- angstrom_cyclos/client.py +77 -0
- angstrom_cyclos/config.py +85 -0
- angstrom_cyclos/constants.py +37 -0
- angstrom_cyclos/exceptions.py +103 -0
- angstrom_cyclos/members/__init__.py +23 -0
- angstrom_cyclos/members/client.py +209 -0
- angstrom_cyclos/members/client_async.py +183 -0
- angstrom_cyclos/members/models.py +75 -0
- angstrom_cyclos/models.py +185 -0
- angstrom_cyclos/operators/__init__.py +23 -0
- angstrom_cyclos/operators/client.py +295 -0
- angstrom_cyclos/operators/client_async.py +231 -0
- angstrom_cyclos/operators/models.py +67 -0
- angstrom_cyclos/organizations/__init__.py +15 -0
- angstrom_cyclos/organizations/client.py +90 -0
- angstrom_cyclos/organizations/client_async.py +70 -0
- angstrom_cyclos/organizations/models.py +19 -0
- angstrom_cyclos/permissions/__init__.py +6 -0
- angstrom_cyclos/permissions/client.py +124 -0
- angstrom_cyclos/permissions/client_async.py +107 -0
- angstrom_cyclos/permissions/models.py +31 -0
- angstrom_cyclos/transactions/__init__.py +6 -0
- angstrom_cyclos/transactions/client.py +69 -0
- angstrom_cyclos/transactions/client_async.py +60 -0
- angstrom_cyclos/transactions/models.py +34 -0
- angstrom_cyclos/transfers/__init__.py +6 -0
- angstrom_cyclos/transfers/client.py +168 -0
- angstrom_cyclos/transfers/client_async.py +135 -0
- angstrom_cyclos/transfers/models.py +75 -0
- angstrom_cyclos/transport/__init__.py +11 -0
- angstrom_cyclos/transport/http.py +715 -0
- angstrom_cyclos/transport/retry.py +99 -0
- angstrom_cyclos/users/__init__.py +6 -0
- angstrom_cyclos/users/client.py +67 -0
- angstrom_cyclos/users/client_async.py +65 -0
- angstrom_cyclos/users/models.py +20 -0
- angstrom_cyclos/webservices/__init__.py +15 -0
- angstrom_cyclos/webservices/client.py +167 -0
- angstrom_cyclos/webservices/client_async.py +128 -0
- angstrom_cyclos/webservices/models.py +57 -0
- cyclos_sdk_py-0.1.0.dist-info/METADATA +298 -0
- cyclos_sdk_py-0.1.0.dist-info/RECORD +56 -0
- cyclos_sdk_py-0.1.0.dist-info/WHEEL +4 -0
- cyclos_sdk_py-0.1.0.dist-info/entry_points.txt +2 -0
- cyclos_sdk_py-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""angstrom-cyclos: Python SDK for Cyclos 4.16 REST API."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
from angstrom_cyclos.async_client import AsyncCyclosClient
|
|
6
|
+
from angstrom_cyclos.client import CyclosClient
|
|
7
|
+
from angstrom_cyclos.config import CyclosConfig
|
|
8
|
+
from angstrom_cyclos.exceptions import (
|
|
9
|
+
CyclosAuthenticationError,
|
|
10
|
+
CyclosAuthorizationError,
|
|
11
|
+
CyclosConflictError,
|
|
12
|
+
CyclosConnectionError,
|
|
13
|
+
CyclosError,
|
|
14
|
+
CyclosIdempotencyError,
|
|
15
|
+
CyclosNotFoundError,
|
|
16
|
+
CyclosRateLimitError,
|
|
17
|
+
CyclosServerError,
|
|
18
|
+
CyclosTimeoutError,
|
|
19
|
+
CyclosTransferError,
|
|
20
|
+
CyclosValidationError,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"__version__",
|
|
25
|
+
"CyclosClient",
|
|
26
|
+
"AsyncCyclosClient",
|
|
27
|
+
"CyclosConfig",
|
|
28
|
+
"CyclosError",
|
|
29
|
+
"CyclosAuthenticationError",
|
|
30
|
+
"CyclosAuthorizationError",
|
|
31
|
+
"CyclosValidationError",
|
|
32
|
+
"CyclosNotFoundError",
|
|
33
|
+
"CyclosConflictError",
|
|
34
|
+
"CyclosRateLimitError",
|
|
35
|
+
"CyclosServerError",
|
|
36
|
+
"CyclosTimeoutError",
|
|
37
|
+
"CyclosConnectionError",
|
|
38
|
+
"CyclosTransferError",
|
|
39
|
+
"CyclosIdempotencyError",
|
|
40
|
+
]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""CLI entry point for angstrom-cyclos."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from angstrom_cyclos import CyclosClient, CyclosConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _sync_main(args: argparse.Namespace) -> int:
|
|
12
|
+
config = CyclosConfig.from_env()
|
|
13
|
+
with CyclosClient(config) as client:
|
|
14
|
+
if args.command == "auth" and args.subcommand == "login":
|
|
15
|
+
print("Use application code or environment variables to authenticate.")
|
|
16
|
+
return 0
|
|
17
|
+
if args.command == "members" and args.subcommand == "get":
|
|
18
|
+
member = client.members.get(args.id)
|
|
19
|
+
print(member.model_dump_json(indent=2))
|
|
20
|
+
return 0
|
|
21
|
+
if args.command == "members" and args.subcommand == "search":
|
|
22
|
+
result = client.members.search(query=args.query)
|
|
23
|
+
print(result.model_dump_json(indent=2))
|
|
24
|
+
return 0
|
|
25
|
+
if args.command == "accounts" and args.subcommand == "balance":
|
|
26
|
+
balance = client.accounts.get_balance(args.member)
|
|
27
|
+
print(balance.model_dump_json(indent=2))
|
|
28
|
+
return 0
|
|
29
|
+
if args.command == "transfers" and args.subcommand == "get":
|
|
30
|
+
transfer = client.transfers.get(args.id)
|
|
31
|
+
print(transfer.model_dump_json(indent=2))
|
|
32
|
+
return 0
|
|
33
|
+
if args.command == "transfers" and args.subcommand == "history":
|
|
34
|
+
history = client.transfers.history(
|
|
35
|
+
member_id=args.member, account_type=args.account_type
|
|
36
|
+
)
|
|
37
|
+
print(history.model_dump_json(indent=2))
|
|
38
|
+
return 0
|
|
39
|
+
return 1
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
43
|
+
parser = argparse.ArgumentParser(prog="cyclos", description="Angstrom Cyclos SDK CLI")
|
|
44
|
+
sub = parser.add_subparsers(dest="command")
|
|
45
|
+
|
|
46
|
+
auth = sub.add_parser("auth")
|
|
47
|
+
auth.add_subparsers(dest="subcommand").add_parser("login")
|
|
48
|
+
|
|
49
|
+
members = sub.add_parser("members")
|
|
50
|
+
members_sub = members.add_subparsers(dest="subcommand")
|
|
51
|
+
get_member = members_sub.add_parser("get")
|
|
52
|
+
get_member.add_argument("id")
|
|
53
|
+
search_member = members_sub.add_parser("search")
|
|
54
|
+
search_member.add_argument("query")
|
|
55
|
+
|
|
56
|
+
accounts = sub.add_parser("accounts")
|
|
57
|
+
accounts_sub = accounts.add_subparsers(dest="subcommand")
|
|
58
|
+
balance = accounts_sub.add_parser("balance")
|
|
59
|
+
balance.add_argument("member")
|
|
60
|
+
|
|
61
|
+
transfers = sub.add_parser("transfers")
|
|
62
|
+
transfers_sub = transfers.add_subparsers(dest="subcommand")
|
|
63
|
+
get_transfer = transfers_sub.add_parser("get")
|
|
64
|
+
get_transfer.add_argument("id")
|
|
65
|
+
history = transfers_sub.add_parser("history")
|
|
66
|
+
history.add_argument("member")
|
|
67
|
+
history.add_argument("--account-type", default="mobileWallet")
|
|
68
|
+
|
|
69
|
+
return parser
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def main(argv: list[str] | None = None) -> int:
|
|
73
|
+
parser = _build_parser()
|
|
74
|
+
args = parser.parse_args(argv)
|
|
75
|
+
if not args.command:
|
|
76
|
+
parser.print_help()
|
|
77
|
+
return 0
|
|
78
|
+
return _sync_main(args)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
if __name__ == "__main__": # pragma: no cover
|
|
82
|
+
sys.exit(main())
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Account management client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from decimal import Decimal
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
from angstrom_cyclos.accounts.models import Account, AccountHistoryEntry, AccountStatus
|
|
9
|
+
from angstrom_cyclos.models import AccountBalance, PaginatedResult
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from angstrom_cyclos.client import CyclosClient
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AccountsClient:
|
|
16
|
+
"""List accounts, retrieve statuses and balances, and fetch history."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, client: CyclosClient) -> None:
|
|
19
|
+
self._client = client
|
|
20
|
+
|
|
21
|
+
def list(
|
|
22
|
+
self,
|
|
23
|
+
member_id: str,
|
|
24
|
+
*,
|
|
25
|
+
page: int = 0,
|
|
26
|
+
page_size: int = 20,
|
|
27
|
+
) -> PaginatedResult[Account]:
|
|
28
|
+
"""List accounts for an owner/member."""
|
|
29
|
+
response = self._client._transport.get(
|
|
30
|
+
f"/{member_id}/accounts",
|
|
31
|
+
params={"page": page, "pageSize": page_size},
|
|
32
|
+
)
|
|
33
|
+
items = [Account.model_validate(a) for a in response.json()]
|
|
34
|
+
return PaginatedResult[Account](
|
|
35
|
+
items=items,
|
|
36
|
+
page=page,
|
|
37
|
+
page_size=page_size,
|
|
38
|
+
has_next=len(items) >= page_size,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def get(self, member_id: str, account_type: str) -> Account:
|
|
42
|
+
"""Retrieve the account status for a specific account type."""
|
|
43
|
+
response = self._client._transport.get(f"/{member_id}/accounts/{account_type}")
|
|
44
|
+
return Account.model_validate(response.json())
|
|
45
|
+
|
|
46
|
+
def get_balance(
|
|
47
|
+
self,
|
|
48
|
+
member_id: str,
|
|
49
|
+
*,
|
|
50
|
+
account_type: str = "mobileWallet",
|
|
51
|
+
) -> AccountBalance:
|
|
52
|
+
"""Return a strongly typed balance for the given member and account type."""
|
|
53
|
+
account = self.get(member_id, account_type)
|
|
54
|
+
status = account.status or AccountStatus()
|
|
55
|
+
return AccountBalance(
|
|
56
|
+
account_id=account.id or member_id,
|
|
57
|
+
account_type=account_type,
|
|
58
|
+
currency=account.currency.name if account.currency else None,
|
|
59
|
+
balance=status.balance or Decimal("0"),
|
|
60
|
+
available_balance=status.available_balance,
|
|
61
|
+
credit_limit=status.credit_limit,
|
|
62
|
+
reserved_amount=status.reserved_amount,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def history(
|
|
66
|
+
self,
|
|
67
|
+
member_id: str,
|
|
68
|
+
account_type: str,
|
|
69
|
+
*,
|
|
70
|
+
page: int = 0,
|
|
71
|
+
page_size: int = 20,
|
|
72
|
+
date_from: str | None = None,
|
|
73
|
+
date_to: str | None = None,
|
|
74
|
+
amount_min: Decimal | None = None,
|
|
75
|
+
amount_max: Decimal | None = None,
|
|
76
|
+
status: str | None = None,
|
|
77
|
+
transfer_type: str | None = None,
|
|
78
|
+
direction: str | None = None,
|
|
79
|
+
) -> PaginatedResult[AccountHistoryEntry]:
|
|
80
|
+
"""Fetch account history (balance transfers)."""
|
|
81
|
+
params: dict[str, Any] = {
|
|
82
|
+
"page": page,
|
|
83
|
+
"pageSize": page_size,
|
|
84
|
+
}
|
|
85
|
+
if date_from:
|
|
86
|
+
params["datePeriod.begin"] = date_from
|
|
87
|
+
if date_to:
|
|
88
|
+
params["datePeriod.end"] = date_to
|
|
89
|
+
if amount_min is not None:
|
|
90
|
+
params["amountRange.min"] = str(amount_min)
|
|
91
|
+
if amount_max is not None:
|
|
92
|
+
params["amountRange.max"] = str(amount_max)
|
|
93
|
+
if status:
|
|
94
|
+
params["statuses"] = status
|
|
95
|
+
if transfer_type:
|
|
96
|
+
params["transferTypes"] = transfer_type
|
|
97
|
+
if direction:
|
|
98
|
+
params["direction"] = direction
|
|
99
|
+
response = self._client._transport.get(
|
|
100
|
+
f"/{member_id}/accounts/{account_type}/history",
|
|
101
|
+
params=params,
|
|
102
|
+
)
|
|
103
|
+
items = [AccountHistoryEntry.model_validate(entry) for entry in response.json()]
|
|
104
|
+
return PaginatedResult[AccountHistoryEntry](
|
|
105
|
+
items=items,
|
|
106
|
+
page=page,
|
|
107
|
+
page_size=page_size,
|
|
108
|
+
has_next=len(items) >= page_size,
|
|
109
|
+
)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Asynchronous account client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from decimal import Decimal
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
from angstrom_cyclos.accounts.models import Account, AccountHistoryEntry, AccountStatus
|
|
9
|
+
from angstrom_cyclos.models import AccountBalance, PaginatedResult
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from angstrom_cyclos.async_client import AsyncCyclosClient
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AsyncAccountsClient:
|
|
16
|
+
"""Async account client."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, client: AsyncCyclosClient) -> None:
|
|
19
|
+
self._client = client
|
|
20
|
+
|
|
21
|
+
async def list(
|
|
22
|
+
self,
|
|
23
|
+
member_id: str,
|
|
24
|
+
*,
|
|
25
|
+
page: int = 0,
|
|
26
|
+
page_size: int = 20,
|
|
27
|
+
) -> PaginatedResult[Account]:
|
|
28
|
+
response = await self._client._transport.get(
|
|
29
|
+
f"/{member_id}/accounts",
|
|
30
|
+
params={"page": page, "pageSize": page_size},
|
|
31
|
+
)
|
|
32
|
+
items = [Account.model_validate(a) for a in response.json()]
|
|
33
|
+
return PaginatedResult[Account](
|
|
34
|
+
items=items,
|
|
35
|
+
page=page,
|
|
36
|
+
page_size=page_size,
|
|
37
|
+
has_next=len(items) >= page_size,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
async def get(self, member_id: str, account_type: str) -> Account:
|
|
41
|
+
response = await self._client._transport.get(f"/{member_id}/accounts/{account_type}")
|
|
42
|
+
return Account.model_validate(response.json())
|
|
43
|
+
|
|
44
|
+
async def get_balance(
|
|
45
|
+
self,
|
|
46
|
+
member_id: str,
|
|
47
|
+
*,
|
|
48
|
+
account_type: str = "mobileWallet",
|
|
49
|
+
) -> AccountBalance:
|
|
50
|
+
account = await self.get(member_id, account_type)
|
|
51
|
+
status = account.status or AccountStatus()
|
|
52
|
+
return AccountBalance(
|
|
53
|
+
account_id=account.id or member_id,
|
|
54
|
+
account_type=account_type,
|
|
55
|
+
currency=account.currency.name if account.currency else None,
|
|
56
|
+
balance=status.balance or Decimal("0"),
|
|
57
|
+
available_balance=status.available_balance,
|
|
58
|
+
credit_limit=status.credit_limit,
|
|
59
|
+
reserved_amount=status.reserved_amount,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
async def history(
|
|
63
|
+
self,
|
|
64
|
+
member_id: str,
|
|
65
|
+
account_type: str,
|
|
66
|
+
*,
|
|
67
|
+
page: int = 0,
|
|
68
|
+
page_size: int = 20,
|
|
69
|
+
date_from: str | None = None,
|
|
70
|
+
date_to: str | None = None,
|
|
71
|
+
amount_min: Decimal | None = None,
|
|
72
|
+
amount_max: Decimal | None = None,
|
|
73
|
+
status: str | None = None,
|
|
74
|
+
transfer_type: str | None = None,
|
|
75
|
+
direction: str | None = None,
|
|
76
|
+
) -> PaginatedResult[AccountHistoryEntry]:
|
|
77
|
+
params: dict[str, Any] = {"page": page, "pageSize": page_size}
|
|
78
|
+
if date_from:
|
|
79
|
+
params["datePeriod.begin"] = date_from
|
|
80
|
+
if date_to:
|
|
81
|
+
params["datePeriod.end"] = date_to
|
|
82
|
+
if amount_min is not None:
|
|
83
|
+
params["amountRange.min"] = str(amount_min)
|
|
84
|
+
if amount_max is not None:
|
|
85
|
+
params["amountRange.max"] = str(amount_max)
|
|
86
|
+
if status:
|
|
87
|
+
params["statuses"] = status
|
|
88
|
+
if transfer_type:
|
|
89
|
+
params["transferTypes"] = transfer_type
|
|
90
|
+
if direction:
|
|
91
|
+
params["direction"] = direction
|
|
92
|
+
response = await self._client._transport.get(
|
|
93
|
+
f"/{member_id}/accounts/{account_type}/history",
|
|
94
|
+
params=params,
|
|
95
|
+
)
|
|
96
|
+
items = [AccountHistoryEntry.model_validate(entry) for entry in response.json()]
|
|
97
|
+
return PaginatedResult[AccountHistoryEntry](
|
|
98
|
+
items=items,
|
|
99
|
+
page=page,
|
|
100
|
+
page_size=page_size,
|
|
101
|
+
has_next=len(items) >= page_size,
|
|
102
|
+
)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Account models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from decimal import Decimal
|
|
6
|
+
|
|
7
|
+
from pydantic import Field
|
|
8
|
+
|
|
9
|
+
from angstrom_cyclos.models import (
|
|
10
|
+
AccountReference,
|
|
11
|
+
Currency,
|
|
12
|
+
CyclosBaseModel,
|
|
13
|
+
EntityReference,
|
|
14
|
+
UserReference,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AccountStatus(CyclosBaseModel):
|
|
19
|
+
"""Account status including balances."""
|
|
20
|
+
|
|
21
|
+
balance: Decimal | None = None
|
|
22
|
+
credit_limit: Decimal | None = Field(default=None, alias="creditLimit")
|
|
23
|
+
upper_credit_limit: Decimal | None = Field(default=None, alias="upperCreditLimit")
|
|
24
|
+
reserved_amount: Decimal | None = Field(default=None, alias="reservedAmount")
|
|
25
|
+
available_balance: Decimal | None = Field(default=None, alias="availableBalance")
|
|
26
|
+
negative_since: str | None = Field(default=None, alias="negativeSince")
|
|
27
|
+
begin_date: str | None = Field(default=None, alias="beginDate")
|
|
28
|
+
end_date: str | None = Field(default=None, alias="endDate")
|
|
29
|
+
balance_at_begin: Decimal | None = Field(default=None, alias="balanceAtBegin")
|
|
30
|
+
balance_at_end: Decimal | None = Field(default=None, alias="balanceAtEnd")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Account(CyclosBaseModel):
|
|
34
|
+
"""Cyclos account with owner and currency."""
|
|
35
|
+
|
|
36
|
+
id: str | None = None
|
|
37
|
+
type: EntityReference | None = None
|
|
38
|
+
user: UserReference | None = None
|
|
39
|
+
kind: str | None = None
|
|
40
|
+
currency: Currency | None = None
|
|
41
|
+
status: AccountStatus | None = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class AccountHistoryEntry(CyclosBaseModel):
|
|
45
|
+
"""Single entry from an account history."""
|
|
46
|
+
|
|
47
|
+
id: str | None = None
|
|
48
|
+
date: str | None = None
|
|
49
|
+
amount: Decimal | None = None
|
|
50
|
+
description: str | None = None
|
|
51
|
+
transaction_number: str | None = Field(default=None, alias="transactionNumber")
|
|
52
|
+
related_account: AccountReference | None = Field(default=None, alias="relatedAccount")
|
|
53
|
+
related_name: str | None = Field(default=None, alias="relatedName")
|
|
54
|
+
statuses: dict[str, str] | None = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
__all__ = ["Account", "AccountStatus", "AccountHistoryEntry", "AccountReference", "Currency"]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Asynchronous Cyclos SDK client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from types import TracebackType
|
|
6
|
+
from typing import Self
|
|
7
|
+
|
|
8
|
+
from angstrom_cyclos.accounts.client_async import AsyncAccountsClient
|
|
9
|
+
from angstrom_cyclos.auth.client_async import AsyncAuthClient
|
|
10
|
+
from angstrom_cyclos.config import CyclosConfig
|
|
11
|
+
from angstrom_cyclos.members.client_async import AsyncMembersClient
|
|
12
|
+
from angstrom_cyclos.operators.client_async import AsyncOperatorsClient
|
|
13
|
+
from angstrom_cyclos.organizations.client_async import AsyncOrganizationsClient
|
|
14
|
+
from angstrom_cyclos.permissions.client_async import AsyncPermissionsClient
|
|
15
|
+
from angstrom_cyclos.transactions.client_async import AsyncTransactionsClient
|
|
16
|
+
from angstrom_cyclos.transfers.client_async import AsyncTransfersClient
|
|
17
|
+
from angstrom_cyclos.transport import AsyncHTTPTransport
|
|
18
|
+
from angstrom_cyclos.users.client_async import AsyncUsersClient
|
|
19
|
+
from angstrom_cyclos.webservices.client_async import AsyncWebServicesClient
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AsyncCyclosClient:
|
|
23
|
+
"""Asynchronous client for the Cyclos 4.16 REST API.
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
>>> import asyncio
|
|
27
|
+
>>> from angstrom_cyclos import AsyncCyclosClient, CyclosConfig
|
|
28
|
+
>>> config = CyclosConfig.from_env()
|
|
29
|
+
>>> async def main() -> None:
|
|
30
|
+
... async with AsyncCyclosClient(config) as client:
|
|
31
|
+
... await client.auth.login("username", "password")
|
|
32
|
+
... await client.members.get("alice")
|
|
33
|
+
>>> asyncio.run(main())
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, config: CyclosConfig | None = None) -> None:
|
|
37
|
+
self._config = config or CyclosConfig.from_env()
|
|
38
|
+
self._transport = AsyncHTTPTransport(self._config)
|
|
39
|
+
self.auth = AsyncAuthClient(self)
|
|
40
|
+
self.members = AsyncMembersClient(self)
|
|
41
|
+
self.users = AsyncUsersClient(self)
|
|
42
|
+
self.organizations = AsyncOrganizationsClient(self)
|
|
43
|
+
self.operators = AsyncOperatorsClient(self)
|
|
44
|
+
self.permissions = AsyncPermissionsClient(self)
|
|
45
|
+
self.webservices = AsyncWebServicesClient(self)
|
|
46
|
+
self.accounts = AsyncAccountsClient(self)
|
|
47
|
+
self.transfers = AsyncTransfersClient(self)
|
|
48
|
+
self.transactions = AsyncTransactionsClient(self)
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def config(self) -> CyclosConfig:
|
|
52
|
+
return self._config
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def _transport(self) -> AsyncHTTPTransport:
|
|
56
|
+
return self.__transport
|
|
57
|
+
|
|
58
|
+
@_transport.setter
|
|
59
|
+
def _transport(self, value: AsyncHTTPTransport) -> None:
|
|
60
|
+
self.__transport = value
|
|
61
|
+
|
|
62
|
+
async def __aenter__(self) -> Self:
|
|
63
|
+
return self
|
|
64
|
+
|
|
65
|
+
async def __aexit__(
|
|
66
|
+
self,
|
|
67
|
+
exc_type: type[BaseException] | None,
|
|
68
|
+
exc_val: BaseException | None,
|
|
69
|
+
exc_tb: TracebackType | None,
|
|
70
|
+
) -> None:
|
|
71
|
+
await self.close()
|
|
72
|
+
|
|
73
|
+
async def close(self) -> None:
|
|
74
|
+
await self._transport.close()
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Authentication client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from angstrom_cyclos.auth.models import Auth, LoginAuth
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from angstrom_cyclos.client import CyclosClient
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AuthClient:
|
|
14
|
+
"""Handles Cyclos login, logout, session refresh, and current user lookup."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, client: CyclosClient) -> None:
|
|
17
|
+
self._client = client
|
|
18
|
+
|
|
19
|
+
def login(self, username: str, password: str) -> LoginAuth:
|
|
20
|
+
"""Authenticate and store the returned session token.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
username: Cyclos login name.
|
|
24
|
+
password: Cyclos password. Never logged.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Parsed login response including ``session_token``.
|
|
28
|
+
"""
|
|
29
|
+
response = self._client._transport.post(
|
|
30
|
+
"/auth/session",
|
|
31
|
+
auth=(username, password),
|
|
32
|
+
)
|
|
33
|
+
data = LoginAuth.model_validate(response.json())
|
|
34
|
+
if data.session_token:
|
|
35
|
+
self._client._transport.session_token = data.session_token
|
|
36
|
+
return data
|
|
37
|
+
|
|
38
|
+
def logout(self) -> None:
|
|
39
|
+
"""Log out the current session and clear local authentication state."""
|
|
40
|
+
self._client._transport.delete("/auth/session")
|
|
41
|
+
self._client._transport.session_token = None
|
|
42
|
+
self._client._transport.access_client_token = None
|
|
43
|
+
|
|
44
|
+
def current_user(self) -> Auth:
|
|
45
|
+
"""Return information about the currently authenticated user."""
|
|
46
|
+
response = self._client._transport.get("/auth")
|
|
47
|
+
return Auth.model_validate(response.json())
|
|
48
|
+
|
|
49
|
+
def is_authenticated(self) -> bool:
|
|
50
|
+
"""Return ``True`` if a session token is currently stored."""
|
|
51
|
+
return self._client._transport.session_token is not None
|
|
52
|
+
|
|
53
|
+
def refresh(self) -> LoginAuth:
|
|
54
|
+
"""Refresh the current session if supported.
|
|
55
|
+
|
|
56
|
+
Cyclos 4.16 does not expose a dedicated refresh endpoint. This method
|
|
57
|
+
re-authenticates using the credentials stored in the configuration
|
|
58
|
+
(if available). Prefer explicit ``login`` calls if credentials are not
|
|
59
|
+
configured.
|
|
60
|
+
"""
|
|
61
|
+
config = self._client._config
|
|
62
|
+
if not config.username or not config.password:
|
|
63
|
+
raise RuntimeError("Cannot refresh session: no credentials configured")
|
|
64
|
+
return self.login(config.username, config.password.get_secret_value())
|
|
65
|
+
|
|
66
|
+
def set_session_token(self, token: str) -> None:
|
|
67
|
+
"""Manually set the session token for subsequent requests."""
|
|
68
|
+
self._client._transport.session_token = token
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Asynchronous authentication client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from angstrom_cyclos.auth.models import Auth, LoginAuth
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from angstrom_cyclos.async_client import AsyncCyclosClient
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AsyncAuthClient:
|
|
14
|
+
"""Async authentication client."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, client: AsyncCyclosClient) -> None:
|
|
17
|
+
self._client = client
|
|
18
|
+
|
|
19
|
+
async def login(self, username: str, password: str) -> LoginAuth:
|
|
20
|
+
response = await self._client._transport.post(
|
|
21
|
+
"/auth/session",
|
|
22
|
+
auth=(username, password),
|
|
23
|
+
)
|
|
24
|
+
data = LoginAuth.model_validate(response.json())
|
|
25
|
+
if data.session_token:
|
|
26
|
+
self._client._transport.session_token = data.session_token
|
|
27
|
+
return data
|
|
28
|
+
|
|
29
|
+
async def logout(self) -> None:
|
|
30
|
+
await self._client._transport.delete("/auth/session")
|
|
31
|
+
self._client._transport.session_token = None
|
|
32
|
+
self._client._transport.access_client_token = None
|
|
33
|
+
|
|
34
|
+
async def current_user(self) -> Auth:
|
|
35
|
+
response = await self._client._transport.get("/auth")
|
|
36
|
+
return Auth.model_validate(response.json())
|
|
37
|
+
|
|
38
|
+
async def is_authenticated(self) -> bool:
|
|
39
|
+
return self._client._transport.session_token is not None
|
|
40
|
+
|
|
41
|
+
async def refresh(self) -> LoginAuth:
|
|
42
|
+
config = self._client._config
|
|
43
|
+
if not config.username or not config.password:
|
|
44
|
+
raise RuntimeError("Cannot refresh session: no credentials configured")
|
|
45
|
+
return await self.login(config.username, config.password.get_secret_value())
|
|
46
|
+
|
|
47
|
+
async def set_session_token(self, token: str) -> None:
|
|
48
|
+
self._client._transport.session_token = token
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Authentication-related Pydantic models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
from angstrom_cyclos.models import CyclosBaseModel, EntityReference, UserReference
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LoginAuth(CyclosBaseModel):
|
|
11
|
+
"""Response from ``POST /auth/session``."""
|
|
12
|
+
|
|
13
|
+
user: UserReference | None = None
|
|
14
|
+
session_token: str | None = Field(default=None, alias="sessionToken")
|
|
15
|
+
principal: str | None = None
|
|
16
|
+
expired_password: bool | None = Field(default=None, alias="expiredPassword")
|
|
17
|
+
pending_agreements: bool | None = Field(default=None, alias="pendingAgreements")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Auth(CyclosBaseModel):
|
|
21
|
+
"""Response from ``GET /auth``."""
|
|
22
|
+
|
|
23
|
+
user: UserReference | None = None
|
|
24
|
+
principal: str | None = None
|
|
25
|
+
access_client: EntityReference | None = Field(default=None, alias="accessClient")
|
|
26
|
+
expired_password: bool | None = Field(default=None, alias="expiredPassword")
|
|
27
|
+
pending_agreements: bool | None = Field(default=None, alias="pendingAgreements")
|