wayfinder-paths 0.1.11__py3-none-any.whl → 0.1.14__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.
Potentially problematic release.
This version of wayfinder-paths might be problematic. Click here for more details.
- wayfinder_paths/adapters/balance_adapter/README.md +13 -14
- wayfinder_paths/adapters/balance_adapter/adapter.py +36 -39
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +123 -0
- wayfinder_paths/adapters/brap_adapter/README.md +11 -16
- wayfinder_paths/adapters/brap_adapter/adapter.py +87 -75
- wayfinder_paths/adapters/brap_adapter/examples.json +63 -52
- wayfinder_paths/adapters/brap_adapter/test_adapter.py +121 -59
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +22 -23
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +114 -60
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +1 -1
- wayfinder_paths/adapters/hyperliquid_adapter/executor.py +44 -5
- wayfinder_paths/adapters/hyperliquid_adapter/test_executor.py +104 -0
- wayfinder_paths/adapters/moonwell_adapter/adapter.py +0 -3
- wayfinder_paths/adapters/pool_adapter/README.md +11 -27
- wayfinder_paths/adapters/pool_adapter/adapter.py +11 -37
- wayfinder_paths/adapters/pool_adapter/examples.json +6 -7
- wayfinder_paths/adapters/pool_adapter/test_adapter.py +8 -8
- wayfinder_paths/adapters/token_adapter/README.md +2 -14
- wayfinder_paths/adapters/token_adapter/adapter.py +16 -10
- wayfinder_paths/adapters/token_adapter/examples.json +4 -8
- wayfinder_paths/adapters/token_adapter/test_adapter.py +5 -3
- wayfinder_paths/core/clients/BRAPClient.py +103 -62
- wayfinder_paths/core/clients/ClientManager.py +1 -68
- wayfinder_paths/core/clients/HyperlendClient.py +127 -66
- wayfinder_paths/core/clients/LedgerClient.py +1 -4
- wayfinder_paths/core/clients/PoolClient.py +126 -88
- wayfinder_paths/core/clients/TokenClient.py +92 -37
- wayfinder_paths/core/clients/WalletClient.py +28 -58
- wayfinder_paths/core/clients/WayfinderClient.py +33 -166
- wayfinder_paths/core/clients/__init__.py +0 -2
- wayfinder_paths/core/clients/protocols.py +35 -52
- wayfinder_paths/core/clients/sdk_example.py +37 -22
- wayfinder_paths/core/config.py +60 -224
- wayfinder_paths/core/engine/StrategyJob.py +7 -55
- wayfinder_paths/core/services/local_evm_txn.py +28 -10
- wayfinder_paths/core/services/local_token_txn.py +1 -1
- wayfinder_paths/core/strategies/Strategy.py +3 -5
- wayfinder_paths/core/strategies/descriptors.py +7 -0
- wayfinder_paths/core/utils/evm_helpers.py +7 -3
- wayfinder_paths/core/utils/wallets.py +12 -19
- wayfinder_paths/core/wallets/README.md +1 -1
- wayfinder_paths/run_strategy.py +8 -17
- wayfinder_paths/scripts/create_strategy.py +5 -5
- wayfinder_paths/scripts/make_wallets.py +5 -5
- wayfinder_paths/scripts/run_strategy.py +3 -3
- wayfinder_paths/strategies/basis_trading_strategy/snapshot_mixin.py +1 -1
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +206 -526
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +228 -11
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +2 -2
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +41 -25
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +54 -9
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +1 -1
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +10 -9
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +12 -6
- wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +3 -3
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +110 -78
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +44 -21
- wayfinder_paths/templates/adapter/README.md +1 -1
- wayfinder_paths/templates/strategy/README.md +3 -3
- wayfinder_paths/templates/strategy/test_strategy.py +3 -2
- {wayfinder_paths-0.1.11.dist-info → wayfinder_paths-0.1.14.dist-info}/METADATA +21 -59
- {wayfinder_paths-0.1.11.dist-info → wayfinder_paths-0.1.14.dist-info}/RECORD +64 -65
- wayfinder_paths/core/clients/AuthClient.py +0 -83
- wayfinder_paths/core/settings.py +0 -61
- {wayfinder_paths-0.1.11.dist-info → wayfinder_paths-0.1.14.dist-info}/LICENSE +0 -0
- {wayfinder_paths-0.1.11.dist-info → wayfinder_paths-0.1.14.dist-info}/WHEEL +0 -0
|
@@ -7,83 +7,138 @@ from __future__ import annotations
|
|
|
7
7
|
|
|
8
8
|
from typing import NotRequired, Required, TypedDict
|
|
9
9
|
|
|
10
|
-
from wayfinder_paths.core.clients.AuthClient import AuthClient
|
|
11
10
|
from wayfinder_paths.core.clients.WayfinderClient import WayfinderClient
|
|
12
|
-
from wayfinder_paths.core.
|
|
11
|
+
from wayfinder_paths.core.config import get_api_base_url
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TokenLinks(TypedDict):
|
|
15
|
+
"""Token links structure"""
|
|
16
|
+
|
|
17
|
+
github: NotRequired[list[str]]
|
|
18
|
+
reddit: NotRequired[str]
|
|
19
|
+
discord: NotRequired[str]
|
|
20
|
+
twitter: NotRequired[str]
|
|
21
|
+
homepage: NotRequired[list[str]]
|
|
22
|
+
telegram: NotRequired[str]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ChainAddress(TypedDict):
|
|
26
|
+
"""Chain address structure"""
|
|
27
|
+
|
|
28
|
+
address: Required[str]
|
|
29
|
+
token_id: Required[str]
|
|
30
|
+
is_contract: NotRequired[bool]
|
|
31
|
+
chain_id: NotRequired[int]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ChainInfo(TypedDict):
|
|
35
|
+
"""Chain information structure"""
|
|
36
|
+
|
|
37
|
+
id: Required[int]
|
|
38
|
+
name: Required[str]
|
|
39
|
+
code: Required[str]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TokenMetadata(TypedDict):
|
|
43
|
+
"""Token metadata structure"""
|
|
44
|
+
|
|
45
|
+
query_processed: NotRequired[str]
|
|
46
|
+
query_type: NotRequired[str]
|
|
47
|
+
has_addresses: NotRequired[bool]
|
|
48
|
+
address_count: NotRequired[int]
|
|
49
|
+
has_price_data: NotRequired[bool]
|
|
13
50
|
|
|
14
51
|
|
|
15
52
|
class TokenDetails(TypedDict):
|
|
16
53
|
"""Token details response structure"""
|
|
17
54
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
symbol: Required[str]
|
|
55
|
+
asset_id: NotRequired[str]
|
|
56
|
+
token_ids: NotRequired[list[str]]
|
|
21
57
|
name: Required[str]
|
|
58
|
+
symbol: Required[str]
|
|
22
59
|
decimals: Required[int]
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
60
|
+
description: NotRequired[str]
|
|
61
|
+
links: NotRequired[TokenLinks]
|
|
62
|
+
categories: NotRequired[list[str]]
|
|
63
|
+
current_price: NotRequired[float]
|
|
64
|
+
market_cap: NotRequired[float]
|
|
65
|
+
total_volume_usd_24h: NotRequired[float]
|
|
66
|
+
price_change_24h: NotRequired[float]
|
|
67
|
+
price_change_7d: NotRequired[float]
|
|
68
|
+
price_change_30d: NotRequired[float]
|
|
69
|
+
price_change_1y: NotRequired[float]
|
|
70
|
+
addresses: NotRequired[dict[str, str]]
|
|
71
|
+
chain_addresses: NotRequired[dict[str, ChainAddress]]
|
|
72
|
+
chain_ids: NotRequired[dict[str, int]]
|
|
73
|
+
id: NotRequired[int]
|
|
74
|
+
token_id: Required[str]
|
|
75
|
+
address: Required[str]
|
|
76
|
+
chain: NotRequired[ChainInfo]
|
|
77
|
+
query: NotRequired[str]
|
|
78
|
+
query_type: NotRequired[str]
|
|
79
|
+
metadata: NotRequired[TokenMetadata]
|
|
27
80
|
image_url: NotRequired[str | None]
|
|
28
|
-
coingecko_id: NotRequired[str | None]
|
|
29
81
|
|
|
30
82
|
|
|
31
83
|
class GasToken(TypedDict):
|
|
32
84
|
"""Gas token response structure"""
|
|
33
85
|
|
|
34
86
|
id: Required[str]
|
|
35
|
-
|
|
36
|
-
|
|
87
|
+
coingecko_id: NotRequired[str]
|
|
88
|
+
token_id: Required[str]
|
|
37
89
|
name: Required[str]
|
|
90
|
+
symbol: Required[str]
|
|
91
|
+
address: Required[str]
|
|
38
92
|
decimals: Required[int]
|
|
39
|
-
|
|
40
|
-
chain_code: Required[str]
|
|
41
|
-
price_usd: NotRequired[float]
|
|
93
|
+
chain: NotRequired[ChainInfo]
|
|
42
94
|
|
|
43
95
|
|
|
44
96
|
class TokenClient(WayfinderClient):
|
|
45
97
|
"""Adapter for token-related operations"""
|
|
46
98
|
|
|
47
|
-
def __init__(self
|
|
48
|
-
super().__init__(
|
|
49
|
-
self.api_base_url = f"{
|
|
50
|
-
self._auth_client: AuthClient | None = AuthClient(api_key=api_key)
|
|
51
|
-
|
|
52
|
-
# ============== Public (No-Auth) Endpoints ==============
|
|
99
|
+
def __init__(self):
|
|
100
|
+
super().__init__()
|
|
101
|
+
self.api_base_url = f"{get_api_base_url()}/v1/blockchain/tokens"
|
|
53
102
|
|
|
54
103
|
async def get_token_details(
|
|
55
|
-
self,
|
|
104
|
+
self, query: str, market_data: bool = False, chain_id: int | None = None
|
|
56
105
|
) -> TokenDetails:
|
|
57
106
|
"""
|
|
58
|
-
Get token data including price from the token-details endpoint
|
|
107
|
+
Get token data including price from the token-details endpoint.
|
|
59
108
|
|
|
60
109
|
Args:
|
|
61
|
-
|
|
110
|
+
query: Token identifier, address, or symbol to query
|
|
111
|
+
market_data: Whether to include market data (default: True)
|
|
112
|
+
chain_id: Optional chain ID
|
|
62
113
|
|
|
63
114
|
Returns:
|
|
64
115
|
Full token data including price information
|
|
65
116
|
"""
|
|
66
|
-
url = f"{
|
|
117
|
+
url = f"{self.api_base_url}/detail/"
|
|
67
118
|
params = {
|
|
68
|
-
"query":
|
|
69
|
-
"
|
|
70
|
-
"force_refresh": str(force_refresh),
|
|
119
|
+
"query": query,
|
|
120
|
+
"market_data": market_data,
|
|
71
121
|
}
|
|
72
|
-
|
|
73
|
-
|
|
122
|
+
if chain_id is not None:
|
|
123
|
+
params["chain_id"] = chain_id
|
|
124
|
+
response = await self._authed_request("GET", url, params=params)
|
|
74
125
|
response.raise_for_status()
|
|
75
126
|
data = response.json()
|
|
76
127
|
return data.get("data", data)
|
|
77
128
|
|
|
78
|
-
async def get_gas_token(self,
|
|
129
|
+
async def get_gas_token(self, query: str) -> GasToken:
|
|
79
130
|
"""
|
|
80
|
-
Fetch the native gas token for a given chain code
|
|
81
|
-
|
|
131
|
+
Fetch the native gas token for a given chain code or query.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
query: Chain code or query string
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Gas token information including chain details
|
|
82
138
|
"""
|
|
83
|
-
url = f"{
|
|
84
|
-
params = {"
|
|
85
|
-
|
|
86
|
-
response = await self._request("GET", url, params=params, headers={})
|
|
139
|
+
url = f"{self.api_base_url}/gas/"
|
|
140
|
+
params = {"query": query}
|
|
141
|
+
response = await self._authed_request("GET", url, params=params)
|
|
87
142
|
response.raise_for_status()
|
|
88
143
|
data = response.json()
|
|
89
144
|
return data.get("data", data)
|
|
@@ -7,80 +7,50 @@ from __future__ import annotations
|
|
|
7
7
|
|
|
8
8
|
from typing import NotRequired, Required, TypedDict
|
|
9
9
|
|
|
10
|
-
from wayfinder_paths.core.clients.AuthClient import AuthClient
|
|
11
10
|
from wayfinder_paths.core.clients.WayfinderClient import WayfinderClient
|
|
12
|
-
from wayfinder_paths.core.
|
|
11
|
+
from wayfinder_paths.core.config import get_api_base_url
|
|
13
12
|
|
|
14
13
|
|
|
15
|
-
class
|
|
16
|
-
"""
|
|
14
|
+
class AddressBalance(TypedDict):
|
|
15
|
+
"""Balance response structure for address/query lookups."""
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
wallet_address: Required[str]
|
|
20
|
-
balance: Required[str]
|
|
21
|
-
balance_human: NotRequired[float | None]
|
|
22
|
-
usd_value: NotRequired[float | None]
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class PoolBalance(TypedDict):
|
|
26
|
-
"""Pool balance response structure"""
|
|
27
|
-
|
|
28
|
-
pool_address: Required[str]
|
|
29
|
-
chain_id: Required[int]
|
|
30
|
-
user_address: Required[str]
|
|
31
|
-
balance: Required[str]
|
|
17
|
+
balance: Required[int]
|
|
32
18
|
balance_human: NotRequired[float | None]
|
|
33
19
|
usd_value: NotRequired[float | None]
|
|
20
|
+
address: NotRequired[str]
|
|
21
|
+
token_id: NotRequired[str | None]
|
|
22
|
+
wallet_address: NotRequired[str]
|
|
23
|
+
chain_id: NotRequired[int]
|
|
34
24
|
|
|
35
25
|
|
|
36
26
|
class WalletClient(WayfinderClient):
|
|
37
|
-
def __init__(self
|
|
38
|
-
super().__init__(
|
|
39
|
-
self.api_base_url =
|
|
40
|
-
self._auth_client = AuthClient(api_key=api_key)
|
|
27
|
+
def __init__(self):
|
|
28
|
+
super().__init__()
|
|
29
|
+
self.api_base_url = get_api_base_url()
|
|
41
30
|
|
|
42
|
-
async def
|
|
31
|
+
async def get_token_balance_for_address(
|
|
43
32
|
self,
|
|
44
33
|
*,
|
|
45
|
-
token_id: str,
|
|
46
34
|
wallet_address: str,
|
|
47
|
-
|
|
48
|
-
|
|
35
|
+
query: str,
|
|
36
|
+
chain_id: int | None = None,
|
|
37
|
+
) -> AddressBalance:
|
|
49
38
|
"""
|
|
50
|
-
Fetch a
|
|
39
|
+
Fetch a balance for a wallet address + chain + query.
|
|
51
40
|
|
|
52
|
-
|
|
41
|
+
Args:
|
|
42
|
+
wallet_address: Wallet address
|
|
43
|
+
query: Token address, pool address, or equivalent identifier
|
|
44
|
+
chain_id: Chain ID (required)
|
|
53
45
|
"""
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"token_id": token_id,
|
|
57
|
-
"wallet_address": wallet_address,
|
|
58
|
-
"human_readable": human_readable,
|
|
59
|
-
}
|
|
60
|
-
response = await self._authed_request("POST", url, json=payload)
|
|
61
|
-
data = response.json()
|
|
62
|
-
return data.get("data", data)
|
|
46
|
+
if chain_id is None:
|
|
47
|
+
raise ValueError("chain_id is required")
|
|
63
48
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
pool_address: str,
|
|
68
|
-
chain_id: int,
|
|
69
|
-
user_address: str,
|
|
70
|
-
human_readable: bool = True,
|
|
71
|
-
) -> PoolBalance:
|
|
72
|
-
"""
|
|
73
|
-
Fetch a wallet's LP/share balance for a given pool address and chain.
|
|
74
|
-
|
|
75
|
-
Mirrors POST /api/v1/public/balances/pool/
|
|
76
|
-
"""
|
|
77
|
-
url = f"{self.api_base_url}/public/balances/pool/"
|
|
78
|
-
payload = {
|
|
79
|
-
"pool_address": pool_address,
|
|
49
|
+
url = f"{self.api_base_url}/v1/blockchain/balances/address/"
|
|
50
|
+
params = {
|
|
51
|
+
"wallet_address": wallet_address,
|
|
80
52
|
"chain_id": chain_id,
|
|
81
|
-
"
|
|
82
|
-
"human_readable": human_readable,
|
|
53
|
+
"query": query,
|
|
83
54
|
}
|
|
84
|
-
response = await self._authed_request("
|
|
85
|
-
|
|
86
|
-
return data.get("data", data)
|
|
55
|
+
response = await self._authed_request("GET", url, params=params)
|
|
56
|
+
return response.json()
|
|
@@ -1,25 +1,22 @@
|
|
|
1
1
|
import json
|
|
2
|
-
import os
|
|
3
2
|
import time
|
|
4
3
|
from typing import Any
|
|
5
4
|
|
|
6
5
|
import httpx
|
|
7
6
|
from loguru import logger
|
|
8
7
|
|
|
8
|
+
from wayfinder_paths.core.config import get_api_base_url
|
|
9
9
|
from wayfinder_paths.core.constants.base import DEFAULT_HTTP_TIMEOUT
|
|
10
|
-
from wayfinder_paths.core.settings import settings
|
|
11
10
|
|
|
12
11
|
|
|
13
12
|
class WayfinderClient:
|
|
14
|
-
def __init__(self
|
|
13
|
+
def __init__(self):
|
|
15
14
|
"""
|
|
16
15
|
Initialize WayfinderClient.
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
api_key: Optional API key for service account authentication.
|
|
20
|
-
If provided, uses API key auth. Otherwise falls back to config.json.
|
|
17
|
+
API key is loaded from system.api_key in config.json.
|
|
21
18
|
"""
|
|
22
|
-
self.api_base_url = f"{
|
|
19
|
+
self.api_base_url = f"{get_api_base_url()}/"
|
|
23
20
|
timeout = httpx.Timeout(DEFAULT_HTTP_TIMEOUT)
|
|
24
21
|
self.client = httpx.AsyncClient(timeout=timeout)
|
|
25
22
|
|
|
@@ -27,154 +24,51 @@ class WayfinderClient:
|
|
|
27
24
|
"Content-Type": "application/json",
|
|
28
25
|
}
|
|
29
26
|
|
|
30
|
-
self._api_key: str | None = api_key
|
|
31
|
-
self._access_token: str | None = None
|
|
32
|
-
self._refresh_token: str | None = None
|
|
33
|
-
|
|
34
|
-
def set_bearer_token(self, token: str) -> None:
|
|
35
|
-
"""
|
|
36
|
-
Set runtime OAuth/JWT Bearer token for Django-backed Wayfinder services.
|
|
37
|
-
"""
|
|
38
|
-
self.headers["Authorization"] = f"Bearer {token}"
|
|
39
|
-
self._access_token = token
|
|
40
|
-
|
|
41
|
-
def set_tokens(self, access: str | None, refresh: str | None) -> None:
|
|
42
|
-
"""Set both access and refresh tokens and configure Authorization header."""
|
|
43
|
-
if access:
|
|
44
|
-
self.set_bearer_token(access)
|
|
45
|
-
if refresh:
|
|
46
|
-
self._refresh_token = refresh
|
|
47
|
-
|
|
48
27
|
def clear_auth(self) -> None:
|
|
49
|
-
"""Clear
|
|
50
|
-
self.headers.pop("
|
|
51
|
-
|
|
52
|
-
async def _refresh_access_token(self) -> bool:
|
|
53
|
-
"""Attempt to refresh access token using stored refresh token."""
|
|
54
|
-
if not self._refresh_token:
|
|
55
|
-
logger.debug("No refresh token available")
|
|
56
|
-
return False
|
|
57
|
-
try:
|
|
58
|
-
logger.info("Attempting to refresh access token")
|
|
59
|
-
start_time = time.time()
|
|
60
|
-
url = f"{settings.WAYFINDER_API_URL}/auth/token/refresh/"
|
|
61
|
-
payload = {"refresh": self._refresh_token}
|
|
62
|
-
response = await self.client.post(
|
|
63
|
-
url, json=payload, headers={"Content-Type": "application/json"}
|
|
64
|
-
)
|
|
65
|
-
if response.status_code != 200:
|
|
66
|
-
logger.warning(
|
|
67
|
-
f"Token refresh failed with status {response.status_code}"
|
|
68
|
-
)
|
|
69
|
-
return False
|
|
70
|
-
data = response.json()
|
|
71
|
-
new_access = data.get("access") or data.get("access_token")
|
|
72
|
-
if not new_access:
|
|
73
|
-
logger.warning("No access token in refresh response")
|
|
74
|
-
return False
|
|
75
|
-
self.set_bearer_token(new_access)
|
|
76
|
-
elapsed = time.time() - start_time
|
|
77
|
-
logger.info(f"Access token refreshed successfully in {elapsed:.2f}s")
|
|
78
|
-
return True
|
|
79
|
-
except Exception as e:
|
|
80
|
-
elapsed = time.time() - start_time
|
|
81
|
-
logger.error(f"Token refresh failed after {elapsed:.2f}s: {e}")
|
|
82
|
-
return False
|
|
28
|
+
"""Clear X-API-KEY header."""
|
|
29
|
+
self.headers.pop("X-API-KEY", None)
|
|
83
30
|
|
|
84
31
|
def _load_config_credentials(self) -> dict[str, str | None]:
|
|
85
32
|
"""
|
|
86
|
-
Load
|
|
33
|
+
Load API key from config.json.
|
|
87
34
|
Expected shape:
|
|
88
35
|
{
|
|
89
|
-
"user": { "username": ..., "password": ..., "refresh_token": ..., "api_key": ... },
|
|
90
36
|
"system": { "api_key": ... }
|
|
91
37
|
}
|
|
92
38
|
"""
|
|
93
|
-
path = os.getenv("WAYFINDER_CONFIG_PATH", "config.json")
|
|
94
39
|
try:
|
|
95
|
-
with open(
|
|
40
|
+
with open("config.json") as f:
|
|
96
41
|
cfg = json.load(f)
|
|
97
|
-
user = cfg.get("user", {}) if isinstance(cfg, dict) else {}
|
|
98
42
|
system = cfg.get("system", {}) if isinstance(cfg, dict) else {}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
"password": user.get("password"),
|
|
102
|
-
"refresh_token": user.get("refresh_token"),
|
|
103
|
-
"api_key": user.get("api_key") or system.get("api_key"),
|
|
104
|
-
}
|
|
43
|
+
api_key = system.get("api_key")
|
|
44
|
+
return {"api_key": api_key}
|
|
105
45
|
except (FileNotFoundError, json.JSONDecodeError, OSError) as e:
|
|
106
|
-
logger.debug(f"Could not load config file at
|
|
107
|
-
return {
|
|
108
|
-
"username": None,
|
|
109
|
-
"password": None,
|
|
110
|
-
"refresh_token": None,
|
|
111
|
-
"api_key": None,
|
|
112
|
-
}
|
|
46
|
+
logger.debug(f"Could not load config file at config.json: {e}")
|
|
47
|
+
return {"api_key": None}
|
|
113
48
|
except Exception as e:
|
|
114
|
-
logger.warning(f"Unexpected error loading config file at
|
|
115
|
-
return {
|
|
116
|
-
"username": None,
|
|
117
|
-
"password": None,
|
|
118
|
-
"refresh_token": None,
|
|
119
|
-
"api_key": None,
|
|
120
|
-
}
|
|
49
|
+
logger.warning(f"Unexpected error loading config file at config.json: {e}")
|
|
50
|
+
return {"api_key": None}
|
|
121
51
|
|
|
122
|
-
|
|
52
|
+
def _ensure_api_key(self) -> bool:
|
|
123
53
|
"""
|
|
124
|
-
Ensure
|
|
125
|
-
Raises PermissionError if no
|
|
54
|
+
Ensure X-API-KEY header is set from system.api_key in config.json.
|
|
55
|
+
Raises PermissionError if no API key found.
|
|
126
56
|
"""
|
|
127
|
-
if self.headers.get("
|
|
57
|
+
if self.headers.get("X-API-KEY"):
|
|
128
58
|
return True
|
|
129
59
|
|
|
130
|
-
|
|
131
|
-
api_key =
|
|
132
|
-
if not api_key:
|
|
133
|
-
creds = self._load_config_credentials()
|
|
134
|
-
api_key = creds.get("api_key") or os.getenv("WAYFINDER_API_KEY")
|
|
60
|
+
creds = self._load_config_credentials()
|
|
61
|
+
api_key = creds.get("api_key")
|
|
135
62
|
|
|
136
63
|
if api_key:
|
|
137
64
|
api_key = api_key.strip() if isinstance(api_key, str) else api_key
|
|
138
65
|
if not api_key:
|
|
139
66
|
raise ValueError("API key cannot be empty")
|
|
140
|
-
self.headers["
|
|
67
|
+
self.headers["X-API-KEY"] = api_key
|
|
141
68
|
return True
|
|
142
69
|
|
|
143
|
-
# Fall back to OAuth token-based auth
|
|
144
|
-
creds = self._load_config_credentials()
|
|
145
|
-
refresh = creds.get("refresh_token")
|
|
146
|
-
|
|
147
|
-
if refresh:
|
|
148
|
-
self._refresh_token = refresh
|
|
149
|
-
refreshed = await self._refresh_access_token()
|
|
150
|
-
if refreshed:
|
|
151
|
-
return True
|
|
152
|
-
|
|
153
|
-
username = creds.get("username")
|
|
154
|
-
password = creds.get("password")
|
|
155
|
-
|
|
156
|
-
if username and password:
|
|
157
|
-
try:
|
|
158
|
-
url = f"{settings.WAYFINDER_API_URL}/auth/token/"
|
|
159
|
-
payload = {"username": username, "password": password}
|
|
160
|
-
response = await self.client.post(
|
|
161
|
-
url,
|
|
162
|
-
json=payload,
|
|
163
|
-
headers={"Content-Type": "application/json"},
|
|
164
|
-
)
|
|
165
|
-
if response.status_code == 200:
|
|
166
|
-
data = response.json()
|
|
167
|
-
access = data.get("access") or data.get("access_token")
|
|
168
|
-
refresh = data.get("refresh") or data.get("refresh_token")
|
|
169
|
-
self.set_tokens(access, refresh)
|
|
170
|
-
return bool(access)
|
|
171
|
-
except Exception as e:
|
|
172
|
-
logger.debug(f"Failed to authenticate with username/password: {e}")
|
|
173
|
-
pass
|
|
174
|
-
|
|
175
70
|
raise PermissionError(
|
|
176
|
-
"Not authenticated: provide api_key
|
|
177
|
-
"or username+password/refresh_token in config.json for personal access"
|
|
71
|
+
"Not authenticated: provide api_key in system.api_key in config.json"
|
|
178
72
|
)
|
|
179
73
|
|
|
180
74
|
async def _request(
|
|
@@ -183,49 +77,32 @@ class WayfinderClient:
|
|
|
183
77
|
url: str,
|
|
184
78
|
*,
|
|
185
79
|
headers: dict[str, str] | None = None,
|
|
186
|
-
retry_on_401: bool =
|
|
80
|
+
retry_on_401: bool = False,
|
|
187
81
|
**kwargs: Any,
|
|
188
82
|
) -> httpx.Response:
|
|
189
83
|
"""
|
|
190
|
-
Wrapper around httpx that injects
|
|
191
|
-
Ensures API key
|
|
84
|
+
Wrapper around httpx that injects X-API-KEY header.
|
|
85
|
+
Ensures API key is set in headers when available (for authentication and rate limiting).
|
|
192
86
|
"""
|
|
193
87
|
logger.debug(f"Making {method} request to {url}")
|
|
194
88
|
start_time = time.time()
|
|
195
89
|
|
|
196
|
-
# Ensure API key
|
|
90
|
+
# Ensure API key is set in headers if available and not already set
|
|
197
91
|
# This ensures API keys are passed to all endpoints (including public ones) for rate limiting
|
|
198
|
-
if not self.headers.get("
|
|
199
|
-
|
|
200
|
-
api_key =
|
|
201
|
-
if not api_key:
|
|
202
|
-
creds = self._load_config_credentials()
|
|
203
|
-
api_key = creds.get("api_key") or os.getenv("WAYFINDER_API_KEY")
|
|
92
|
+
if not self.headers.get("X-API-KEY"):
|
|
93
|
+
creds = self._load_config_credentials()
|
|
94
|
+
api_key = creds.get("api_key")
|
|
204
95
|
|
|
205
96
|
if api_key:
|
|
206
97
|
api_key = api_key.strip() if isinstance(api_key, str) else api_key
|
|
207
98
|
if api_key:
|
|
208
|
-
self.headers["
|
|
99
|
+
self.headers["X-API-KEY"] = api_key
|
|
209
100
|
|
|
210
101
|
merged_headers = dict(self.headers)
|
|
211
102
|
if headers:
|
|
212
103
|
merged_headers.update(headers)
|
|
213
104
|
resp = await self.client.request(method, url, headers=merged_headers, **kwargs)
|
|
214
105
|
|
|
215
|
-
if resp.status_code == 401 and retry_on_401 and self._refresh_token:
|
|
216
|
-
logger.info("Received 401, attempting token refresh and retry")
|
|
217
|
-
refreshed = await self._refresh_access_token()
|
|
218
|
-
if refreshed:
|
|
219
|
-
merged_headers = dict(self.headers)
|
|
220
|
-
if headers:
|
|
221
|
-
merged_headers.update(headers)
|
|
222
|
-
resp = await self.client.request(
|
|
223
|
-
method, url, headers=merged_headers, **kwargs
|
|
224
|
-
)
|
|
225
|
-
logger.info("Retry after token refresh successful")
|
|
226
|
-
else:
|
|
227
|
-
logger.error("Token refresh failed, request will fail")
|
|
228
|
-
|
|
229
106
|
elapsed = time.time() - start_time
|
|
230
107
|
if resp.status_code >= 400:
|
|
231
108
|
logger.warning(
|
|
@@ -248,17 +125,7 @@ class WayfinderClient:
|
|
|
248
125
|
**kwargs: Any,
|
|
249
126
|
) -> httpx.Response:
|
|
250
127
|
"""
|
|
251
|
-
Ensure
|
|
252
|
-
Retries once on 401 by re-acquiring tokens.
|
|
128
|
+
Ensure X-API-KEY header is set (from system.api_key in config.json) and perform the request.
|
|
253
129
|
"""
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
raise PermissionError("Not authenticated: set env tokens or credentials")
|
|
257
|
-
try:
|
|
258
|
-
return await self._request(method, url, headers=headers, **kwargs)
|
|
259
|
-
except httpx.HTTPStatusError as e:
|
|
260
|
-
if e.response is not None and e.response.status_code == 401:
|
|
261
|
-
# Retry after attempting re-acquire/refresh
|
|
262
|
-
await self._ensure_bearer_token()
|
|
263
|
-
return await self._request(method, url, headers=headers, **kwargs)
|
|
264
|
-
raise
|
|
130
|
+
self._ensure_api_key()
|
|
131
|
+
return await self._request(method, url, headers=headers, **kwargs)
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
Core client modules for API communication
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
from wayfinder_paths.core.clients.AuthClient import AuthClient
|
|
6
5
|
from wayfinder_paths.core.clients.BRAPClient import BRAPClient
|
|
7
6
|
from wayfinder_paths.core.clients.ClientManager import ClientManager
|
|
8
7
|
from wayfinder_paths.core.clients.HyperlendClient import HyperlendClient
|
|
@@ -23,7 +22,6 @@ from wayfinder_paths.core.clients.WayfinderClient import WayfinderClient
|
|
|
23
22
|
__all__ = [
|
|
24
23
|
"WayfinderClient",
|
|
25
24
|
"ClientManager",
|
|
26
|
-
"AuthClient",
|
|
27
25
|
"TokenClient",
|
|
28
26
|
"WalletClient",
|
|
29
27
|
"LedgerClient",
|