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 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=headers or {}, method=method)
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(auth=ProfileBearerAuth(self.profile), timeout=self.timeout) as http_client:
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,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: insightfactory-cli
3
- Version: 1.1.2.dev31
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
 
@@ -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=RW43KupAdhXfBsX6MxzZdJ-R57brX0q0TgIq8xXgvPs,269
9
- if_cli/http.py,sha256=W8HL3uZLeOu5MgmsOavM1Y6x4AcrInIQsAr5hx-j8AE,7671
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=8QZy4A7NqnWSnTULQct-Tj6RT906RstBJOloSTzgflU,16462
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=Ga0OM918Y1CFyQFGs2wO0ocyjcMe1Wk0LvffOZOaE34,18559
31
- insightfactory_cli-1.1.2.dev31.dist-info/METADATA,sha256=LkShsicB2QHUEjaS9FmsW9T2dPlGIl92Ew-OMsSCg28,24331
32
- insightfactory_cli-1.1.2.dev31.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
33
- insightfactory_cli-1.1.2.dev31.dist-info/entry_points.txt,sha256=UlX854D7f-7dBj5F0wGdakVy6vopvP9hA1BN-gv739w,44
34
- insightfactory_cli-1.1.2.dev31.dist-info/licenses/LICENSE,sha256=8eZ1YAABL398qESVJc_FlK1voYQ0GAh-R_rNBQK6QH8,234
35
- insightfactory_cli-1.1.2.dev31.dist-info/RECORD,,
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,,