truefoundry 0.7.2__py3-none-any.whl → 0.9.0rc1__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.

Potentially problematic release.


This version of truefoundry might be problematic. Click here for more details.

truefoundry/_client.py CHANGED
@@ -33,4 +33,4 @@ class _LazyTrueFoundry(truefoundry_sdk.TrueFoundry):
33
33
  )
34
34
 
35
35
 
36
- client = _LazyTrueFoundry().v1
36
+ client = _LazyTrueFoundry()
truefoundry/cli/util.py CHANGED
@@ -51,6 +51,9 @@ def handle_exception(exception):
51
51
  title="Command Failed",
52
52
  border_style="red",
53
53
  )
54
+ elif isinstance(exception, KeyError):
55
+ # KeyError messages in Python just suck - they tell you the key that was missing, but not the context
56
+ console.print_exception(show_locals=False, max_frames=1)
54
57
  else:
55
58
  print_dict_as_table_panel(
56
59
  {"Error": str(exception)},
@@ -109,6 +109,9 @@ class PythonSDKConfig(BaseModel):
109
109
  use_sfy_server_auth_apis: Optional[bool] = Field(
110
110
  alias="useSFYServerAuthAPIs", default=False
111
111
  )
112
+ python_build_default_image_tag: str = Field(
113
+ alias="pythonBuildDefaultImageTag", default="3.11"
114
+ )
112
115
 
113
116
 
114
117
  class DeviceCode(BaseModel):
@@ -1,6 +1,6 @@
1
1
  # generated by datamodel-codegen:
2
2
  # filename: application.json
3
- # timestamp: 2025-05-03T01:45:46+00:00
3
+ # timestamp: 2025-05-15T05:28:11+00:00
4
4
 
5
5
  from __future__ import annotations
6
6
 
@@ -360,13 +360,6 @@ class Image(BaseModel):
360
360
  )
361
361
 
362
362
 
363
- class JobEvent(str, Enum):
364
- START = "START"
365
- SUCCEEDED = "SUCCEEDED"
366
- FAILED = "FAILED"
367
- TERMINATED = "TERMINATED"
368
-
369
-
370
363
  class Claim(BaseModel):
371
364
  key: str
372
365
  values: List[str]
@@ -623,9 +616,11 @@ class PythonBuild(BaseModel):
623
616
  """
624
617
 
625
618
  type: Literal["tfy-python-buildpack"] = Field(..., description="")
626
- python_version: constr(regex=r"^\d+(\.\d+){1,2}([\-\.a-z0-9]+)?$") = Field(
627
- "3.11",
628
- description="Python version to run your application. Should be one of the tags listed on [Official Python Docker Page](https://hub.docker.com/_/python)",
619
+ python_version: Optional[constr(regex=r"^\d+(\.\d+){1,2}([\-\.a-z0-9]+)?$")] = (
620
+ Field(
621
+ None,
622
+ description="Python version to run your application. Should be one of the tags listed on [Official Python Docker Page](https://hub.docker.com/_/python)",
623
+ )
629
624
  )
630
625
  build_context_path: str = Field(
631
626
  "./", description="Build path relative to project root path."
@@ -963,9 +958,11 @@ class TaskPythonBuild(BaseModel):
963
958
  None,
964
959
  description="FQN of the container registry. If you can't find your registry here,\nadd it through the [Integrations](/integrations?tab=docker-registry) page",
965
960
  )
966
- python_version: constr(regex=r"^\d+(\.\d+){1,2}([\-\.a-z0-9]+)?$") = Field(
967
- "3.11",
968
- description="Python version to run your application. Should be one of the tags listed on [Official Python Docker Page](https://hub.docker.com/_/python)",
961
+ python_version: Optional[constr(regex=r"^\d+(\.\d+){1,2}([\-\.a-z0-9]+)?$")] = (
962
+ Field(
963
+ None,
964
+ description="Python version to run your application. Should be one of the tags listed on [Official Python Docker Page](https://hub.docker.com/_/python)",
965
+ )
969
966
  )
970
967
  requirements_path: Optional[str] = Field(
971
968
  None,
@@ -1060,13 +1057,6 @@ class WorkbenchImage(BaseModel):
1060
1057
  )
1061
1058
 
1062
1059
 
1063
- class WorkflowEvent(str, Enum):
1064
- SUCCEEDED = "SUCCEEDED"
1065
- FAILED = "FAILED"
1066
- ABORTED = "ABORTED"
1067
- TIMED_OUT = "TIMED_OUT"
1068
-
1069
-
1070
1060
  class ArtifactsDownload(BaseModel):
1071
1061
  """
