futurehouse-client 0.3.18.dev186__py3-none-any.whl → 0.3.19__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.
- futurehouse_client/__init__.py +11 -1
- futurehouse_client/clients/__init__.py +1 -2
- futurehouse_client/clients/job_client.py +27 -1
- futurehouse_client/clients/rest_client.py +439 -255
- futurehouse_client/models/app.py +70 -2
- futurehouse_client/py.typed +0 -0
- futurehouse_client/utils/auth.py +86 -101
- {futurehouse_client-0.3.18.dev186.dist-info → futurehouse_client-0.3.19.dist-info}/METADATA +2 -3
- futurehouse_client-0.3.19.dist-info/RECORD +18 -0
- {futurehouse_client-0.3.18.dev186.dist-info → futurehouse_client-0.3.19.dist-info}/WHEEL +1 -1
- futurehouse_client-0.3.18.dev186.dist-info/RECORD +0 -17
- {futurehouse_client-0.3.18.dev186.dist-info → futurehouse_client-0.3.19.dist-info}/top_level.txt +0 -0
futurehouse_client/models/app.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import copy
|
1
2
|
import json
|
2
3
|
import os
|
3
4
|
import re
|
@@ -675,7 +676,8 @@ class TaskResponse(BaseModel):
|
|
675
676
|
|
676
677
|
@model_validator(mode="before")
|
677
678
|
@classmethod
|
678
|
-
def validate_fields(cls,
|
679
|
+
def validate_fields(cls, original_data: Mapping[str, Any]) -> Mapping[str, Any]:
|
680
|
+
data = copy.deepcopy(original_data) # Avoid mutating the original data
|
679
681
|
# Extract fields from environment frame state
|
680
682
|
if not isinstance(data, dict):
|
681
683
|
return data
|
@@ -690,7 +692,72 @@ class TaskResponse(BaseModel):
|
|
690
692
|
return data
|
691
693
|
|
692
694
|
|
695
|
+
class PhoenixTaskResponse(TaskResponse):
|
696
|
+
"""
|
697
|
+
Response scheme for tasks executed with Phoenix.
|
698
|
+
|
699
|
+
Additional fields:
|
700
|
+
answer: Final answer from Phoenix
|
701
|
+
"""
|
702
|
+
|
703
|
+
model_config = ConfigDict(extra="ignore")
|
704
|
+
answer: str | None = None
|
705
|
+
|
706
|
+
@model_validator(mode="before")
|
707
|
+
@classmethod
|
708
|
+
def validate_phoenix_fields(
|
709
|
+
cls, original_data: Mapping[str, Any]
|
710
|
+
) -> Mapping[str, Any]:
|
711
|
+
data = copy.deepcopy(original_data)
|
712
|
+
if not isinstance(data, dict):
|
713
|
+
return data
|
714
|
+
if not (env_frame := data.get("environment_frame", {})):
|
715
|
+
return data
|
716
|
+
state = env_frame.get("state", {}).get("state", {})
|
717
|
+
data["answer"] = state.get("answer")
|
718
|
+
return data
|
719
|
+
|
720
|
+
|
721
|
+
class FinchTaskResponse(TaskResponse):
|
722
|
+
"""
|
723
|
+
Response scheme for tasks executed with Finch.
|
724
|
+
|
725
|
+
Additional fields:
|
726
|
+
answer: Final answer from Finch
|
727
|
+
notebook: a dictionary with `cells` and `metadata` regarding the notebook content
|
728
|
+
"""
|
729
|
+
|
730
|
+
model_config = ConfigDict(extra="ignore")
|
731
|
+
answer: str | None = None
|
732
|
+
notebook: dict[str, Any] | None = None
|
733
|
+
|
734
|
+
@model_validator(mode="before")
|
735
|
+
@classmethod
|
736
|
+
def validate_finch_fields(
|
737
|
+
cls, original_data: Mapping[str, Any]
|
738
|
+
) -> Mapping[str, Any]:
|
739
|
+
data = copy.deepcopy(original_data)
|
740
|
+
if not isinstance(data, dict):
|
741
|
+
return data
|
742
|
+
if not (env_frame := data.get("environment_frame", {})):
|
743
|
+
return data
|
744
|
+
state = env_frame.get("state", {}).get("state", {})
|
745
|
+
data["answer"] = state.get("answer")
|
746
|
+
data["notebook"] = state.get("nb_state")
|
747
|
+
return data
|
748
|
+
|
749
|
+
|
693
750
|
class PQATaskResponse(TaskResponse):
|
751
|
+
"""
|
752
|
+
Response scheme for tasks executed with PQA.
|
753
|
+
|
754
|
+
Additional fields:
|
755
|
+
answer: Final answer from PQA
|
756
|
+
formatted_answer: Formatted answer from PQA
|
757
|
+
answer_reasoning: Reasoning used to generate the final answer, if available
|
758
|
+
has_successful_answer: Whether the answer is successful
|
759
|
+
"""
|
760
|
+
|
694
761
|
model_config = ConfigDict(extra="ignore")
|
695
762
|
|
696
763
|
answer: str | None = None
|
@@ -702,7 +769,8 @@ class PQATaskResponse(TaskResponse):
|
|
702
769
|
|
703
770
|
@model_validator(mode="before")
|
704
771
|
@classmethod
|
705
|
-
def validate_pqa_fields(cls,
|
772
|
+
def validate_pqa_fields(cls, original_data: Mapping[str, Any]) -> Mapping[str, Any]:
|
773
|
+
data = copy.deepcopy(original_data) # Avoid mutating the original data
|
706
774
|
if not isinstance(data, dict):
|
707
775
|
return data
|
708
776
|
if not (env_frame := data.get("environment_frame", {})):
|
File without changes
|
futurehouse_client/utils/auth.py
CHANGED
@@ -1,107 +1,92 @@
|
|
1
|
-
import asyncio
|
2
1
|
import logging
|
3
|
-
from collections.abc import
|
4
|
-
from
|
5
|
-
from typing import Any, Final, Optional, ParamSpec, TypeVar, overload
|
2
|
+
from collections.abc import Collection, Generator
|
3
|
+
from typing import ClassVar, Final
|
6
4
|
|
7
5
|
import httpx
|
8
|
-
from httpx import HTTPStatusError
|
9
6
|
|
10
|
-
|
11
|
-
|
12
|
-
T = TypeVar("T")
|
13
|
-
P = ParamSpec("P")
|
14
|
-
|
15
|
-
AUTH_ERRORS_TO_RETRY_ON: Final[set[int]] = {
|
16
|
-
httpx.codes.UNAUTHORIZED,
|
17
|
-
httpx.codes.FORBIDDEN,
|
18
|
-
}
|
19
|
-
|
20
|
-
|
21
|
-
class AuthError(Exception):
|
22
|
-
"""Raised when authentication fails with 401/403 status."""
|
23
|
-
|
24
|
-
def __init__(self, status_code: int, message: str, request=None, response=None):
|
25
|
-
self.status_code = status_code
|
26
|
-
self.request = request
|
27
|
-
self.response = response
|
28
|
-
super().__init__(message)
|
29
|
-
|
30
|
-
|
31
|
-
def is_auth_error(e: Exception) -> bool:
|
32
|
-
if isinstance(e, AuthError):
|
33
|
-
return True
|
34
|
-
if isinstance(e, HTTPStatusError):
|
35
|
-
return e.response.status_code in AUTH_ERRORS_TO_RETRY_ON
|
36
|
-
return False
|
37
|
-
|
38
|
-
|
39
|
-
def get_status_code(e: Exception) -> Optional[int]:
|
40
|
-
if isinstance(e, AuthError):
|
41
|
-
return e.status_code
|
42
|
-
if isinstance(e, HTTPStatusError):
|
43
|
-
return e.response.status_code
|
44
|
-
return None
|
7
|
+
from futurehouse_client.models.app import APIKeyPayload, AuthType
|
45
8
|
|
9
|
+
logger = logging.getLogger(__name__)
|
46
10
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
11
|
+
INVALID_REFRESH_TYPE_MSG: Final[str] = (
|
12
|
+
"API key auth is required to refresh auth tokens."
|
13
|
+
)
|
14
|
+
JWT_TOKEN_CACHE_EXPIRY: int = 300 # seconds
|
15
|
+
|
16
|
+
|
17
|
+
def _run_auth(
|
18
|
+
client: httpx.Client,
|
19
|
+
auth_type: AuthType = AuthType.API_KEY,
|
20
|
+
api_key: str | None = None,
|
21
|
+
jwt: str | None = None,
|
22
|
+
) -> str:
|
23
|
+
auth_payload: APIKeyPayload | None
|
24
|
+
if auth_type == AuthType.API_KEY:
|
25
|
+
auth_payload = APIKeyPayload(api_key=api_key)
|
26
|
+
elif auth_type == AuthType.JWT:
|
27
|
+
auth_payload = None
|
28
|
+
try:
|
29
|
+
if auth_payload:
|
30
|
+
response = client.post("/auth/login", json=auth_payload.model_dump())
|
31
|
+
response.raise_for_status()
|
32
|
+
token_data = response.json()
|
33
|
+
elif jwt:
|
34
|
+
token_data = {"access_token": jwt, "expires_in": JWT_TOKEN_CACHE_EXPIRY}
|
35
|
+
else:
|
36
|
+
raise ValueError("JWT token required for JWT authentication.")
|
37
|
+
|
38
|
+
return token_data["access_token"]
|
39
|
+
except Exception as e:
|
40
|
+
raise Exception("Failed to authenticate") from e # noqa: TRY002
|
41
|
+
|
42
|
+
|
43
|
+
class RefreshingJWT(httpx.Auth):
|
44
|
+
"""Automatically (re-)inject a JWT and transparently retry exactly once when we hit a 401/403."""
|
45
|
+
|
46
|
+
RETRY_STATUSES: ClassVar[Collection[httpx.codes]] = {
|
47
|
+
httpx.codes.UNAUTHORIZED,
|
48
|
+
httpx.codes.FORBIDDEN,
|
49
|
+
}
|
50
|
+
|
51
|
+
def __init__(
|
52
|
+
self,
|
53
|
+
auth_client: httpx.Client,
|
54
|
+
auth_type: AuthType = AuthType.API_KEY,
|
55
|
+
api_key: str | None = None,
|
56
|
+
jwt: str | None = None,
|
57
|
+
):
|
58
|
+
self.auth_type = auth_type
|
59
|
+
self.auth_client = auth_client
|
60
|
+
self.api_key = api_key
|
61
|
+
self._jwt = _run_auth(
|
62
|
+
client=auth_client,
|
63
|
+
jwt=jwt,
|
64
|
+
auth_type=auth_type,
|
65
|
+
api_key=api_key,
|
66
|
+
)
|
67
|
+
|
68
|
+
def refresh_token(self) -> None:
|
69
|
+
if self.auth_type == AuthType.JWT:
|
70
|
+
logger.error(INVALID_REFRESH_TYPE_MSG)
|
71
|
+
raise ValueError(INVALID_REFRESH_TYPE_MSG)
|
72
|
+
self._jwt = _run_auth(
|
73
|
+
client=self.auth_client,
|
74
|
+
auth_type=self.auth_type,
|
75
|
+
api_key=self.api_key,
|
76
|
+
)
|
77
|
+
|
78
|
+
def auth_flow(
|
79
|
+
self, request: httpx.Request
|
80
|
+
) -> Generator[httpx.Request, httpx.Response, None]:
|
81
|
+
request.headers["Authorization"] = f"Bearer {self._jwt}"
|
82
|
+
response = yield request
|
83
|
+
|
84
|
+
# If it failed, refresh once and replay the request
|
85
|
+
if response.status_code in self.RETRY_STATUSES:
|
86
|
+
logger.info(
|
87
|
+
"Received %s, refreshing token and retrying …",
|
88
|
+
response.status_code,
|
89
|
+
)
|
90
|
+
self.refresh_token()
|
91
|
+
request.headers["Authorization"] = f"Bearer {self._jwt}"
|
92
|
+
yield request # second (and final) attempt, again or use a while loop
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: futurehouse-client
|
3
|
-
Version: 0.3.
|
3
|
+
Version: 0.3.19
|
4
4
|
Summary: A client for interacting with endpoints of the FutureHouse service.
|
5
5
|
Author-email: FutureHouse technical staff <hello@futurehouse.org>
|
6
6
|
Classifier: Operating System :: OS Independent
|
@@ -8,10 +8,9 @@ Classifier: Programming Language :: Python :: 3 :: Only
|
|
8
8
|
Classifier: Programming Language :: Python :: 3.11
|
9
9
|
Classifier: Programming Language :: Python :: 3.12
|
10
10
|
Classifier: Programming Language :: Python
|
11
|
-
Requires-Python: <3.
|
11
|
+
Requires-Python: <3.14,>=3.11
|
12
12
|
Description-Content-Type: text/markdown
|
13
13
|
Requires-Dist: cloudpickle
|
14
|
-
Requires-Dist: dm-tree<0.1.9
|
15
14
|
Requires-Dist: fhaviary
|
16
15
|
Requires-Dist: httpx
|
17
16
|
Requires-Dist: ldp>=0.22.0
|
@@ -0,0 +1,18 @@
|
|
1
|
+
futurehouse_client/__init__.py,sha256=BztM_ntbgmIEjzvnBWcvPhvLjM8xGDFCK0Upf3-nIn8,488
|
2
|
+
futurehouse_client/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
|
+
futurehouse_client/clients/__init__.py,sha256=-HXNj-XJ3LRO5XM6MZ709iPs29YpApss0Q2YYg1qMZw,280
|
4
|
+
futurehouse_client/clients/job_client.py,sha256=JgB5IUAyCmnhGRsYc3bgKldA-lkM1JLwHRwwUeOCdus,11944
|
5
|
+
futurehouse_client/clients/rest_client.py,sha256=3wfVz6d2KuRQUr_nms7P25yVR6aTjsRrSkqmVs55soA,54552
|
6
|
+
futurehouse_client/models/__init__.py,sha256=5x-f9AoM1hGzJBEHcHAXSt7tPeImST5oZLuMdwp0mXc,554
|
7
|
+
futurehouse_client/models/app.py,sha256=VCtg0ygd-TSrR6DtfljTBt9jnl1eBNal8UXHFdkDg88,28587
|
8
|
+
futurehouse_client/models/client.py,sha256=n4HD0KStKLm6Ek9nL9ylP-bkK10yzAaD1uIDF83Qp_A,1828
|
9
|
+
futurehouse_client/models/rest.py,sha256=lgwkMIXz0af-49BYSkKeS7SRqvN3motqnAikDN4YGTc,789
|
10
|
+
futurehouse_client/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
11
|
+
futurehouse_client/utils/auth.py,sha256=tgWELjKfg8eWme_qdcRmc8TjQN9DVZuHHaVXZNHLchk,2960
|
12
|
+
futurehouse_client/utils/general.py,sha256=A_rtTiYW30ELGEZlWCIArO7q1nEmqi8hUlmBRYkMQ_c,767
|
13
|
+
futurehouse_client/utils/module_utils.py,sha256=aFyd-X-pDARXz9GWpn8SSViUVYdSbuy9vSkrzcVIaGI,4955
|
14
|
+
futurehouse_client/utils/monitoring.py,sha256=UjRlufe67kI3VxRHOd5fLtJmlCbVA2Wqwpd4uZhXkQM,8728
|
15
|
+
futurehouse_client-0.3.19.dist-info/METADATA,sha256=FbtQGStv4salVccxR5wtpdlGbufSqxoiCtM44qDOHJs,12731
|
16
|
+
futurehouse_client-0.3.19.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
17
|
+
futurehouse_client-0.3.19.dist-info/top_level.txt,sha256=TRuLUCt_qBnggdFHCX4O_BoCu1j2X43lKfIZC-ElwWY,19
|
18
|
+
futurehouse_client-0.3.19.dist-info/RECORD,,
|
@@ -1,17 +0,0 @@
|
|
1
|
-
futurehouse_client/__init__.py,sha256=ddxO7JE97c6bt7LjNglZZ2Ql8bYCGI9laSFeh9MP6VU,344
|
2
|
-
futurehouse_client/clients/__init__.py,sha256=tFWqwIAY5PvwfOVsCje4imjTpf6xXNRMh_UHIKVI1_0,320
|
3
|
-
futurehouse_client/clients/job_client.py,sha256=uNkqQbeZw7wbA0qDWcIOwOykrosza-jev58paJZ_mbA,11150
|
4
|
-
futurehouse_client/clients/rest_client.py,sha256=W9ASP1ZKYS7UL5J9b-Km77YXEiDQ9hCf4X_9PqaZZZc,47914
|
5
|
-
futurehouse_client/models/__init__.py,sha256=5x-f9AoM1hGzJBEHcHAXSt7tPeImST5oZLuMdwp0mXc,554
|
6
|
-
futurehouse_client/models/app.py,sha256=w_1e4F0IiC-BKeOLqYkABYo4U-Nka1S-F64S_eHB2KM,26421
|
7
|
-
futurehouse_client/models/client.py,sha256=n4HD0KStKLm6Ek9nL9ylP-bkK10yzAaD1uIDF83Qp_A,1828
|
8
|
-
futurehouse_client/models/rest.py,sha256=lgwkMIXz0af-49BYSkKeS7SRqvN3motqnAikDN4YGTc,789
|
9
|
-
futurehouse_client/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
|
-
futurehouse_client/utils/auth.py,sha256=Lq9mjSGc7iuRP6fmLICCS6KjzLHN6-tJUuhYp0XXrkE,3342
|
11
|
-
futurehouse_client/utils/general.py,sha256=A_rtTiYW30ELGEZlWCIArO7q1nEmqi8hUlmBRYkMQ_c,767
|
12
|
-
futurehouse_client/utils/module_utils.py,sha256=aFyd-X-pDARXz9GWpn8SSViUVYdSbuy9vSkrzcVIaGI,4955
|
13
|
-
futurehouse_client/utils/monitoring.py,sha256=UjRlufe67kI3VxRHOd5fLtJmlCbVA2Wqwpd4uZhXkQM,8728
|
14
|
-
futurehouse_client-0.3.18.dev186.dist-info/METADATA,sha256=PvjehEQZu2ihl7kG1uDvWJVUxyYbV7J-VmAe42Ml3zo,12767
|
15
|
-
futurehouse_client-0.3.18.dev186.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
|
16
|
-
futurehouse_client-0.3.18.dev186.dist-info/top_level.txt,sha256=TRuLUCt_qBnggdFHCX4O_BoCu1j2X43lKfIZC-ElwWY,19
|
17
|
-
futurehouse_client-0.3.18.dev186.dist-info/RECORD,,
|
{futurehouse_client-0.3.18.dev186.dist-info → futurehouse_client-0.3.19.dist-info}/top_level.txt
RENAMED
File without changes
|