futurehouse-client 0.0.5__tar.gz → 0.0.6__tar.gz
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-0.0.5 → futurehouse_client-0.0.6}/PKG-INFO +3 -1
- {futurehouse_client-0.0.5 → futurehouse_client-0.0.6}/futurehouse_client/clients/job_client.py +82 -22
- {futurehouse_client-0.0.5 → futurehouse_client-0.0.6}/futurehouse_client/clients/rest_client.py +50 -10
- futurehouse_client-0.0.6/futurehouse_client/utils/monitoring.py +246 -0
- {futurehouse_client-0.0.5 → futurehouse_client-0.0.6}/futurehouse_client.egg-info/PKG-INFO +3 -1
- {futurehouse_client-0.0.5 → futurehouse_client-0.0.6}/futurehouse_client.egg-info/SOURCES.txt +1 -0
- {futurehouse_client-0.0.5 → futurehouse_client-0.0.6}/futurehouse_client.egg-info/requires.txt +3 -0
- {futurehouse_client-0.0.5 → futurehouse_client-0.0.6}/pyproject.toml +4 -1
- {futurehouse_client-0.0.5 → futurehouse_client-0.0.6}/LICENSE +0 -0
- {futurehouse_client-0.0.5 → futurehouse_client-0.0.6}/README.md +0 -0
- {futurehouse_client-0.0.5 → futurehouse_client-0.0.6}/docs/__init__.py +0 -0
- {futurehouse_client-0.0.5 → futurehouse_client-0.0.6}/docs/client_notebook.ipynb +0 -0
- {futurehouse_client-0.0.5 → futurehouse_client-0.0.6}/futurehouse_client/__init__.py +0 -0
- {futurehouse_client-0.0.5 → futurehouse_client-0.0.6}/futurehouse_client/clients/__init__.py +0 -0
- {futurehouse_client-0.0.5 → futurehouse_client-0.0.6}/futurehouse_client/models/__init__.py +0 -0
- {futurehouse_client-0.0.5 → futurehouse_client-0.0.6}/futurehouse_client/models/app.py +0 -0
- {futurehouse_client-0.0.5 → futurehouse_client-0.0.6}/futurehouse_client/models/client.py +0 -0
- {futurehouse_client-0.0.5 → futurehouse_client-0.0.6}/futurehouse_client/models/rest.py +0 -0
- {futurehouse_client-0.0.5 → futurehouse_client-0.0.6}/futurehouse_client/utils/__init__.py +0 -0
- {futurehouse_client-0.0.5 → futurehouse_client-0.0.6}/futurehouse_client/utils/module_utils.py +0 -0
- {futurehouse_client-0.0.5 → futurehouse_client-0.0.6}/futurehouse_client.egg-info/dependency_links.txt +0 -0
- {futurehouse_client-0.0.5 → futurehouse_client-0.0.6}/futurehouse_client.egg-info/top_level.txt +0 -0
- {futurehouse_client-0.0.5 → futurehouse_client-0.0.6}/setup.cfg +0 -0
- {futurehouse_client-0.0.5 → futurehouse_client-0.0.6}/tests/test_rest.py +0 -0
- {futurehouse_client-0.0.5 → futurehouse_client-0.0.6}/uv.lock +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: futurehouse-client
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.6
|
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
|
@@ -35,6 +35,8 @@ Requires-Dist: pytest-timeout; extra == "dev"
|
|
35
35
|
Requires-Dist: pytest-xdist; extra == "dev"
|
36
36
|
Requires-Dist: ruff; extra == "dev"
|
37
37
|
Requires-Dist: setuptools_scm; extra == "dev"
|
38
|
+
Provides-Extra: monitoring
|
39
|
+
Requires-Dist: newrelic>=8.8.0; extra == "monitoring"
|
38
40
|
|
39
41
|
# FutureHouse Platform API Documentation
|
40
42
|
|
{futurehouse_client-0.0.5 → futurehouse_client-0.0.6}/futurehouse_client/clients/job_client.py
RENAMED
@@ -14,6 +14,10 @@ from futurehouse_client.models.rest import (
|
|
14
14
|
StoreAgentStatePostRequest,
|
15
15
|
StoreEnvironmentFrameRequest,
|
16
16
|
)
|
17
|
+
from futurehouse_client.utils.monitoring import (
|
18
|
+
external_trace,
|
19
|
+
insert_distributed_trace_headers,
|
20
|
+
)
|
17
21
|
|
18
22
|
logger = logging.getLogger(__name__)
|
19
23
|
|
@@ -90,14 +94,32 @@ class JobClient:
|
|
90
94
|
data = FinalEnvironmentRequest(status=status)
|
91
95
|
try:
|
92
96
|
async with httpx.AsyncClient(timeout=self.REQUEST_TIMEOUT) as client:
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
97
|
+
url = f"{self.base_uri}/v0.1/trajectories/{self.trajectory_id}/environment-frame"
|
98
|
+
headers = {
|
99
|
+
"Authorization": f"Bearer {self.oauth_jwt}",
|
100
|
+
"x-trajectory-id": self.trajectory_id,
|
101
|
+
}
|
102
|
+
|
103
|
+
with external_trace(
|
104
|
+
url=url,
|
105
|
+
method="PATCH",
|
106
|
+
library="httpx",
|
107
|
+
custom_params={
|
108
|
+
"trajectory_id": self.trajectory_id,
|
109
|
+
"agent": self.agent,
|
110
|
+
"environment": self.environment,
|
111
|
+
"status": status,
|
112
|
+
"operation": "finalize_environment",
|
99
113
|
},
|
100
|
-
)
|
114
|
+
):
|
115
|
+
headers = insert_distributed_trace_headers(headers)
|
116
|
+
|
117
|
+
response = await client.patch(
|
118
|
+
url=url,
|
119
|
+
json=data.model_dump(mode="json"),
|
120
|
+
headers=headers,
|
121
|
+
)
|
122
|
+
|
101
123
|
response.raise_for_status()
|
102
124
|
logger.debug(f"Environment updated with status {status}")
|
103
125
|
except httpx.HTTPStatusError:
|
@@ -145,14 +167,32 @@ class JobClient:
|
|
145
167
|
|
146
168
|
try:
|
147
169
|
async with httpx.AsyncClient(timeout=self.REQUEST_TIMEOUT) as client:
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
170
|
+
url = f"{self.base_uri}/v0.1/trajectories/{self.trajectory_id}/agent-state"
|
171
|
+
headers = {
|
172
|
+
"Authorization": f"Bearer {self.oauth_jwt}",
|
173
|
+
"x-trajectory-id": self.trajectory_id,
|
174
|
+
}
|
175
|
+
|
176
|
+
with external_trace(
|
177
|
+
url=url,
|
178
|
+
method="POST",
|
179
|
+
library="httpx",
|
180
|
+
custom_params={
|
181
|
+
"trajectory_id": self.trajectory_id,
|
182
|
+
"agent": self.agent,
|
183
|
+
"environment": self.environment,
|
184
|
+
"step": step,
|
185
|
+
"timestep": self.current_timestep,
|
186
|
+
"operation": "store_agent_state",
|
154
187
|
},
|
155
|
-
)
|
188
|
+
):
|
189
|
+
headers = insert_distributed_trace_headers(headers)
|
190
|
+
|
191
|
+
response = await client.post(
|
192
|
+
url=url,
|
193
|
+
json=data.model_dump(mode="json"),
|
194
|
+
headers=headers,
|
195
|
+
)
|
156
196
|
response.raise_for_status()
|
157
197
|
logger.info(f"Successfully stored agent state for step {step}")
|
158
198
|
return response.json()
|
@@ -198,14 +238,34 @@ class JobClient:
|
|
198
238
|
|
199
239
|
try:
|
200
240
|
async with httpx.AsyncClient(timeout=self.REQUEST_TIMEOUT) as client:
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
241
|
+
url = f"{self.base_uri}/v0.1/trajectories/{self.trajectory_id}/environment-frame"
|
242
|
+
headers = {
|
243
|
+
"Authorization": f"Bearer {self.oauth_jwt}",
|
244
|
+
"x-trajectory-id": self.trajectory_id,
|
245
|
+
}
|
246
|
+
|
247
|
+
custom_params = {
|
248
|
+
"trajectory_id": self.trajectory_id,
|
249
|
+
"agent": self.agent,
|
250
|
+
"environment": self.environment,
|
251
|
+
"timestep": self.current_timestep,
|
252
|
+
"operation": "store_environment_frame",
|
253
|
+
}
|
254
|
+
if self.current_step:
|
255
|
+
custom_params["step"] = self.current_step
|
256
|
+
if state_identifier:
|
257
|
+
custom_params["state_identifier"] = state_identifier
|
258
|
+
|
259
|
+
with external_trace(
|
260
|
+
url=url, method="POST", library="httpx", custom_params=custom_params
|
261
|
+
):
|
262
|
+
headers = insert_distributed_trace_headers(headers)
|
263
|
+
|
264
|
+
response = await client.post(
|
265
|
+
url=url,
|
266
|
+
json=data.model_dump(mode="json"),
|
267
|
+
headers=headers,
|
268
|
+
)
|
209
269
|
response.raise_for_status()
|
210
270
|
logger.debug(
|
211
271
|
f"Successfully stored environment frame for state {state_identifier}",
|
{futurehouse_client-0.0.5 → futurehouse_client-0.0.6}/futurehouse_client/clients/rest_client.py
RENAMED
@@ -15,9 +15,19 @@ from uuid import UUID
|
|
15
15
|
|
16
16
|
import cloudpickle
|
17
17
|
from aviary.functional import EnvironmentBuilder
|
18
|
-
from httpx import
|
18
|
+
from httpx import (
|
19
|
+
Client,
|
20
|
+
CloseError,
|
21
|
+
ConnectError,
|
22
|
+
ConnectTimeout,
|
23
|
+
HTTPStatusError,
|
24
|
+
NetworkError,
|
25
|
+
ReadError,
|
26
|
+
ReadTimeout,
|
27
|
+
RemoteProtocolError,
|
28
|
+
)
|
19
29
|
from pydantic import BaseModel, ConfigDict, model_validator
|
20
|
-
from requests.exceptions import Timeout
|
30
|
+
from requests.exceptions import RequestException, Timeout
|
21
31
|
from tenacity import (
|
22
32
|
retry,
|
23
33
|
retry_if_exception_type,
|
@@ -37,11 +47,29 @@ from futurehouse_client.utils.module_utils import (
|
|
37
47
|
OrganizationSelector,
|
38
48
|
fetch_environment_function_docstring,
|
39
49
|
)
|
50
|
+
from futurehouse_client.utils.monitoring import (
|
51
|
+
external_trace,
|
52
|
+
)
|
40
53
|
|
41
54
|
logger = logging.getLogger(__name__)
|
42
55
|
|
43
56
|
TaskRequest.model_rebuild()
|
44
57
|
|
58
|
+
retry_if_connection_error = retry_if_exception_type((
|
59
|
+
# From requests
|
60
|
+
Timeout,
|
61
|
+
ConnectionError,
|
62
|
+
RequestException,
|
63
|
+
# From httpx
|
64
|
+
ConnectError,
|
65
|
+
ConnectTimeout,
|
66
|
+
ReadTimeout,
|
67
|
+
ReadError,
|
68
|
+
NetworkError,
|
69
|
+
RemoteProtocolError,
|
70
|
+
CloseError,
|
71
|
+
))
|
72
|
+
|
45
73
|
FILE_UPLOAD_IGNORE_PARTS = {
|
46
74
|
".ruff_cache",
|
47
75
|
"__pycache__",
|
@@ -342,7 +370,7 @@ class RestClient:
|
|
342
370
|
@retry(
|
343
371
|
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
|
344
372
|
wait=wait_exponential(multiplier=RETRY_MULTIPLIER, max=MAX_RETRY_WAIT),
|
345
|
-
retry=
|
373
|
+
retry=retry_if_connection_error,
|
346
374
|
)
|
347
375
|
def get_task(
|
348
376
|
self, task_id: str | None = None, history: bool = False, verbose: bool = False
|
@@ -350,10 +378,22 @@ class RestClient:
|
|
350
378
|
"""Get details for a specific task."""
|
351
379
|
try:
|
352
380
|
task_id = task_id or self.trajectory_id
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
381
|
+
url = f"/v0.1/trajectories/{task_id}"
|
382
|
+
full_url = f"{self.base_url}{url}"
|
383
|
+
|
384
|
+
with external_trace(
|
385
|
+
url=full_url,
|
386
|
+
method="GET",
|
387
|
+
library="httpx",
|
388
|
+
custom_params={
|
389
|
+
"operation": "get_job",
|
390
|
+
"job_id": task_id,
|
391
|
+
},
|
392
|
+
):
|
393
|
+
response = self.client.get(
|
394
|
+
url,
|
395
|
+
params={"history": history},
|
396
|
+
)
|
357
397
|
response.raise_for_status()
|
358
398
|
verbose_response = TaskResponseVerbose(**response.json())
|
359
399
|
if verbose:
|
@@ -372,7 +412,7 @@ class RestClient:
|
|
372
412
|
@retry(
|
373
413
|
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
|
374
414
|
wait=wait_exponential(multiplier=RETRY_MULTIPLIER, max=MAX_RETRY_WAIT),
|
375
|
-
retry=
|
415
|
+
retry=retry_if_connection_error,
|
376
416
|
)
|
377
417
|
def create_task(self, task_data: TaskRequest | dict[str, Any]):
|
378
418
|
"""Create a new futurehouse task."""
|
@@ -398,7 +438,7 @@ class RestClient:
|
|
398
438
|
@retry(
|
399
439
|
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
|
400
440
|
wait=wait_exponential(multiplier=RETRY_MULTIPLIER, max=MAX_RETRY_WAIT),
|
401
|
-
retry=
|
441
|
+
retry=retry_if_connection_error,
|
402
442
|
)
|
403
443
|
def get_build_status(self, build_id: UUID | None = None) -> dict[str, Any]:
|
404
444
|
"""Get the status of a build."""
|
@@ -411,7 +451,7 @@ class RestClient:
|
|
411
451
|
@retry(
|
412
452
|
stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
|
413
453
|
wait=wait_exponential(multiplier=RETRY_MULTIPLIER, max=MAX_RETRY_WAIT),
|
414
|
-
retry=
|
454
|
+
retry=retry_if_connection_error,
|
415
455
|
)
|
416
456
|
def create_job(self, config: JobDeploymentConfig) -> dict[str, Any]: # noqa: PLR0915
|
417
457
|
"""Creates a futurehouse job deployment from the environment and environment files.
|
@@ -0,0 +1,246 @@
|
|
1
|
+
"""Utilities for monitoring and observability integration.
|
2
|
+
|
3
|
+
This module provides utilities for integrating with monitoring and observability tools
|
4
|
+
like NewRelic. It handles availability checking and provides wrapper functions that
|
5
|
+
conditionally use monitoring tools only when they're available and properly initialized.
|
6
|
+
|
7
|
+
NOTE: NewRelic is an optional dependency. To use monitoring functionality, install
|
8
|
+
the package with the monitoring extras:
|
9
|
+
pip install futurehouse-client[monitoring]
|
10
|
+
|
11
|
+
Environment variables:
|
12
|
+
NEW_RELIC_ENVIRONMENT: The environment to use for NewRelic reporting (dev, staging, prod)
|
13
|
+
NEW_RELIC_CONFIG_FILE: Path to the NewRelic configuration file
|
14
|
+
NEW_RELIC_LICENSE_KEY: Your NewRelic license key
|
15
|
+
"""
|
16
|
+
|
17
|
+
import contextlib
|
18
|
+
import json
|
19
|
+
import logging
|
20
|
+
import os
|
21
|
+
from contextvars import ContextVar
|
22
|
+
|
23
|
+
logger = logging.getLogger(__name__)
|
24
|
+
|
25
|
+
# Check if NewRelic initialization is enabled (default: False)
|
26
|
+
NEWRELIC_AUTO_INIT = (
|
27
|
+
os.environ.get("FUTUREHOUSE_NEWRELIC_AUTO_INIT", "false").lower() == "true"
|
28
|
+
)
|
29
|
+
|
30
|
+
# Check if NewRelic is installed
|
31
|
+
try:
|
32
|
+
import newrelic.agent
|
33
|
+
|
34
|
+
NEWRELIC_INSTALLED = True
|
35
|
+
except ImportError:
|
36
|
+
NEWRELIC_INSTALLED = False
|
37
|
+
logger.info("NewRelic package not installed")
|
38
|
+
|
39
|
+
# Context variable to track NewRelic initialization state
|
40
|
+
newrelic_initialized: ContextVar[bool] = ContextVar(
|
41
|
+
"newrelic_initialized", default=False
|
42
|
+
)
|
43
|
+
|
44
|
+
|
45
|
+
def ensure_newrelic() -> bool: # noqa: PLR0911
|
46
|
+
"""Check if NewRelic is available and initialize it if auto-init is enabled.
|
47
|
+
|
48
|
+
This will use environment variables:
|
49
|
+
- FUTUREHOUSE_NEWRELIC_AUTO_INIT: Set to "true" to enable automatic initialization (default: "false")
|
50
|
+
- NEW_RELIC_CONFIG_FILE: Path to the NewRelic config file (required)
|
51
|
+
- NEW_RELIC_ENVIRONMENT: Environment (dev, staging, prod)
|
52
|
+
|
53
|
+
Returns:
|
54
|
+
bool: True if NewRelic is available for use, False otherwise
|
55
|
+
"""
|
56
|
+
if newrelic_initialized.get():
|
57
|
+
return True
|
58
|
+
|
59
|
+
if not NEWRELIC_AUTO_INIT:
|
60
|
+
return False
|
61
|
+
|
62
|
+
if not NEWRELIC_INSTALLED:
|
63
|
+
logger.info("NewRelic package is not installed")
|
64
|
+
return False
|
65
|
+
|
66
|
+
nr_config = os.environ.get("NEW_RELIC_CONFIG_FILE")
|
67
|
+
if not nr_config:
|
68
|
+
logger.warning("NEW_RELIC_CONFIG_FILE environment variable must be set")
|
69
|
+
return False
|
70
|
+
|
71
|
+
try:
|
72
|
+
nr_env = os.environ.get("NEW_RELIC_ENVIRONMENT", "dev")
|
73
|
+
newrelic.agent.initialize(nr_config, environment=nr_env)
|
74
|
+
|
75
|
+
app = newrelic.agent.application()
|
76
|
+
if app is None:
|
77
|
+
logger.warning("NewRelic initialization failed: no application returned")
|
78
|
+
return False
|
79
|
+
|
80
|
+
newrelic_initialized.set(True)
|
81
|
+
logger.info(f"NewRelic initialized successfully for environment: {nr_env}")
|
82
|
+
except Exception as e:
|
83
|
+
logger.warning(f"NewRelic initialization failed: {e}")
|
84
|
+
return False
|
85
|
+
|
86
|
+
else:
|
87
|
+
return True
|
88
|
+
|
89
|
+
|
90
|
+
def insert_distributed_trace_headers(headers: dict[str, str]) -> dict[str, str]:
|
91
|
+
"""Insert distributed trace headers if NewRelic is available.
|
92
|
+
|
93
|
+
Args:
|
94
|
+
headers: The headers dictionary to modify.
|
95
|
+
|
96
|
+
Returns:
|
97
|
+
The modified headers dictionary with NewRelic distributed trace headers if available,
|
98
|
+
otherwise the original headers.
|
99
|
+
"""
|
100
|
+
if not ensure_newrelic():
|
101
|
+
return headers
|
102
|
+
|
103
|
+
try:
|
104
|
+
nr_headers: list[tuple[str, str]] = []
|
105
|
+
newrelic.agent.insert_distributed_trace_headers(nr_headers)
|
106
|
+
for header in nr_headers:
|
107
|
+
headers[header[0]] = header[1]
|
108
|
+
except Exception as e:
|
109
|
+
logger.info(f"Error inserting distributed trace headers: {e}")
|
110
|
+
|
111
|
+
return headers
|
112
|
+
|
113
|
+
|
114
|
+
@contextlib.contextmanager
|
115
|
+
def external_trace(
|
116
|
+
url: str,
|
117
|
+
method: str = "GET",
|
118
|
+
library: str = "httpx",
|
119
|
+
custom_params: dict | None = None,
|
120
|
+
):
|
121
|
+
"""Context manager for NewRelic external traces that works whether NewRelic is available or not.
|
122
|
+
|
123
|
+
Creates an ExternalTrace span in NewRelic for HTTP requests to external services. This provides detailed timing and proper distributed tracing between services.
|
124
|
+
"External" refers to HTTP requests made to services outside of your application (like third-party APIs or other microservices).
|
125
|
+
|
126
|
+
Args:
|
127
|
+
url: The URL being called.
|
128
|
+
method: The HTTP method (GET, POST, etc.).
|
129
|
+
library: The library being used for the HTTP call.
|
130
|
+
custom_params: Optional dictionary of custom parameters to add to the transaction.
|
131
|
+
|
132
|
+
Yields:
|
133
|
+
None: This is a context manager that doesn't yield a value.
|
134
|
+
"""
|
135
|
+
if not ensure_newrelic():
|
136
|
+
yield
|
137
|
+
return
|
138
|
+
|
139
|
+
# Proceed with tracing
|
140
|
+
try:
|
141
|
+
with newrelic.agent.ExternalTrace(
|
142
|
+
library=library,
|
143
|
+
url=url,
|
144
|
+
method=method,
|
145
|
+
):
|
146
|
+
txn = newrelic.agent.current_transaction()
|
147
|
+
if txn:
|
148
|
+
txn.add_custom_parameter("request_url", url)
|
149
|
+
txn.add_custom_parameter("request_method", method)
|
150
|
+
|
151
|
+
if custom_params:
|
152
|
+
for key, value in custom_params.items():
|
153
|
+
txn.add_custom_parameter(key, value)
|
154
|
+
|
155
|
+
yield
|
156
|
+
except Exception as e:
|
157
|
+
# If there's an exception in the transaction handling,
|
158
|
+
# log it but don't let it break the client
|
159
|
+
try:
|
160
|
+
txn = newrelic.agent.current_transaction()
|
161
|
+
if txn:
|
162
|
+
txn.add_custom_parameter("external_request_url", url)
|
163
|
+
txn.add_custom_parameter("external_request_method", method)
|
164
|
+
txn.add_custom_parameter("error_type", e.__class__.__name__)
|
165
|
+
txn.add_custom_parameter("error_message", str(e))
|
166
|
+
txn.record_exception(e)
|
167
|
+
except Exception as nr_error:
|
168
|
+
# If even the error handling fails, just log it
|
169
|
+
logger.info(f"Failed to record NewRelic error: {nr_error}")
|
170
|
+
|
171
|
+
# Always re-raise the original exception
|
172
|
+
raise
|
173
|
+
|
174
|
+
|
175
|
+
@contextlib.contextmanager
|
176
|
+
def monitored_transaction(
|
177
|
+
name: str, group: str = "Task", custom_params: dict | None = None
|
178
|
+
):
|
179
|
+
"""Context manager for NewRelic transactions that appear in distributed traces.
|
180
|
+
|
181
|
+
This uses WebTransaction for better visibility in distributed traces, even for
|
182
|
+
background job-type workloads.
|
183
|
+
|
184
|
+
Args:
|
185
|
+
name: Name of the transaction (e.g., 'run_crow_job')
|
186
|
+
group: Group for transaction categorization (default: 'Task')
|
187
|
+
custom_params: Optional dictionary of custom parameters to add to the transaction
|
188
|
+
|
189
|
+
Yields:
|
190
|
+
None: This is a context manager that doesn't yield a value.
|
191
|
+
"""
|
192
|
+
if not ensure_newrelic():
|
193
|
+
logger.info("NewRelic not available, skipping transaction")
|
194
|
+
yield
|
195
|
+
return
|
196
|
+
|
197
|
+
try:
|
198
|
+
app = newrelic.agent.application()
|
199
|
+
if app is None:
|
200
|
+
logger.warning("No NewRelic application found, skipping transaction")
|
201
|
+
yield
|
202
|
+
return
|
203
|
+
|
204
|
+
parsed_headers = None
|
205
|
+
trace_context = os.environ.get("NEW_RELIC_DISTRIBUTED_TRACING_CONTEXT")
|
206
|
+
if trace_context:
|
207
|
+
try:
|
208
|
+
parsed_headers = json.loads(trace_context)
|
209
|
+
except Exception as e:
|
210
|
+
logger.warning(f"Failed to parse distributed trace context: {e}")
|
211
|
+
else:
|
212
|
+
logger.info("No distributed trace context found")
|
213
|
+
|
214
|
+
with newrelic.agent.WebTransaction(app, name, group=group):
|
215
|
+
if parsed_headers:
|
216
|
+
current_txn = newrelic.agent.current_transaction()
|
217
|
+
if current_txn:
|
218
|
+
accepted = newrelic.agent.accept_distributed_trace_headers(
|
219
|
+
parsed_headers
|
220
|
+
)
|
221
|
+
if not accepted:
|
222
|
+
logger.warning("Failed to accept distributed trace headers")
|
223
|
+
|
224
|
+
if custom_params:
|
225
|
+
txn = newrelic.agent.current_transaction()
|
226
|
+
if txn:
|
227
|
+
for key, value in custom_params.items():
|
228
|
+
txn.add_custom_parameter(key, value)
|
229
|
+
|
230
|
+
yield
|
231
|
+
logger.info(f"Completed NewRelic transaction: {name}")
|
232
|
+
except Exception as e:
|
233
|
+
# If there's an exception in the transaction handling,
|
234
|
+
# log it but don't let it break the client
|
235
|
+
try:
|
236
|
+
txn = newrelic.agent.current_transaction()
|
237
|
+
if txn:
|
238
|
+
txn.add_custom_parameter("error_type", e.__class__.__name__)
|
239
|
+
txn.add_custom_parameter("error_message", str(e))
|
240
|
+
txn.record_exception(e)
|
241
|
+
except Exception as nr_error:
|
242
|
+
# If even the error handling fails, just log it
|
243
|
+
logger.info(f"Failed to record NewRelic error: {nr_error}")
|
244
|
+
|
245
|
+
logger.warning(f"Error in NewRelic transaction: {e}")
|
246
|
+
yield
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: futurehouse-client
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.6
|
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
|
@@ -35,6 +35,8 @@ Requires-Dist: pytest-timeout; extra == "dev"
|
|
35
35
|
Requires-Dist: pytest-xdist; extra == "dev"
|
36
36
|
Requires-Dist: ruff; extra == "dev"
|
37
37
|
Requires-Dist: setuptools_scm; extra == "dev"
|
38
|
+
Provides-Extra: monitoring
|
39
|
+
Requires-Dist: newrelic>=8.8.0; extra == "monitoring"
|
38
40
|
|
39
41
|
# FutureHouse Platform API Documentation
|
40
42
|
|
@@ -24,7 +24,7 @@ dependencies = [
|
|
24
24
|
"tenacity",
|
25
25
|
]
|
26
26
|
description = "A client for interacting with endpoints of the FutureHouse service."
|
27
|
-
|
27
|
+
dynamic = ["version"]
|
28
28
|
name = "futurehouse-client"
|
29
29
|
readme = "README.md"
|
30
30
|
requires-python = ">=3.11,<3.13" # Pin to have dm-tree macOS wheels and avoid requiring cmake
|
@@ -48,6 +48,9 @@ dev = [
|
|
48
48
|
"ruff",
|
49
49
|
"setuptools_scm",
|
50
50
|
]
|
51
|
+
monitoring = [
|
52
|
+
"newrelic>=8.8.0",
|
53
|
+
]
|
51
54
|
|
52
55
|
[tool.setuptools]
|
53
56
|
license-files = []
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{futurehouse_client-0.0.5 → futurehouse_client-0.0.6}/futurehouse_client/clients/__init__.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{futurehouse_client-0.0.5 → futurehouse_client-0.0.6}/futurehouse_client/utils/module_utils.py
RENAMED
File without changes
|
File without changes
|
{futurehouse_client-0.0.5 → futurehouse_client-0.0.6}/futurehouse_client.egg-info/top_level.txt
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|