insightfactory-cli 1.1.2.dev31__py3-none-any.whl → 1.2.0.dev34__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.
- if_cli/attribution.py +51 -0
- if_cli/constants.py +1 -0
- if_cli/http.py +11 -1
- if_cli/oauth.py +2 -0
- if_cli/router/upstream.py +6 -1
- {insightfactory_cli-1.1.2.dev31.dist-info → insightfactory_cli-1.2.0.dev34.dist-info}/METADATA +9 -1
- {insightfactory_cli-1.1.2.dev31.dist-info → insightfactory_cli-1.2.0.dev34.dist-info}/RECORD +10 -9
- {insightfactory_cli-1.1.2.dev31.dist-info → insightfactory_cli-1.2.0.dev34.dist-info}/WHEEL +0 -0
- {insightfactory_cli-1.1.2.dev31.dist-info → insightfactory_cli-1.2.0.dev34.dist-info}/entry_points.txt +0 -0
- {insightfactory_cli-1.1.2.dev31.dist-info → insightfactory_cli-1.2.0.dev34.dist-info}/licenses/LICENSE +0 -0
if_cli/attribution.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""The `X-IF-UA` client-attribution header.
|
|
2
|
+
|
|
3
|
+
The Interfaces API attributes work to the client that made the request by reading
|
|
4
|
+
`X-IF-UA`, a list of RFC 9110 product tokens. It takes the first token, splits it at
|
|
5
|
+
`/` into client and version, and ignores the rest. So a tool that drives `if-cli`
|
|
6
|
+
names itself first through `INSIGHTFACTORY_UA` and `if-cli` appends its own token:
|
|
7
|
+
|
|
8
|
+
X-IF-UA: if-cli/1.2.0 # INSIGHTFACTORY_UA unset
|
|
9
|
+
X-IF-UA: if-fde-harness/5.10.0 if-cli/1.2.0 # INSIGHTFACTORY_UA=if-fde-harness/5.10.0
|
|
10
|
+
|
|
11
|
+
The environment value is only ever sent after it matches the product-token grammar,
|
|
12
|
+
so a stray newline or shell fragment in it cannot add a header or reach the factory.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
import re
|
|
19
|
+
from functools import cache
|
|
20
|
+
from importlib.metadata import version
|
|
21
|
+
|
|
22
|
+
from if_cli.constants import ENV_UA
|
|
23
|
+
|
|
24
|
+
HEADER_NAME = "X-IF-UA"
|
|
25
|
+
MAX_CALLER_PRODUCTS_LENGTH = 256
|
|
26
|
+
|
|
27
|
+
# RFC 9110 section 5.6.2: token = 1*tchar. Section 10.1.5: product = token ["/" product-version].
|
|
28
|
+
_TCHAR = r"[!#$%&'*+\-.^_`|~0-9A-Za-z]"
|
|
29
|
+
_PRODUCT = rf"{_TCHAR}+(?:/{_TCHAR}+)?"
|
|
30
|
+
_PRODUCTS = re.compile(rf"{_PRODUCT}(?: {_PRODUCT})*")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@cache
|
|
34
|
+
def _own_product() -> str:
|
|
35
|
+
return f"if-cli/{version('insightfactory-cli')}"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def is_valid_products(value: str) -> bool:
|
|
39
|
+
"""Whether `value` is one or more space-separated product tokens, within the length cap."""
|
|
40
|
+
return len(value) <= MAX_CALLER_PRODUCTS_LENGTH and _PRODUCTS.fullmatch(value) is not None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def build_x_if_ua() -> str:
|
|
44
|
+
"""The `X-IF-UA` value: the caller's products from `INSIGHTFACTORY_UA`, then `if-cli`'s own.
|
|
45
|
+
|
|
46
|
+
An unset, empty or invalid `INSIGHTFACTORY_UA` leaves `if-cli`'s token alone.
|
|
47
|
+
"""
|
|
48
|
+
caller = os.environ.get(ENV_UA)
|
|
49
|
+
if caller and is_valid_products(caller):
|
|
50
|
+
return f"{caller} {_own_product()}"
|
|
51
|
+
return _own_product()
|
if_cli/constants.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
ENV_PROFILE: str = "INSIGHTFACTORY_CONFIG_PROFILE"
|
|
2
2
|
ENV_REQUEST_TIMEOUT: str = "INSIGHTFACTORY_REQUEST_TIMEOUT"
|
|
3
|
+
ENV_UA: str = "INSIGHTFACTORY_UA"
|
|
3
4
|
DEFAULT_SCOPE: str = "openid profile email offline_access"
|
|
4
5
|
LOOPBACK_HOST: str = "127.0.0.1"
|
|
5
6
|
DEFAULT_CALLBACK_PORT: int = 53682
|
if_cli/http.py
CHANGED
|
@@ -10,6 +10,7 @@ from email.message import Message
|
|
|
10
10
|
from typing import NoReturn
|
|
11
11
|
from urllib.parse import ParseResult, urljoin, urlparse
|
|
12
12
|
|
|
13
|
+
from if_cli.attribution import HEADER_NAME, build_x_if_ua
|
|
13
14
|
from if_cli.constants import ENV_REQUEST_TIMEOUT
|
|
14
15
|
from if_cli.runtime import die
|
|
15
16
|
|
|
@@ -170,6 +171,7 @@ def fetch_with_timeout(
|
|
|
170
171
|
body: str | bytes | None = None,
|
|
171
172
|
timeout: float | None = None,
|
|
172
173
|
warn_unsafe_retry: bool = False,
|
|
174
|
+
attribute: bool = True,
|
|
173
175
|
) -> HttpResponse:
|
|
174
176
|
"""Issue one request, dying on timeout.
|
|
175
177
|
|
|
@@ -177,7 +179,15 @@ def fetch_with_timeout(
|
|
|
177
179
|
sense for a request the caller issued on the user's behalf against the factory.
|
|
178
180
|
It is wrong advice for an OAuth round trip, where retrying is the correct response
|
|
179
181
|
and there is no factory-side state to confirm.
|
|
182
|
+
|
|
183
|
+
`attribute` adds the `X-IF-UA` header (see `if_cli.attribution`) unless the
|
|
184
|
+
caller already set one. It is on for factory requests and off for the token
|
|
185
|
+
endpoint, which belongs to the identity provider: that is a third party, and the
|
|
186
|
+
header is for the factory's own client metrics.
|
|
180
187
|
"""
|
|
188
|
+
request_headers = dict(headers or {})
|
|
189
|
+
if attribute and not any(name.lower() == HEADER_NAME.lower() for name in request_headers):
|
|
190
|
+
request_headers[HEADER_NAME] = build_x_if_ua()
|
|
181
191
|
data: bytes | None
|
|
182
192
|
if body is None:
|
|
183
193
|
data = None
|
|
@@ -185,7 +195,7 @@ def fetch_with_timeout(
|
|
|
185
195
|
data = body
|
|
186
196
|
else:
|
|
187
197
|
data = body.encode("utf-8")
|
|
188
|
-
request = urllib.request.Request(url, data=data, headers=
|
|
198
|
+
request = urllib.request.Request(url, data=data, headers=request_headers, method=method)
|
|
189
199
|
effective_timeout = resolve_request_timeout(timeout)
|
|
190
200
|
try:
|
|
191
201
|
# Callers pass endpoints validated by parse_http_url or derived from a
|
if_cli/oauth.py
CHANGED
|
@@ -128,6 +128,8 @@ def _token_request(token_endpoint: str, form: dict[str, str]) -> dict[str, objec
|
|
|
128
128
|
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
129
129
|
body=urlencode(form),
|
|
130
130
|
timeout=DEFAULT_REQUEST_TIMEOUT_S,
|
|
131
|
+
# The token endpoint is the identity provider's, not the factory's.
|
|
132
|
+
attribute=False,
|
|
131
133
|
)
|
|
132
134
|
if not response.ok:
|
|
133
135
|
body = response.text()
|
if_cli/router/upstream.py
CHANGED
|
@@ -9,6 +9,7 @@ import mcp_types as types
|
|
|
9
9
|
from mcp.client import Client
|
|
10
10
|
from mcp.client.streamable_http import streamable_http_client
|
|
11
11
|
|
|
12
|
+
from if_cli.attribution import HEADER_NAME, build_x_if_ua
|
|
12
13
|
from if_cli.config import Profile
|
|
13
14
|
from if_cli.http import parse_url, url_origin
|
|
14
15
|
from if_cli.oauth import get_valid_token
|
|
@@ -200,7 +201,11 @@ class Upstream:
|
|
|
200
201
|
async def _run(self, action, *, elicitation_callback=None):
|
|
201
202
|
url = f"{self.profile['host']}/mcp"
|
|
202
203
|
try:
|
|
203
|
-
async with httpx2.AsyncClient(
|
|
204
|
+
async with httpx2.AsyncClient(
|
|
205
|
+
auth=ProfileBearerAuth(self.profile),
|
|
206
|
+
timeout=self.timeout,
|
|
207
|
+
headers={HEADER_NAME: build_x_if_ua()},
|
|
208
|
+
) as http_client:
|
|
204
209
|
async with Client(
|
|
205
210
|
streamable_http_client(url, http_client=http_client),
|
|
206
211
|
elicitation_callback=elicitation_callback,
|
{insightfactory_cli-1.1.2.dev31.dist-info → insightfactory_cli-1.2.0.dev34.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: insightfactory-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0.dev34
|
|
4
4
|
Summary: Profile-based authentication CLI for the InsightFactory Interfaces API
|
|
5
5
|
Project-URL: Homepage, https://insightfactory.ai
|
|
6
6
|
Author-email: "insightfactory.ai Support" <support@insightfactory.ai>
|
|
@@ -128,6 +128,14 @@ accepted and completed the request after `if-cli` gave up, so re-issuing a
|
|
|
128
128
|
timeout message tells you to confirm the current state before retrying rather
|
|
129
129
|
than repeat the call.
|
|
130
130
|
|
|
131
|
+
Every request to the factory carries an `X-IF-UA` header naming the client,
|
|
132
|
+
`if-cli/<version>` by default. A tool that runs `if-cli` can name itself first
|
|
133
|
+
by setting `INSIGHTFACTORY_UA` to one or more RFC 9110 product tokens, such as
|
|
134
|
+
`if-fde-harness/5.10.0`, and the header becomes
|
|
135
|
+
`if-fde-harness/5.10.0 if-cli/<version>`. The CLI ignores a value that is not
|
|
136
|
+
valid product-token syntax or is longer than 256 characters. Token requests to
|
|
137
|
+
the identity provider never carry the header.
|
|
138
|
+
|
|
131
139
|
Override the config directory with `INSIGHTFACTORY_CONFIG_DIR`, which is useful
|
|
132
140
|
in tests and CI. The default is `~/.insightfactory`.
|
|
133
141
|
|
{insightfactory_cli-1.1.2.dev31.dist-info → insightfactory_cli-1.2.0.dev34.dist-info}/RECORD
RENAMED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
if_cli/__init__.py,sha256=YYcz-hEiXXUjdBFGipOrVHynsyH4ZLqQOCDobvCAu8Y,26
|
|
2
2
|
if_cli/__main__.py,sha256=2hLFIiT-gocMb0VJVzVYtGVIoHrxyMChucRgexbXLSQ,68
|
|
3
|
+
if_cli/attribution.py,sha256=hNp1hBueRUh1EKYs61AZF76voehw70F7gZKiI5zOMCw,1869
|
|
3
4
|
if_cli/cache.py,sha256=UfvHs3GQIVg0sVfKYWw4UGlq8d-8BEXuZIDfC1hwwjA,6587
|
|
4
5
|
if_cli/callback_page.py,sha256=lTHd3-vzlT9ndAL7XYonBfy97lBW7apRw3V7SmhNObo,5907
|
|
5
6
|
if_cli/cli.py,sha256=fF7HvsUgpKhkLFdXLMJ1JXHGjp3RwJt2YmPSEW0d8gY,6270
|
|
6
7
|
if_cli/colour.py,sha256=2hbaM4ubTHGja6YZRD886eqnB7T2IaZyW5rvfOjxe4M,682
|
|
7
8
|
if_cli/config.py,sha256=QiphIhVLDN3sp39xMWKwXEyJWxx4IhXT28oEtmPc0ew,10601
|
|
8
|
-
if_cli/constants.py,sha256=
|
|
9
|
-
if_cli/http.py,sha256=
|
|
9
|
+
if_cli/constants.py,sha256=WxZozWfhMCFWU_Wupa__CaJI9UgqJ28W8cchGtp6p4Q,303
|
|
10
|
+
if_cli/http.py,sha256=LNkQtyUR1DAgLcR2CrNzTMHFQO_j_vfHuOBagmQEYNk,8249
|
|
10
11
|
if_cli/main.py,sha256=pn-X2wrHNplc8ceT5_nEv5XlrTN3KgpK8UU4gAmBiWY,3212
|
|
11
|
-
if_cli/oauth.py,sha256=
|
|
12
|
+
if_cli/oauth.py,sha256=lSJXzOsOzHlkZX56JWoh9G6-sD9MOGgmwpI18tdhA6c,16563
|
|
12
13
|
if_cli/runtime.py,sha256=Pz2iVclqy-uu1UMQWK2P_Hx7O4zAF30UpSFqQvGN0NY,2663
|
|
13
14
|
if_cli/assets/__init__.py,sha256=2vBBor8lUEQofiC6EvS5BvF_ANhfK8bfR9S0OXI8j3g,41
|
|
14
15
|
if_cli/assets/insightfactoryai-logo.svg,sha256=yFxHkoHl6VNm8ppCInTZCVN_bdOog9h-NV8R4Gw1Qew,8192
|
|
@@ -27,9 +28,9 @@ if_cli/router/content.py,sha256=POL2n5NIY8yenQHJe8pUbTuTNpPSUSm0Y6Txbvehzhs,1448
|
|
|
27
28
|
if_cli/router/log.py,sha256=wtaqsFcD3hPJRu4jCJaDMSedj-43Y07ym62YgrOm1yM,740
|
|
28
29
|
if_cli/router/policy.py,sha256=CclbjuOA5mwwoQiNDa17iQeN9eXRbfy11bS9JFd8gL4,2840
|
|
29
30
|
if_cli/router/server.py,sha256=yCjGAm-tnZBOTg5AfvojnLcKcZLEbQQeT9iqqX8XV5A,27397
|
|
30
|
-
if_cli/router/upstream.py,sha256=
|
|
31
|
-
insightfactory_cli-1.
|
|
32
|
-
insightfactory_cli-1.
|
|
33
|
-
insightfactory_cli-1.
|
|
34
|
-
insightfactory_cli-1.
|
|
35
|
-
insightfactory_cli-1.
|
|
31
|
+
if_cli/router/upstream.py,sha256=Ly4fcuQRywS6db2oDYoe8F4c9hryN75FdBqSWzA09g4,18720
|
|
32
|
+
insightfactory_cli-1.2.0.dev34.dist-info/METADATA,sha256=xbyLKunNzQ2g7wv-86s5ruh_-rnUyetLKVWwYINNBp8,24817
|
|
33
|
+
insightfactory_cli-1.2.0.dev34.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
34
|
+
insightfactory_cli-1.2.0.dev34.dist-info/entry_points.txt,sha256=UlX854D7f-7dBj5F0wGdakVy6vopvP9hA1BN-gv739w,44
|
|
35
|
+
insightfactory_cli-1.2.0.dev34.dist-info/licenses/LICENSE,sha256=8eZ1YAABL398qESVJc_FlK1voYQ0GAh-R_rNBQK6QH8,234
|
|
36
|
+
insightfactory_cli-1.2.0.dev34.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|