iis-access 0.1.1__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.
- iis_access/__init__.py +19 -0
- iis_access/_types.py +23 -0
- iis_access/auth.py +150 -0
- iis_access/config.py +45 -0
- iis_access/managed_keys.py +54 -0
- iis_access/nexus.py +19 -0
- iis_access/provider.py +142 -0
- iis_access/sync.py +30 -0
- iis_access/usage.py +40 -0
- iis_access-0.1.1.dist-info/METADATA +48 -0
- iis_access-0.1.1.dist-info/RECORD +12 -0
- iis_access-0.1.1.dist-info/WHEEL +4 -0
iis_access/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""IIS Tools Python access library - optional auth integration for CLI tools."""
|
|
2
|
+
|
|
3
|
+
from ._types import AuthContext, ProviderResolution, NoProviderAvailable
|
|
4
|
+
from .auth import get_auth_context, is_authenticated
|
|
5
|
+
from .provider import resolve_provider, register_free_provider
|
|
6
|
+
from .managed_keys import get_managed_key
|
|
7
|
+
from .usage import report_usage
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"AuthContext",
|
|
11
|
+
"ProviderResolution",
|
|
12
|
+
"NoProviderAvailable",
|
|
13
|
+
"get_auth_context",
|
|
14
|
+
"is_authenticated",
|
|
15
|
+
"resolve_provider",
|
|
16
|
+
"register_free_provider",
|
|
17
|
+
"get_managed_key",
|
|
18
|
+
"report_usage",
|
|
19
|
+
]
|
iis_access/_types.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass
|
|
5
|
+
class AuthContext:
|
|
6
|
+
account_id: str
|
|
7
|
+
access_token: str
|
|
8
|
+
plan: str # "free", "pro", "enterprise"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class ProviderResolution:
|
|
13
|
+
provider: str # "openai", "elevenlabs", "edge-tts", etc.
|
|
14
|
+
method: str # "nexus", "managed_key", "local_key", "free"
|
|
15
|
+
api_key: str | None # The key to use (None for free providers)
|
|
16
|
+
endpoint: str | None # Override endpoint (for Nexus)
|
|
17
|
+
credits_estimated: float | None # Estimated credit cost
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class NoProviderAvailable(Exception):
|
|
21
|
+
"""Raised when no provider can fulfill the request."""
|
|
22
|
+
|
|
23
|
+
pass
|
iis_access/auth.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from ._types import AuthContext
|
|
5
|
+
from .config import get_account_url, get_credentials, is_auth_disabled
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger("iis_access")
|
|
8
|
+
|
|
9
|
+
_cached_context: AuthContext | None = None
|
|
10
|
+
_refresh_lock: asyncio.Lock | None = None
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _get_lock() -> asyncio.Lock:
|
|
14
|
+
global _refresh_lock
|
|
15
|
+
if _refresh_lock is None:
|
|
16
|
+
_refresh_lock = asyncio.Lock()
|
|
17
|
+
return _refresh_lock
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def get_auth_context() -> AuthContext | None:
|
|
21
|
+
"""Get authentication context. Returns None if not authenticated."""
|
|
22
|
+
global _cached_context
|
|
23
|
+
|
|
24
|
+
if is_auth_disabled():
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
if _cached_context is not None:
|
|
28
|
+
return _cached_context
|
|
29
|
+
|
|
30
|
+
async with _get_lock():
|
|
31
|
+
# Double-check after acquiring lock
|
|
32
|
+
if _cached_context is not None:
|
|
33
|
+
return _cached_context
|
|
34
|
+
|
|
35
|
+
creds = get_credentials()
|
|
36
|
+
if not creds or "accountId" not in creds:
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
account_id = creds["accountId"]
|
|
40
|
+
refresh_token = _get_refresh_token(account_id)
|
|
41
|
+
if not refresh_token:
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
# Exchange refresh token for access token
|
|
45
|
+
try:
|
|
46
|
+
import aiohttp
|
|
47
|
+
|
|
48
|
+
account_url = get_account_url()
|
|
49
|
+
timeout = aiohttp.ClientTimeout(total=5)
|
|
50
|
+
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
51
|
+
resp = await session.post(
|
|
52
|
+
f"{account_url}/auth/refresh",
|
|
53
|
+
json={"refreshToken": refresh_token},
|
|
54
|
+
)
|
|
55
|
+
if resp.status != 200:
|
|
56
|
+
logger.info(
|
|
57
|
+
"Token refresh failed (status %d), continuing without auth",
|
|
58
|
+
resp.status,
|
|
59
|
+
)
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
data = await resp.json()
|
|
63
|
+
access_token = data.get("accessToken")
|
|
64
|
+
new_refresh_token = data.get("refreshToken")
|
|
65
|
+
plan = data.get("plan") or _extract_plan_from_jwt(access_token)
|
|
66
|
+
|
|
67
|
+
if not access_token:
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
# Store new refresh token back
|
|
71
|
+
if new_refresh_token:
|
|
72
|
+
_store_refresh_token(account_id, new_refresh_token)
|
|
73
|
+
|
|
74
|
+
_cached_context = AuthContext(
|
|
75
|
+
account_id=account_id,
|
|
76
|
+
access_token=access_token,
|
|
77
|
+
plan=plan,
|
|
78
|
+
)
|
|
79
|
+
return _cached_context
|
|
80
|
+
|
|
81
|
+
except Exception as e:
|
|
82
|
+
logger.warning("Auth failed (non-fatal): %s", e)
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def is_authenticated() -> bool:
|
|
87
|
+
"""Synchronous check if there are credentials available."""
|
|
88
|
+
creds = get_credentials()
|
|
89
|
+
return creds is not None and "accountId" in creds
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _get_refresh_token(account_id: str) -> str | None:
|
|
93
|
+
"""Try keyring first, then fall back to credentials file."""
|
|
94
|
+
# Try keyring (optional dependency)
|
|
95
|
+
kr = _get_keyring()
|
|
96
|
+
if kr:
|
|
97
|
+
try:
|
|
98
|
+
token = kr.get_password("iis.tools", account_id)
|
|
99
|
+
if token:
|
|
100
|
+
return token
|
|
101
|
+
except Exception:
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
# Fall back to credentials file
|
|
105
|
+
creds = get_credentials()
|
|
106
|
+
if creds and "refreshToken" in creds:
|
|
107
|
+
return creds["refreshToken"]
|
|
108
|
+
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _store_refresh_token(account_id: str, token: str) -> None:
|
|
113
|
+
"""Store refresh token in keyring if available."""
|
|
114
|
+
kr = _get_keyring()
|
|
115
|
+
if kr:
|
|
116
|
+
try:
|
|
117
|
+
kr.set_password("iis.tools", account_id, token)
|
|
118
|
+
except Exception as e:
|
|
119
|
+
logger.debug("Failed to store refresh token in keyring: %s", e)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _extract_plan_from_jwt(token: str | None) -> str:
|
|
123
|
+
"""Extract plan claim from JWT payload without verifying signature."""
|
|
124
|
+
if not token:
|
|
125
|
+
return "free"
|
|
126
|
+
try:
|
|
127
|
+
import base64, json as _json
|
|
128
|
+
payload = token.split(".")[1]
|
|
129
|
+
payload += "=" * (4 - len(payload) % 4)
|
|
130
|
+
claims = _json.loads(base64.urlsafe_b64decode(payload))
|
|
131
|
+
return claims.get("plan", "free")
|
|
132
|
+
except Exception:
|
|
133
|
+
return "free"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _get_keyring():
|
|
137
|
+
"""Lazy import keyring (optional dependency)."""
|
|
138
|
+
try:
|
|
139
|
+
import keyring
|
|
140
|
+
|
|
141
|
+
return keyring
|
|
142
|
+
except ImportError:
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _reset_auth_cache() -> None:
|
|
147
|
+
"""Reset auth cache (for testing)."""
|
|
148
|
+
global _cached_context, _refresh_lock
|
|
149
|
+
_cached_context = None
|
|
150
|
+
_refresh_lock = None
|
iis_access/config.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
_IIS_DIR = Path.home() / ".iis"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_account_url() -> str:
|
|
9
|
+
if url := os.environ.get("IIS_ACCOUNT_URL"):
|
|
10
|
+
return url.rstrip("/")
|
|
11
|
+
config = _load_config()
|
|
12
|
+
return config.get("accountUrl", "https://account.iis.tools").rstrip("/")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_nexus_url() -> str:
|
|
16
|
+
if url := os.environ.get("IIS_NEXUS_URL"):
|
|
17
|
+
return url.rstrip("/")
|
|
18
|
+
config = _load_config()
|
|
19
|
+
return config.get("nexusUrl", "http://localhost:9666").rstrip("/")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def is_auth_disabled() -> bool:
|
|
23
|
+
return os.environ.get("IIS_NO_AUTH") == "1"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_credentials() -> dict | None:
|
|
27
|
+
path = _IIS_DIR / "credentials.json"
|
|
28
|
+
if not path.exists():
|
|
29
|
+
return None
|
|
30
|
+
try:
|
|
31
|
+
with open(path) as f:
|
|
32
|
+
return json.load(f)
|
|
33
|
+
except (json.JSONDecodeError, OSError):
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _load_config() -> dict:
|
|
38
|
+
path = _IIS_DIR / "config.json"
|
|
39
|
+
if not path.exists():
|
|
40
|
+
return {}
|
|
41
|
+
try:
|
|
42
|
+
with open(path) as f:
|
|
43
|
+
return json.load(f)
|
|
44
|
+
except (json.JSONDecodeError, OSError):
|
|
45
|
+
return {}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from .auth import get_auth_context
|
|
4
|
+
from .config import get_account_url
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger("iis_access")
|
|
7
|
+
|
|
8
|
+
# In-memory cache for managed keys (process lifetime)
|
|
9
|
+
_key_cache: dict[str, str] = {}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def get_managed_key(provider: str) -> str | None:
|
|
13
|
+
"""
|
|
14
|
+
Retrieve user's managed API key for a provider from iis-account.
|
|
15
|
+
Returns None if not authenticated, no key exists, or unreachable.
|
|
16
|
+
"""
|
|
17
|
+
if provider in _key_cache:
|
|
18
|
+
return _key_cache[provider]
|
|
19
|
+
|
|
20
|
+
context = await get_auth_context()
|
|
21
|
+
if context is None:
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
import aiohttp
|
|
26
|
+
|
|
27
|
+
account_url = get_account_url()
|
|
28
|
+
timeout = aiohttp.ClientTimeout(total=5)
|
|
29
|
+
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
30
|
+
resp = await session.get(
|
|
31
|
+
f"{account_url}/keys/managed/{provider}",
|
|
32
|
+
headers={"Authorization": f"Bearer {context.access_token}"},
|
|
33
|
+
)
|
|
34
|
+
if resp.status == 200:
|
|
35
|
+
data = await resp.json()
|
|
36
|
+
key = data.get("key")
|
|
37
|
+
if key:
|
|
38
|
+
_key_cache[provider] = key
|
|
39
|
+
return key
|
|
40
|
+
elif resp.status == 404:
|
|
41
|
+
logger.debug("No managed key for provider '%s'", provider)
|
|
42
|
+
else:
|
|
43
|
+
logger.debug(
|
|
44
|
+
"Managed key retrieval failed (status %d)", resp.status
|
|
45
|
+
)
|
|
46
|
+
except Exception as e:
|
|
47
|
+
logger.debug("Managed key retrieval failed (non-fatal): %s", e)
|
|
48
|
+
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _reset_key_cache() -> None:
|
|
53
|
+
"""Reset managed key cache (for testing)."""
|
|
54
|
+
_key_cache.clear()
|
iis_access/nexus.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from .config import get_nexus_url
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger("iis_access")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
async def is_nexus_running() -> bool:
|
|
9
|
+
"""Check if Nexus local agent is running and responsive."""
|
|
10
|
+
try:
|
|
11
|
+
import aiohttp
|
|
12
|
+
|
|
13
|
+
nexus_url = get_nexus_url()
|
|
14
|
+
timeout = aiohttp.ClientTimeout(total=1)
|
|
15
|
+
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
16
|
+
resp = await session.get(f"{nexus_url}/health")
|
|
17
|
+
return resp.status == 200
|
|
18
|
+
except Exception:
|
|
19
|
+
return False
|
iis_access/provider.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import logging
|
|
3
|
+
import shutil
|
|
4
|
+
from typing import Callable
|
|
5
|
+
|
|
6
|
+
from ._types import ProviderResolution, NoProviderAvailable
|
|
7
|
+
from .nexus import is_nexus_running
|
|
8
|
+
from .managed_keys import get_managed_key
|
|
9
|
+
from .auth import get_auth_context
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("iis_access")
|
|
12
|
+
|
|
13
|
+
# Known env var names for local API keys
|
|
14
|
+
_ENV_KEY_MAP: dict[str, str] = {
|
|
15
|
+
"openai": "OPENAI_API_KEY",
|
|
16
|
+
"anthropic": "ANTHROPIC_API_KEY",
|
|
17
|
+
"elevenlabs": "ELEVENLABS_API_KEY",
|
|
18
|
+
"azure-openai": "AZURE_OPENAI_API_KEY",
|
|
19
|
+
"google-ai": "GOOGLE_AI_API_KEY",
|
|
20
|
+
"mistral": "MISTRAL_API_KEY",
|
|
21
|
+
"groq": "GROQ_API_KEY",
|
|
22
|
+
"deepseek": "DEEPSEEK_API_KEY",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# Free provider registry
|
|
26
|
+
_FREE_PROVIDERS: dict[str, list[tuple[str, Callable[[], bool] | None]]] = {
|
|
27
|
+
"tts": [
|
|
28
|
+
("edge-tts", None), # Always available (pip package)
|
|
29
|
+
("mac-say", lambda: shutil.which("say") is not None),
|
|
30
|
+
],
|
|
31
|
+
"transcription": [
|
|
32
|
+
("whisper-local", lambda: shutil.which("whisper") is not None),
|
|
33
|
+
],
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def register_free_provider(
|
|
38
|
+
task_type: str,
|
|
39
|
+
provider: str,
|
|
40
|
+
check: Callable[[], bool] | None = None,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Register a free provider for a task type."""
|
|
43
|
+
if task_type not in _FREE_PROVIDERS:
|
|
44
|
+
_FREE_PROVIDERS[task_type] = []
|
|
45
|
+
_FREE_PROVIDERS[task_type].append((provider, check))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
async def resolve_provider(
|
|
49
|
+
task_type: str,
|
|
50
|
+
preferred: str | None = None,
|
|
51
|
+
require_capability: str | None = None,
|
|
52
|
+
) -> ProviderResolution:
|
|
53
|
+
"""
|
|
54
|
+
Resolve the best provider for the given task type.
|
|
55
|
+
|
|
56
|
+
Resolution order:
|
|
57
|
+
1. Nexus with pooled credits
|
|
58
|
+
2. Nexus with managed key
|
|
59
|
+
3. Local API key (env var)
|
|
60
|
+
4. Free provider
|
|
61
|
+
5. Raise NoProviderAvailable
|
|
62
|
+
"""
|
|
63
|
+
context = await get_auth_context()
|
|
64
|
+
|
|
65
|
+
# Branch 1 & 2: Nexus
|
|
66
|
+
if await is_nexus_running():
|
|
67
|
+
nexus_url = _get_nexus_endpoint()
|
|
68
|
+
|
|
69
|
+
# Branch 1: Nexus with pooled credits (if authenticated with plan)
|
|
70
|
+
if context and context.plan in ("pro", "enterprise"):
|
|
71
|
+
return ProviderResolution(
|
|
72
|
+
provider=preferred or "nexus-default",
|
|
73
|
+
method="nexus",
|
|
74
|
+
api_key=None,
|
|
75
|
+
endpoint=nexus_url,
|
|
76
|
+
credits_estimated=None, # Nexus handles pricing
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Branch 2: Nexus with managed key
|
|
80
|
+
if context and preferred:
|
|
81
|
+
managed_key = await get_managed_key(preferred)
|
|
82
|
+
if managed_key:
|
|
83
|
+
return ProviderResolution(
|
|
84
|
+
provider=preferred,
|
|
85
|
+
method="managed_key",
|
|
86
|
+
api_key=managed_key,
|
|
87
|
+
endpoint=nexus_url,
|
|
88
|
+
credits_estimated=None,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Branch 3: Local API key
|
|
92
|
+
providers_to_check = [preferred] if preferred else list(_ENV_KEY_MAP.keys())
|
|
93
|
+
for provider in providers_to_check:
|
|
94
|
+
if not provider:
|
|
95
|
+
continue
|
|
96
|
+
env_var = _ENV_KEY_MAP.get(provider)
|
|
97
|
+
if env_var:
|
|
98
|
+
key = os.environ.get(env_var)
|
|
99
|
+
if key:
|
|
100
|
+
return ProviderResolution(
|
|
101
|
+
provider=provider,
|
|
102
|
+
method="local_key",
|
|
103
|
+
api_key=key,
|
|
104
|
+
endpoint=None,
|
|
105
|
+
credits_estimated=None,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Also check managed key without Nexus (direct access)
|
|
109
|
+
if context and preferred:
|
|
110
|
+
managed_key = await get_managed_key(preferred)
|
|
111
|
+
if managed_key:
|
|
112
|
+
return ProviderResolution(
|
|
113
|
+
provider=preferred,
|
|
114
|
+
method="managed_key",
|
|
115
|
+
api_key=managed_key,
|
|
116
|
+
endpoint=None,
|
|
117
|
+
credits_estimated=None,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Branch 4: Free provider
|
|
121
|
+
free_providers = _FREE_PROVIDERS.get(task_type, [])
|
|
122
|
+
for name, check_fn in free_providers:
|
|
123
|
+
if check_fn is None or check_fn():
|
|
124
|
+
return ProviderResolution(
|
|
125
|
+
provider=name,
|
|
126
|
+
method="free",
|
|
127
|
+
api_key=None,
|
|
128
|
+
endpoint=None,
|
|
129
|
+
credits_estimated=0,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Branch 5: No provider available
|
|
133
|
+
raise NoProviderAvailable(
|
|
134
|
+
f"No provider available for task type '{task_type}'"
|
|
135
|
+
+ (f" (preferred: {preferred})" if preferred else "")
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _get_nexus_endpoint() -> str:
|
|
140
|
+
from .config import get_nexus_url
|
|
141
|
+
|
|
142
|
+
return get_nexus_url()
|
iis_access/sync.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
from ._types import ProviderResolution
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def resolve_provider_sync(
|
|
7
|
+
task_type: str,
|
|
8
|
+
preferred: str | None = None,
|
|
9
|
+
require_capability: str | None = None,
|
|
10
|
+
) -> ProviderResolution:
|
|
11
|
+
"""Sync wrapper for resolve_provider."""
|
|
12
|
+
from .provider import resolve_provider
|
|
13
|
+
|
|
14
|
+
return asyncio.run(resolve_provider(task_type, preferred, require_capability))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_managed_key_sync(provider: str) -> str | None:
|
|
18
|
+
"""Sync wrapper for get_managed_key."""
|
|
19
|
+
from .managed_keys import get_managed_key
|
|
20
|
+
|
|
21
|
+
return asyncio.run(get_managed_key(provider))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def report_usage_sync(
|
|
25
|
+
service: str, credits: float = 0, requests: int = 1
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Sync wrapper for report_usage."""
|
|
28
|
+
from .usage import report_usage
|
|
29
|
+
|
|
30
|
+
asyncio.run(report_usage(service, credits, requests))
|
iis_access/usage.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from .auth import get_auth_context
|
|
4
|
+
from .config import get_account_url
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger("iis_access")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def report_usage(
|
|
10
|
+
service: str,
|
|
11
|
+
credits: float = 0,
|
|
12
|
+
requests: int = 1,
|
|
13
|
+
) -> None:
|
|
14
|
+
"""
|
|
15
|
+
Fire-and-forget usage report to iis-account.
|
|
16
|
+
Never raises - logs warnings on failure.
|
|
17
|
+
Skipped if not authenticated.
|
|
18
|
+
"""
|
|
19
|
+
context = await get_auth_context()
|
|
20
|
+
if context is None:
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
import aiohttp
|
|
25
|
+
|
|
26
|
+
account_url = get_account_url()
|
|
27
|
+
timeout = aiohttp.ClientTimeout(total=5)
|
|
28
|
+
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
29
|
+
await session.post(
|
|
30
|
+
f"{account_url}/usage/record",
|
|
31
|
+
json={
|
|
32
|
+
"accountId": context.account_id,
|
|
33
|
+
"service": service,
|
|
34
|
+
"requests": requests,
|
|
35
|
+
"credits": credits,
|
|
36
|
+
},
|
|
37
|
+
headers={"Authorization": f"Bearer {context.access_token}"},
|
|
38
|
+
)
|
|
39
|
+
except Exception as e:
|
|
40
|
+
logger.debug("Usage reporting failed (non-fatal): %s", e)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: iis-access
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Python auth library for IIS tools ecosystem
|
|
5
|
+
Project-URL: Repository, https://github.com/hschmied/account
|
|
6
|
+
Project-URL: Homepage, https://iis.tools
|
|
7
|
+
Author-email: IIS Labs <dev@iis.tools>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Keywords: api-key,auth,iis,provider-resolution
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Requires-Dist: aiohttp>=3.9
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: aioresponses>=0.7; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
22
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
23
|
+
Provides-Extra: keyring
|
|
24
|
+
Requires-Dist: keyring>=25.0; extra == 'keyring'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# iis-access
|
|
28
|
+
|
|
29
|
+
Python auth library for IIS tools ecosystem. Provides optional authentication, provider resolution, managed key retrieval, and usage reporting for Python-based IIS CLI tools (aria, sonar, iris).
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install iis-access
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from iis_access import resolve_provider, report_usage
|
|
41
|
+
|
|
42
|
+
# Resolve the best available provider for a task
|
|
43
|
+
resolution = await resolve_provider("tts", preferred="elevenlabs")
|
|
44
|
+
print(resolution.provider, resolution.method)
|
|
45
|
+
|
|
46
|
+
# Report usage (fire-and-forget, never raises)
|
|
47
|
+
await report_usage("aria", credits=1.5)
|
|
48
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
iis_access/__init__.py,sha256=pOCjnVHB--w0Eq46f7akfQ2WHqO1y7HCKCg-8oa2Rzw,577
|
|
2
|
+
iis_access/_types.py,sha256=WwyOteO-VyDgRBclXhczSCa-d6g-kZuJfAAcOnT2WB8,613
|
|
3
|
+
iis_access/auth.py,sha256=P4IKxhDvzWPBGHevGs09LaoV3c6BiULL9yNlwieAzQg,4427
|
|
4
|
+
iis_access/config.py,sha256=II9rbpyiUNOq3u5FxjQsbjVaHALtvIO_5j1eHasj1aw,1091
|
|
5
|
+
iis_access/managed_keys.py,sha256=newufFQIbU0NspHWKKWWagWaQeVajDL4zEbbfqDt9Es,1652
|
|
6
|
+
iis_access/nexus.py,sha256=57Em0iS2fXF-eEdywp-iPpki3OfOf5TVFlz0OPUByQA,525
|
|
7
|
+
iis_access/provider.py,sha256=QY9OoE9vAYEiFOzMGncb_Q3iZzpKEIWtysMEOHKEPAE,4359
|
|
8
|
+
iis_access/sync.py,sha256=qqcqK_3XkMrrFN3joMwsU6d895N83vFrl1nveHOKIfA,804
|
|
9
|
+
iis_access/usage.py,sha256=5DNNHTTsbjnb2expBMM-kkXc2z6EfxsiezAPEzfQllI,1126
|
|
10
|
+
iis_access-0.1.1.dist-info/METADATA,sha256=p5f8p8D-5Ou_ifeIugffbQtvIGCD-ncSMInUOPg0CyM,1587
|
|
11
|
+
iis_access-0.1.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
12
|
+
iis_access-0.1.1.dist-info/RECORD,,
|