1072
1062
  Download and cache models in a volume to enhance loading speeds and reduce costs by avoiding repeated downloads. [Docs](https://docs.truefoundry.com/docs/download-and-cache-models)
@@ -1485,10 +1475,6 @@ class WorkflowAlert(BaseModel):
1485
1475
  """
1486
1476
 
1487
1477
  notification_target: Optional[Union[Email, SlackWebhook, SlackBot]] = None
1488
- events: List[WorkflowEvent] = Field(
1489
- ...,
1490
- description="Specify the events to send alerts for, it should be one of the following: SUCCEEDED, FAILED, ABORTED, TIMED_OUT",
1491
- )
1492
1478
  on_completion: bool = Field(
1493
1479
  False, description="Send an alert when the job completes"
1494
1480
  )
@@ -1556,10 +1542,6 @@ class JobAlert(BaseModel):
1556
1542
  description="List of recipients' email addresses if the notification channel is Email.",
1557
1543
  )
1558
1544
  notification_target: Optional[Union[Email, SlackWebhook, SlackBot]] = None
1559
- events: List[JobEvent] = Field(
1560
- ...,
1561
- description="Specify the events to send alerts for, it should be one of the following: START, SUCCEEDED, FAILED, TERMINATED",
1562
- )
1563
1545
  on_start: bool = Field(False, description="Send an alert when the job starts")
1564
1546
  on_completion: bool = False
1565
1547
  on_failure: bool = Field(True, description="Send an alert when the job fails")
@@ -36,6 +36,10 @@ def build(
36
36
  build_configuration: PythonBuild,
37
37
  extra_opts: Optional[List[str]] = None,
38
38
  ):
39
+ if not build_configuration.python_version:
40
+ raise ValueError(
41
+ "`python_version` is required for `tfy-python-buildpack` builder"
42
+ )
39
43
  mount_python_package_manager_conf_secret = (
40
44
  has_python_package_manager_conf_secret(extra_opts) if extra_opts else False
41
45
  )
@@ -174,6 +174,10 @@ def generate_dockerfile_content(
174
174
  requirements_destination_path = (
175
175
  "/tmp/requirements.txt" if requirements_path else None
176
176
  )
177
+ if not build_configuration.python_version:
178
+ raise ValueError(
179
+ "`python_version` is required for `tfy-python-buildpack` builder"
180
+ )
177
181
  if package_manager == PythonPackageManager.PIP.value:
178
182
  python_packages_install_command = generate_pip_install_command(
179
183
  requirements_path=requirements_destination_path,
@@ -1,10 +1,7 @@
1
1
  try:
2
2
  from mcp import ClientSession
3
+ from mcp.client.streamable_http import streamablehttp_client
3
4
  from mcp.types import TextContent
4
-
5
- from truefoundry.deploy.lib.clients._mcp_streamable_http import (
6
- streamablehttp_client,
7
- )
8
5
  except ImportError:
9
6
  import sys
10
7
 
@@ -75,7 +72,7 @@ class AskClient:
75
72
  (
76
73
  read_stream,
77
74
  write_stream,
78
- self._terminate_cb,
75
+ _,
79
76
  ) = await self._streams_context.__aenter__()
80
77
  self._session_context = ClientSession(
81
78
  read_stream=read_stream, write_stream=write_stream
@@ -99,7 +96,6 @@ class AskClient:
99
96
 
100
97
  async def cleanup(self):
101
98
  """Properly close all async contexts opened during session initialization."""
102
- await self._terminate_cb()
103
99
  for context in [
104
100
  getattr(self, "_session_context", None),
105
101
  getattr(self, "_streams_context", None),
@@ -25,6 +25,19 @@ from truefoundry.pydantic_v1 import BaseModel
25
25
  Component = TypeVar("Component", bound=BaseModel)
26
26
 
27
27
 
28
+ def _set_python_version_if_missing(build_spec: autogen_models.PythonBuild) -> None:
29
+ if build_spec.python_version is None:
30
+ client = ServiceFoundryServiceClient()
31
+ server_default_python_version = (
32
+ client.python_sdk_config.python_build_default_image_tag
33
+ )
34
+ logger.warning(
35
+ f"No python version was provided in the spec, "
36
+ f"using the default python version ({server_default_python_version}) from the server."
37
+ )
38
+ build_spec.python_version = server_default_python_version
39
+
40
+
28
41
  def _handle_if_local_source(component: Component, workspace_fqn: str) -> Component:
29
42
  if (
30
43
  hasattr(component, "image")
@@ -33,7 +46,7 @@ def _handle_if_local_source(component: Component, workspace_fqn: str) -> Compone
33
46
  ):
34
47
  new_component = component.copy(deep=True)
35
48
 
36
- if component.image.build_source.local_build:
49
+ if new_component.image.build_source.local_build:
37
50
  if not env_has_docker():
38
51
  logger.warning(
39
52
  "Did not find Docker locally installed on this system, image will be built remotely. "
@@ -64,25 +77,33 @@ def _handle_if_local_source(component: Component, workspace_fqn: str) -> Compone
64
77
  local_build = False
65
78
 
66
79
  if local_build:
80
+ if isinstance(new_component.image.build_spec, autogen_models.PythonBuild):
81
+ _set_python_version_if_missing(new_component.image.build_spec)
67
82
  # We are to build the image locally, push and update `image` in spec
68
- logger.info("Building image for %s '%s'", component.type, component.name)
83
+ logger.info(
84
+ "Building image for %s '%s'", new_component.type, new_component.name
85
+ )
69
86
  new_component.image = local_source_to_image(
70
- build=component.image,
71
- docker_registry_fqn=component.image.docker_registry,
87
+ build=new_component.image,
88
+ docker_registry_fqn=new_component.image.docker_registry,
72
89
  workspace_fqn=workspace_fqn,
73
- component_name=component.name,
90
+ component_name=new_component.name,
74
91
  )
75
92
  else:
76
93
  # We'll build image on TrueFoundry servers, upload the source and update image.build_source
77
- logger.info("Uploading code for %s '%s'", component.type, component.name)
94
+ logger.info(
95
+ "Uploading code for %s '%s'", new_component.type, new_component.name
96
+ )
78
97
  client = ServiceFoundryServiceClient()
79
98
  new_component.image.build_source = local_source_to_remote_source(
80
- local_source=component.image.build_source,
99
+ local_source=new_component.image.build_source,
81
100
  workspace_fqn=workspace_fqn,
82
- component_name=component.name,
101
+ component_name=new_component.name,
83
102
  upload_code_package=client.upload_code_package,
84
103
  )
85
- logger.debug("Uploaded code for %s '%s'", component.type, component.name)
104
+ logger.debug(
105
+ "Uploaded code for %s '%s'", new_component.type, new_component.name
106
+ )
86
107
  return new_component
87
108
  return component
88
109
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: truefoundry
3
- Version: 0.7.2
3
+ Version: 0.9.0rc1
4
4
  Summary: TrueFoundry CLI
5
5
  Author-email: TrueFoundry Team <abhishek@truefoundry.com>
6
6
  Requires-Python: <3.14,>=3.8.1
@@ -14,7 +14,7 @@ Requires-Dist: gitpython<4.0.0,>=3.1.43
14
14
  Requires-Dist: importlib-metadata<9.0.0,>=4.11.3
15
15
  Requires-Dist: importlib-resources<7.0.0,>=5.2.0
16
16
  Requires-Dist: mako<2.0.0,>=1.1.6
17
- Requires-Dist: mcp==1.7.1; python_version >= '3.10'
17
+ Requires-Dist: mcp==1.8.1; python_version >= '3.10'
18
18
  Requires-Dist: numpy<3.0.0,>=1.23.0
19
19
  Requires-Dist: openai<2.0.0,>=1.16.2
20
20
  Requires-Dist: packaging<26.0,>=20.0
@@ -31,7 +31,7 @@ Requires-Dist: requirements-parser<0.12.0,>=0.11.0
31
31
  Requires-Dist: rich-click<2.0.0,>=1.2.1
32
32
  Requires-Dist: rich<14.0.0,>=13.7.1
33
33
  Requires-Dist: tqdm<5.0.0,>=4.0.0
34
- Requires-Dist: truefoundry-sdk==0.0.16
34
+ Requires-Dist: truefoundry-sdk<0.2.0,>=0.1.1
35
35
  Requires-Dist: typing-extensions>=4.0
36
36
  Requires-Dist: urllib3<3,>=1.26.18
37
37
  Requires-Dist: yq<4.0.0,>=3.1.0
@@ -1,5 +1,5 @@
1
1
  truefoundry/__init__.py,sha256=VVpO-Awh1v93VOURe7hank8QpeSPc0dCykwr14GOFsw,967
2
- truefoundry/_client.py,sha256=VQEfRvPE7nuqq--q28cpmnIYPG3RH52RSifIFOzzvTg,1420
2
+ truefoundry/_client.py,sha256=Y3qHi_Lg4Sx6GNvsjAHIoAfFr8PJnqgCrXmpNAI3ECg,1417
3
3
  truefoundry/logger.py,sha256=u-YCNjg5HBwE70uQcpjIG64Ghos-K2ulTWaxC03BSj4,714
4
4
  truefoundry/pydantic_v1.py,sha256=jSuhGtz0Mbk1qYu8jJ1AcnIDK4oxUsdhALc4spqstmM,345
5
5
  truefoundry/version.py,sha256=bqiT4Q-VWrTC6P4qfK43mez-Ppf-smWfrl6DcwV7mrw,137
@@ -33,13 +33,13 @@ truefoundry/cli/config.py,sha256=f7z0_gmYZiNImB7Bxz0AnOlrxY2X4lFnX4jYW1I7NHQ,139
33
33
  truefoundry/cli/console.py,sha256=9-dMy4YPisCJQziRKTg8Qa0UJnOGl1soiUnJjsnLDvE,242
34
34
  truefoundry/cli/const.py,sha256=dVHPo1uAiDSSMXwXoT2mR5kNQjExT98QNVRz98Hz_Ts,510
35
35
  truefoundry/cli/display_util.py,sha256=s0_eWUUAK1dbmqW5h_qAG93roH81dh-g1nLjuQVFm6k,5130
36
- truefoundry/cli/util.py,sha256=BjDUG4WU8iF540YdrBtBQI2WqfNAYyk9iuMiKq7b6jk,3705
36
+ truefoundry/cli/util.py,sha256=7DmKXY5OPslPu2LO6vrUUfDtoHeo12sJTDUA0GOi8IM,3922
37
37
  truefoundry/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
38
  truefoundry/common/auth_service_client.py,sha256=N3YxKlx63r6cPZqbgb2lqBOPI69ShB7D7RCIq4FSCjc,7949
39
39
  truefoundry/common/constants.py,sha256=eWcElAYIVb0jnHUAcsHvgnkdKf2E1nCg_Ybbi8ibxF0,4365
40
40
  truefoundry/common/credential_file_manager.py,sha256=1yEk1Zm2xS4G0VDFwKSZ4w0VUrcPWQ1nJnoBaz9xyKA,4251
41
41
  truefoundry/common/credential_provider.py,sha256=_OhJ2XFlDaVsrUO-FyywxctcGGqDdC2pgcvwEKqQD0Q,4071
42
- truefoundry/common/entities.py,sha256=ko33kesGy3vI9NJ5Ganq8HpnaURTOHictr6h75764no,3893
42
+ truefoundry/common/entities.py,sha256=b4R6ss06-ygDS3C4Tqa_GOq5LFKDYbt7x4Mghnfz6yo,4007
43
43
  truefoundry/common/exceptions.py,sha256=jkU0N7hV_P-EhXeud4I5vuB9glXXZSWPf8LcH04mSbw,459
44
44
  truefoundry/common/request_utils.py,sha256=e9qrAQ1MutU7JALDKcucmNd0KQEVBqgW3yx0w1zeHIU,5700
45
45
  truefoundry/common/servicefoundry_client.py,sha256=2fYhdVPSvLXz5C5tosOq86JD8WM3IRUIy1VO9deDxZI,3340
@@ -50,7 +50,7 @@ truefoundry/common/utils.py,sha256=j3QP0uOsaGD_VmDDR68JTwoYE1okkAq6OqpVkzVf48Q,6
50
50
  truefoundry/common/warnings.py,sha256=rs6BHwk7imQYedo07iwh3TWEOywAR3Lqhj0AY4khByg,504
51
51
  truefoundry/deploy/__init__.py,sha256=6D22iiCgd5xlzBaG34q9Cx4rGgwf5qIAKQrOCgaCXYY,2746
52
52
  truefoundry/deploy/python_deploy_codegen.py,sha256=AainOFR20XvhNeztJkLPWGZ40lAT_nwc-ZmG77Kum4o,6525
53
- truefoundry/deploy/_autogen/models.py,sha256=p6sZVLmrFbzujNW6PzjGbPA1OiNhq5TgX5tH0evJvRE,72038
53
+ truefoundry/deploy/_autogen/models.py,sha256=gGH63evQTTnU0fEjtNgCsD0aqIxhdp316GL3Xb65NJk,71461
54
54
  truefoundry/deploy/builder/__init__.py,sha256=nGQiR3r16iumRy7xbVQ6q-k0EApmijspsfVpXDE-9po,4953
55
55
  truefoundry/deploy/builder/constants.py,sha256=amUkHoHvVKzGv0v_knfiioRuKiJM0V0xW0diERgWiI0,508
56
56
  truefoundry/deploy/builder/docker_service.py,sha256=sm7GWeIqyrKaZpxskdLejZlsxcZnM3BTDJr6orvPN4E,3948
@@ -59,8 +59,8 @@ truefoundry/deploy/builder/builders/__init__.py,sha256=tlFLXqyDaKLd4iZbo4Hcu_8gO
59
59
  truefoundry/deploy/builder/builders/dockerfile.py,sha256=XMbMlPUTMPCyaHl7jJQY1ODtlRkpI61PcvgG6Ck5jNc,1522
60
60
  truefoundry/deploy/builder/builders/tfy_notebook_buildpack/__init__.py,sha256=RGWGqY8xOF7vycUPJd10N7ZzahWv24lO0anrOPtLuDU,1796
61
61
  truefoundry/deploy/builder/builders/tfy_notebook_buildpack/dockerfile_template.py,sha256=rQgdvKmAT9HArVW4TAG5yd2QTKRs3S5LJ9RQbc_EkHE,2518
62
- truefoundry/deploy/builder/builders/tfy_python_buildpack/__init__.py,sha256=wnPwIIArn1H_g4CeTNl5W7EaqRq5FbWDnJpd6E4RSDY,1864
63
- truefoundry/deploy/builder/builders/tfy_python_buildpack/dockerfile_template.py,sha256=2KnYTL6yhy0KdiTjSkRfEj_gyCYtmDa2ZsnB06tOpT8,8973
62
+ truefoundry/deploy/builder/builders/tfy_python_buildpack/__init__.py,sha256=_fjqHKn80qKi68SAMMALge7_A6e1sTsQWichw8uoGIw,2025
63
+ truefoundry/deploy/builder/builders/tfy_python_buildpack/dockerfile_template.py,sha256=f4l3fH21E2b8W3-JotMKc0AdPcCxV7LRPxxYJa7z_UQ,9134
64
64
  truefoundry/deploy/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
65
65
  truefoundry/deploy/cli/commands/__init__.py,sha256=f7sXiQK9UuxDJmvBa-QCFNyumUpGGMhZbCxdwJzWXwQ,1116
66
66
  truefoundry/deploy/cli/commands/apply_command.py,sha256=Y2e_C8HVpo8CssVod-3JRz-89qStC5JRaNzJ7O2mRlY,2039
@@ -95,8 +95,7 @@ truefoundry/deploy/lib/session.py,sha256=fLdgR6ZDp8-hFl5NTON4ngnWLsMzGxvKtfpDOOw
95
95
  truefoundry/deploy/lib/util.py,sha256=J7r8San2wKo48A7-BlH2-OKTlBO67zlPjLEhMsL8os0,1059
96
96
  truefoundry/deploy/lib/win32.py,sha256=1RcvPTdlOAJ48rt8rCbE2Ufha2ztRqBAE9dueNXArrY,5009
97
97
  truefoundry/deploy/lib/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
98
- truefoundry/deploy/lib/clients/_mcp_streamable_http.py,sha256=oO_pdgVy0BkksLDZo-arcnaJ4gSaTJmMMZF9u-uF7Mk,10851
99
- truefoundry/deploy/lib/clients/ask_client.py,sha256=PINeyjZF9IMkphU6ln6JVKlFJrHPhkkE5wolFBrp_f8,13589
98
+ truefoundry/deploy/lib/clients/ask_client.py,sha256=77106708EC16wsi2M1n1_5HgOVboEZoq9_obKsf24M0,13494
100
99
  truefoundry/deploy/lib/clients/servicefoundry_client.py,sha256=fmRlPYCimk1ZLbMgdzfJVCbcKRCVnFYL5T3j2uJA0Tc,27037
101
100
  truefoundry/deploy/lib/dao/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
102
101
  truefoundry/deploy/lib/dao/application.py,sha256=oMszpueXPUfTUuN_XdKwoRjQyqAgWHhZ-10cbprCVdM,9226
@@ -108,7 +107,7 @@ truefoundry/deploy/lib/model/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NM
108
107
  truefoundry/deploy/lib/model/entity.py,sha256=Bp9sLB-M5INCpw5lPmFdygHWS1zvnLicnSiSCi2iqhQ,8591
109
108
  truefoundry/deploy/v2/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
110
109
  truefoundry/deploy/v2/lib/__init__.py,sha256=WEiVMZXOVljzEE3tpGJil14liIn_PCDoACJ6b3tZ6sI,188
111
- truefoundry/deploy/v2/lib/deploy.py,sha256=EOqVQTCMrvs_Kwp8Ft0FZYlRErPuFq4eDBDR1qEZ7_E,11767
110
+ truefoundry/deploy/v2/lib/deploy.py,sha256=kAnh6RO4ci7AVjlIoN1Sr5FmcOU7nbkwNvbrS802spY,12625
112
111
  truefoundry/deploy/v2/lib/deploy_workflow.py,sha256=G5BzMIbap8pgDX1eY-TITruUxQdkKhYtBmRwLL6lDeY,14342
113
112
  truefoundry/deploy/v2/lib/deployable_patched_models.py,sha256=xbHFD3pURflvCm8EODPvjfvRrv67mlSrjPUknY8SMB8,4060
114
113
  truefoundry/deploy/v2/lib/models.py,sha256=ogc1UYs1Z2nBdGSKCrde9sk8d0GxFKMkem99uqO5CmM,1148
@@ -376,7 +375,7 @@ truefoundry/workflow/remote_filesystem/__init__.py,sha256=LQ95ViEjJ7Ts4JcCGOxMPs
376
375
  truefoundry/workflow/remote_filesystem/logger.py,sha256=em2l7D6sw7xTLDP0kQSLpgfRRCLpN14Qw85TN7ujQcE,1022
377
376
  truefoundry/workflow/remote_filesystem/tfy_signed_url_client.py,sha256=xcT0wQmQlgzcj0nP3tJopyFSVWT1uv3nhiTIuwfXYeg,12342
378
377
  truefoundry/workflow/remote_filesystem/tfy_signed_url_fs.py,sha256=nSGPZu0Gyd_jz0KsEE-7w_BmnTD8CVF1S8cUJoxaCbc,13305
379
- truefoundry-0.7.2.dist-info/METADATA,sha256=wjXiGA-c9eUYLeH9Qcy2QfYLBmi9FOa1j-R5AABgtgg,2459
380
- truefoundry-0.7.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
381
- truefoundry-0.7.2.dist-info/entry_points.txt,sha256=xVjn7RMN-MW2-9f7YU-bBdlZSvvrwzhpX1zmmRmsNPU,98
382
- truefoundry-0.7.2.dist-info/RECORD,,
378
+ truefoundry-0.9.0rc1.dist-info/METADATA,sha256=Jke--bMLUmA_EhDKqqVyPODCMgbsIHC0W7yH2nRsjqk,2468
379
+ truefoundry-0.9.0rc1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
380
+ truefoundry-0.9.0rc1.dist-info/entry_points.txt,sha256=xVjn7RMN-MW2-9f7YU-bBdlZSvvrwzhpX1zmmRmsNPU,98
381
+ truefoundry-0.9.0rc1.dist-info/RECORD,,
@@ -1,264 +0,0 @@
1
- """
2
- StreamableHTTP Client Transport Module
3
- # From https://github.com/modelcontextprotocol/python-sdk/pull/573
4
-
5
- This module implements the StreamableHTTP transport for MCP clients,
6
- providing support for HTTP POST requests with optional SSE streaming responses
7
- and session management.
8
- """
9
-
10
- import logging
11
- from contextlib import asynccontextmanager
12
- from datetime import timedelta
13
- from typing import Any
14
-
15
- import anyio
16
- import httpx
17
- from httpx_sse import EventSource, aconnect_sse
18
- from mcp.types import (
19
- ErrorData,
20
- JSONRPCError,
21
- JSONRPCMessage,
22
- JSONRPCNotification,
23
- JSONRPCRequest,
24
- )
25
-
26
- logger = logging.getLogger(__name__)
27
-
28
- # Header names
29
- MCP_SESSION_ID_HEADER = "mcp-session-id"
30
- LAST_EVENT_ID_HEADER = "last-event-id"
31
-
32
- # Content types
33
- CONTENT_TYPE_JSON = "application/json"
34
- CONTENT_TYPE_SSE = "text/event-stream"
35
-
36
-
37
- @asynccontextmanager
38
- async def streamablehttp_client(
39
- url: str,
40
- headers: dict[str, Any] | None = None,
41
- timeout: timedelta = timedelta(seconds=30),
42
- sse_read_timeout: timedelta = timedelta(seconds=60 * 5),
43
- ):
44
- """
45
- Client transport for StreamableHTTP.
46
-
47
- `sse_read_timeout` determines how long (in seconds) the client will wait for a new
48
- event before disconnecting. All other HTTP operations are controlled by `timeout`.
49
-
50
- Yields:
51
- Tuple of (read_stream, write_stream, terminate_callback)
52
- """
53
-
54
- read_stream_writer, read_stream = anyio.create_memory_object_stream[
55
- JSONRPCMessage | Exception
56
- ](0)
57
- write_stream, write_stream_reader = anyio.create_memory_object_stream[
58
- JSONRPCMessage
59
- ](0)
60
-
61
- async def get_stream():
62
- """
63
- Optional GET stream for server-initiated messages
64
- """
65
- nonlocal session_id
66
- try:
67
- # Only attempt GET if we have a session ID
68
- if not session_id:
69
- return
70
-
71
- get_headers = request_headers.copy()
72
- get_headers[MCP_SESSION_ID_HEADER] = session_id
73
-
74
- async with aconnect_sse(
75
- client,
76
- "GET",
77
- url,
78
- headers=get_headers,
79
- timeout=httpx.Timeout(timeout.seconds, read=sse_read_timeout.seconds),
80
- ) as event_source:
81
- event_source.response.raise_for_status()
82
- logger.debug("GET SSE connection established")
83
-
84
- async for sse in event_source.aiter_sse():
85
- if sse.event == "message":
86
- try:
87
- message = JSONRPCMessage.model_validate_json(sse.data)
88
- logger.debug(f"GET message: {message}")
89
- await read_stream_writer.send(message)
90
- except Exception as exc:
91
- logger.error(f"Error parsing GET message: {exc}")
92
- await read_stream_writer.send(exc)
93
- else:
94
- logger.warning(f"Unknown SSE event from GET: {sse.event}")
95
- except Exception as exc:
96
- # GET stream is optional, so don't propagate errors
97
- logger.debug(f"GET stream error (non-fatal): {exc}")
98
-
99
- async def post_writer(client: httpx.AsyncClient):
100
- nonlocal session_id
101
- try:
102
- async with write_stream_reader:
103
- async for message in write_stream_reader:
104
- # Add session ID to headers if we have one
105
- post_headers = request_headers.copy()
106
- if session_id:
107
- post_headers[MCP_SESSION_ID_HEADER] = session_id
108
-
109
- logger.debug(f"Sending client message: {message}")
110
-
111
- # Handle initial initialization request
112
- is_initialization = (
113
- isinstance(message.root, JSONRPCRequest)
114
- and message.root.method == "initialize"
115
- )
116
- if (
117
- isinstance(message.root, JSONRPCNotification)
118
- and message.root.method == "notifications/initialized"
119
- ):
120
- tg.start_soon(get_stream)
121
-
122
- async with client.stream(
123
- "POST",
124
- url,
125
- json=message.model_dump(
126
- by_alias=True, mode="json", exclude_none=True
127
- ),
128
- headers=post_headers,
129
- ) as response:
130
- if response.status_code == 202:
131
- logger.debug("Received 202 Accepted")
132
- continue
133
- # Check for 404 (session expired/invalid)
134
- if response.status_code == 404:
135
- if isinstance(message.root, JSONRPCRequest):
136
- jsonrpc_error = JSONRPCError(
137
- jsonrpc="2.0",
138
- id=message.root.id,
139
- error=ErrorData(
140
- code=32600,
141
- message="Session terminated",
142
- ),
143
- )
144
- await read_stream_writer.send(
145
- JSONRPCMessage(jsonrpc_error)
146
- )
147
- continue
148
-
149
- if not response.is_success:
150
- _response_content = await response.aread()
151
- logger.error(
152
- f"Response: {response.status_code} {_response_content}"
153
- )
154
- response.raise_for_status()
155
-
156
- # Extract session ID from response headers
157
- if is_initialization:
158
- new_session_id = response.headers.get(MCP_SESSION_ID_HEADER)
159
- if new_session_id:
160
- session_id = new_session_id
161
- logger.info(f"Received session ID: {session_id}")
162
-
163
- # Handle different response types
164
- content_type = response.headers.get("content-type", "").lower()
165
-
166
- if content_type.startswith(CONTENT_TYPE_JSON):
167
- try:
168
- content = await response.aread()
169
- json_message = JSONRPCMessage.model_validate_json(
170
- content
171
- )
172
- await read_stream_writer.send(json_message)
173
- except Exception as exc:
174
- logger.error(f"Error parsing JSON response: {exc}")
175
- await read_stream_writer.send(exc)
176
-
177
- elif content_type.startswith(CONTENT_TYPE_SSE):
178
- # Parse SSE events from the response
179
- try:
180
- event_source = EventSource(response)
181
- async for sse in event_source.aiter_sse():
182
- if sse.event == "message":
183
- try:
184
- await read_stream_writer.send(
185
- JSONRPCMessage.model_validate_json(
186
- sse.data
187
- )
188
- )
189
- except Exception as exc:
190
- logger.exception("Error parsing message")
191
- await read_stream_writer.send(exc)
192
- else:
193
- logger.warning(f"Unknown event: {sse.event}")
194
-
195
- except Exception as e:
196
- logger.exception("Error reading SSE stream:")
197
- await read_stream_writer.send(e)
198
-
199
- else:
200
- # For 202 Accepted with no body
201
- if response.status_code == 202:
202
- logger.debug("Received 202 Accepted")
203
- continue
204
-
205
- error_msg = f"Unexpected content type: {content_type}"
206
- logger.error(error_msg)
207
- await read_stream_writer.send(ValueError(error_msg))
208
-
209
- except Exception as exc:
210
- logger.error(f"Error in post_writer: {exc}")
211
- finally:
212
- await read_stream_writer.aclose()
213
- await write_stream.aclose()
214
-
215
- async def terminate_session():
216
- """
217
- Terminate the session by sending a DELETE request.
218
- """
219
- nonlocal session_id
220
- if not session_id:
221
- return # No session to terminate
222
-
223
- try:
224
- delete_headers = request_headers.copy()
225
- delete_headers[MCP_SESSION_ID_HEADER] = session_id
226
-
227
- response = await client.delete(
228
- url,
229
- headers=delete_headers,
230
- )
231
-
232
- if response.status_code == 405:
233
- # Server doesn't allow client-initiated termination
234
- logger.debug("Server does not allow session termination")
235
- elif response.status_code != 200:
236
- logger.warning(f"Session termination failed: {response.status_code}")
237
- except Exception as exc:
238
- logger.warning(f"Session termination failed: {exc}")
239
-
240
- async with anyio.create_task_group() as tg:
241
- try:
242
- logger.debug(f"Connecting to StreamableHTTP endpoint: {url}")
243
- # Set up headers with required Accept header
244
- request_headers = {
245
- "Accept": f"{CONTENT_TYPE_JSON}, {CONTENT_TYPE_SSE}",
246
- "Content-Type": CONTENT_TYPE_JSON,
247
- **(headers or {}),
248
- }
249
- # Track session ID if provided by server
250
- session_id: str | None = None
251
-
252
- async with httpx.AsyncClient(
253
- headers=request_headers,
254
- timeout=httpx.Timeout(timeout.seconds, read=sse_read_timeout.seconds),
255
- follow_redirects=True,
256
- ) as client:
257
- tg.start_soon(post_writer, client)
258
- try:
259
- yield read_stream, write_stream, terminate_session
260
- finally:
261
- tg.cancel_scope.cancel()
262
- finally:
263
- await read_stream_writer.aclose()
264
- await write_stream.aclose()